UNPKG

ts-bacnet

Version:

The BACnet protocol library written in pure JavaScript.

1,084 lines 80.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.bacnetErrorToString = exports.BacnetError = void 0; // Util Modules const events_1 = __importDefault(require("events")); const debug_1 = __importDefault(require("debug")); // Local Modules const transport_1 = __importDefault(require("./transport")); const baServices = __importStar(require("./services")); const baAsn1 = __importStar(require("./asn1")); const baApdu = __importStar(require("./apdu")); const baNpdu = __importStar(require("./npdu")); const baBvlc = __importStar(require("./bvlc")); const baEnum = __importStar(require("./enum")); const EventEmitter = events_1.default.EventEmitter; const debug = (0, debug_1.default)("bacnet:client:debug"); const trace = (0, debug_1.default)("bacnet:client:trace"); 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", }; //TODO is this correct? const errorReasonEnums = { BacnetAbort: baEnum.RejectReason, another: baEnum.AbortReason, }; class BacnetError extends Error { constructor(message, bacnetErrorClass, bacnetErrorCode) { super(message); this.bacnetErrorClass = bacnetErrorClass; this.bacnetErrorCode = bacnetErrorCode; } } exports.BacnetError = BacnetError; function bacnetErrorToString(err) { var _a; const isError = err instanceof Error || err instanceof BacnetError; if (!isError) return; const message = err.message; const reasonClass = message === null || message === void 0 ? void 0 : message.split(" - ")[0]; const reason = +((_a = message === null || message === void 0 ? void 0 : message.split("Reason:")[1]) === null || _a === void 0 ? void 0 : _a.split(" ")[0]); // if (typeof reason !== "number") return; if (!errorReasonEnums[reasonClass]) return; const reasonString = errorReasonEnums[reasonClass][reason]; const alternativeReasonString = errorReasonEnums["another"][reason]; return (reasonClass + ": " + reasonString + "or alternatively it could be " + alternativeReasonString); } exports.bacnetErrorToString = bacnetErrorToString; 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 transport_1.default({ 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); const result = baServices.error.decode(buffer, offset); trace("Received error:", result); if (!result) { return debug("Couldn`t decode Error"); } const err = new BacnetError(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 BacnetError("BacnetAbort - Reason:" + reason); err.bacnetAbortReason = reason; //@ts-ignore err.bacnetErrorClass = baEnum.AbortReason; 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( //@ts-ignore 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('ts-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('ts-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 hasForwardedFrom = // typeof receiver !== "string" && receiver.forwardedFrom; // const buffer = this._getBuffer(!!hasForwardedFrom); //@ts-ignore const buffer = this._getBuffer(receiver && receiver.forwardedFrom); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE, //@ts-ignore 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('ts-bacnet'); * const client = new bacnet(); * * client.timeSync('192.168.1.43', new Date()); */ timeSync(receiver, dateTime) { const buffer = this._getBuffer(!!(typeof receiver !== "string" && 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('ts-bacnet'); * const client = new bacnet(); * * client.timeSyncUTC('192.168.1.43', new Date()); */ timeSyncUTC(receiver, dateTime) { const buffer = this._getBuffer(!!(typeof receiver !== "string" && 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('ts-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) { //@ts-ignore 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(!!(typeof receiver !== "string" && receiver.forwardedFrom)); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, //@ts-ignore receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); const type = baEnum.PduType.CONFIRMED_REQUEST | //@ts-ignore (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('ts-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) { //@ts-ignore 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(!!(typeof receiver !== "string" && receiver.forwardedFrom)); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, //@ts-ignore Todo somehow types do not match at all 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('ts-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) { //@ts-ignore 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 isForwarded = typeof receiver !== "string" && receiver.forwardedFrom; const buffer = this._getBuffer(!!isForwarded); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, //@ts-ignore Todo somehow types do not match at all receiver, null, DEFAULT_HOP_COUNT, baEnum.NetworkLayerMessageType.WHO_IS_ROUTER_TO_NETWORK, 0); const type = baEnum.PduType.CONFIRMED_REQUEST | //@ts-ignore (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 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 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('ts-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) { //@ts-ignore 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(!!(typeof receiver !== "string" && receiver.forwardedFrom)); baNpdu.encode(buffer, baEnum.NpduControlPriority.NORMAL_MESSAGE | baEnum.NpduControlBit.EXPECTING_REPLY, //@ts-ignore 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('ts-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('ts-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(!!(typeof receiver !== "string" && 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('ts-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(!!(typeof receiver !== "string" && 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(!!(typeof receiver !== "string" && 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(!!(typeof receiver !== "string" && receiver.forwardedFrom)); baNp