'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.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} values.priority - The priority of the updated property.
* @example
* const bacnet = require('node-bacnet');
* const client = new bacnet();
*
* client.unconfirmedCOVNotification(
* '127.0.0.1',
* 3,
* 433,
* {type: 2, instance: 122},
* 120,
* [
* {
* property: {id: 85},
* value: [{type: baEnum.ApplicationTag.REAL, value: 12.3}]
* },
* {
* property: {id: 111},
* value: [{type: baEnum.ApplicationTag.BIT_STRING, value: 0xFFFF}]
* }
* ]);
*/
unconfirmedCOVNotification(receiver, subscriberProcessId, initiatingDeviceId, monitoredObjectId, timeRemaining, values) {
const buffer = this._getBuffer();
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UNCONFIRMED_COV_NOTIFICATION);
baServices.covNotify.encode(buffer, subscriberProcessId, initiatingDeviceId, monitoredObjectId, timeRemaining, values);
baBvlc.encode(buffer.buffer, baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU, buffer.offset);
this._transport.send(buffer.buffer, buffer.offset, receiver);
}
/**
*
* @param receiver
* @param objectId
* @param values
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CREATE_OBJECT, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
baServices.createObject.encode(buffer, objectId, values);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
*
* @param receiver
* @param objectId
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.DELETE_OBJECT, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
baServices.deleteObject.encode(buffer, objectId);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
*
* @param receiver
* @param objectId
* @param reference
* @param values
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
*
* @param receiver
* @param objectId
* @param reference
* @param values
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
*
* @param receiver
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.GET_ALARM_SUMMARY, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
const result = baServices.alarmSummary.decode(data.buffer, data.offset, data.length);
if (!result) {
return void next(new Error('INVALID_DECODING'));
}
next(null, result);
});
}
/**
*
* @param receiver
* @param objectId
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.GET_EVENT_INFORMATION, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
baAsn1.encodeContextObjectId(buffer, 0, objectId.type, objectId.instance);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
const result = baServices.eventInformation.decode(data.buffer, data.offset, data.length);
if (!result) {
return void next(new Error('INVALID_DECODING'));
}
next(null, result);
});
}
/**
*
* @param receiver
* @param objectId
* @param eventState
* @param ackText
* @param evTimeStamp
* @param ackTimeStamp
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.ACKNOWLEDGE_ALARM, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
baServices.alarmAcknowledge.encode(buffer, 57, objectId, eventState, ackText, evTimeStamp, ackTimeStamp);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
*
* @param receiver
* @param vendorId
* @param serviceNumber
* @param data
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CONFIRMED_PRIVATE_TRANSFER, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
baServices.privateTransfer.encode(buffer, vendorId, serviceNumber, data);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
*
* @param receiver
* @param vendorId
* @param serviceNumber
* @param data
*/
unconfirmedPrivateTransfer(receiver, vendorId, serviceNumber, data) {
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UNCONFIRMED_PRIVATE_TRANSFER);
baServices.privateTransfer.encode(buffer, vendorId, serviceNumber, data);
this.sendBvlc(receiver, buffer);
}
/**
*
* @param receiver
* @param acknowledgmentFilter
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.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);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
const result = baServices.getEnrollmentSummary.decodeAcknowledge(data.buffer, data.offset, data.length);
if (!result) {
return void next(new Error('INVALID_DECODING'));
}
next(null, result);
});
}
/**
*
* @param receiver
* @param eventNotification
*/
unconfirmedEventNotification(receiver, eventNotification) {
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.UNCONFIRMED_EVENT_NOTIFICATION);
baServices.eventNotifyData.encode(buffer, eventNotification);
this.sendBvlc(receiver, buffer);
}
/**
*
* @param receiver
* @param eventNotification
* @param options
* @param 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(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, receiver);
baApdu.encodeConfirmedServiceRequest(buffer, baEnum.PduType.CONFIRMED_REQUEST, baEnum.ConfirmedServiceChoice.CONFIRMED_EVENT_NOTIFICATION, settings.maxSegments, settings.maxApdu, settings.invokeId, 0, 0);
baServices.eventNotifyData.encode(buffer, eventNotification);
this.sendBvlc(receiver, buffer);
this._addCallback(settings.invokeId, (err, data) => {
if (err) {
return void next(err);
}
next();
});
}
/**
* The readPropertyResponse call sends a response with information about one of our properties.
* @function bacnet.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.PduType.COMPLEX_ACK, baEnum.ConfirmedServiceChoice.READ_PROPERTY, invokeId);
baServices.readProperty.encodeAcknowledge(buffer, objectId, property.id, property.index, value);
this.sendBvlc(receiver, buffer);
}
readPropertyMultipleResponse(receiver, invokeId, values) {
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeComplexAck(buffer, baEnum.PduType.COMPLEX_ACK, baEnum.ConfirmedServiceChoice.READ_PROPERTY_MULTIPLE, invokeId);
baServices.readPropertyMultiple.encodeAcknowledge(buffer, values);
this.sendBvlc(receiver, buffer);
}
/**
* The iAmResponse command is sent as a reply to a whoIs request.
* @function bacnet.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.
*/
iAmResponse(receiver, deviceId, segmentation, vendorId) {
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.I_AM);
baServices.iAm.encode(buffer, deviceId, this._transport.getMaxPayload(), segmentation, vendorId);
this.sendBvlc(receiver, buffer);
}
/**
*
* @param receiver
* @param deviceId
* @param objectId
* @param objectName
*/
iHaveResponse(receiver, deviceId, objectId, objectName) {
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.EecodeUnconfirmedServiceRequest(buffer, baEnum.PduType.UNCONFIRMED_REQUEST, baEnum.UnconfirmedServiceChoice.I_HAVE);
baServices.iHave(buffer, deviceId, objectId, objectName);
this.sendBvlc(receiver, buffer);
}
/**
*
* @param receiver
* @param service
* @param invokeId
*/
simpleAckResponse(receiver, service, invokeId) {
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeSimpleAck(buffer, baEnum.PduType.SIMPLE_ACK, service, invokeId);
this.sendBvlc(receiver, buffer);
}
/**
*
* @param receiver
* @param service
* @param invokeId
* @param errorClass
* @param errorCode
*/
errorResponse(receiver, service, invokeId, errorClass, errorCode) {
trace(`error response on ${JSON.stringify(receiver)} service: ${JSON.stringify(service)} invokeId: ${invokeId} errorClass: ${errorClass} errorCode: ${errorCode}`);
trace(`error message ${baServices.error.buildMessage({ 'class': errorClass, 'code': errorCode })}`);
const buffer = this._getBuffer(receiver && receiver.forwardedFrom);
baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, receiver);
baApdu.encodeError(buffer, baEnum.PduType.ERROR, service, invokeId);
baServices.error.encode(buffer, errorClass, errorCode);
this.sendBvlc(receiver, buffer);
}
/**
*
* @param receiver
* @param buffer
*/
sendBvlc(receiver, buffer) {
if (typeof receiver === 'string') {
receiver = {
address: receiver
};
}
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 bacnet.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);
}
/**
* Unloads the current bacnet instance and closes the underlying UDP socket.
* @function bacnet.close
* @example
* const bacnet = require('node-bacnet');
* const client = new bacnet();
*
* client.close();
*/
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.ApplicationTag.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,
};
}
}
module.exports = Client;