From 635e41954ffd95a4b0cff252581f380981d4d278 Mon Sep 17 00:00:00 2001 From: Fabio Huser Date: Mon, 21 Aug 2017 08:03:24 +0200 Subject: [PATCH] feat(bacnet-service): implement decoding functionality for COV and CreateObject --- lib/bacnet-asn1.js | 8 +- lib/bacnet-client.js | 9 +- lib/bacnet-services.js | 246 +++++++++++++++++++++++------- test/unit/bacnet-services.spec.js | 172 +++++++++++++++++++++ 4 files changed, 367 insertions(+), 68 deletions(-) diff --git a/lib/bacnet-asn1.js b/lib/bacnet-asn1.js index 6d4c7ce..a30897d 100644 --- a/lib/bacnet-asn1.js +++ b/lib/bacnet-asn1.js @@ -1651,14 +1651,12 @@ var decodeContextDate = function(buffer, offset, tagNumber) { }; }; -var decodeContextObjectId = function(buffer, offset, tagNumber) { +module.exports.decodeContextObjectId = function(buffer, offset, tagNumber) { var result = decodeIsContextTagWithLength(buffer, offset, tagNumber); if (!result.value) return; var decodedValue = decodeObjectId(buffer, offset + result.len); - return { - len: result.len + decodedValue.len, - value: decodedValue.value - }; + decodedValue.len = decodedValue.len + result.len; + return decodedValue; }; var bacappDecodeContextData = function(buffer, offset, maxApduLen, propertyTag) { diff --git a/lib/bacnet-client.js b/lib/bacnet-client.js index e730d04..858f6b7 100644 --- a/lib/bacnet-client.js +++ b/lib/bacnet-client.js @@ -211,10 +211,9 @@ module.exports = function(options) { if (!result) return debug('Received invalid readRange message'); self.emit('readRange', {address: address, invokeId: invokeId, request: result}); } else if (service === baEnum.BacnetConfirmedServices.SERVICE_CONFIRMED_CREATE_OBJECT) { - // TODO: Implement - // result = baServices.decodeCreateObject(buffer, offset, length); - // if (!result) return debug('Received invalid createObject message'); - // self.emit('createObject', {address: address, invokeId: invokeId, request: result}); + result = baServices.decodeCreateObject(buffer, offset, length); + if (!result) return debug('Received invalid createObject message'); + self.emit('createObject', {address: address, invokeId: invokeId, request: result}); } else if (service === baEnum.BacnetConfirmedServices.SERVICE_CONFIRMED_DELETE_OBJECT) { result = baServices.decodeDeleteObject(buffer, offset, length); if (!result) return debug('Received invalid deleteObject message'); @@ -727,7 +726,7 @@ module.exports = function(options) { var invokeId = getInvokeId(); baNpdu.encode(buffer, baEnum.BacnetNpduControls.PRIORITY_NORMAL_MESSAGE | baEnum.BacnetNpduControls.EXPECTING_REPLY, address); baAdpu.encodeConfirmedServiceRequest(buffer, baEnum.BacnetPduTypes.PDU_TYPE_CONFIRMED_SERVICE_REQUEST, baEnum.BacnetConfirmedServices.SERVICE_CONFIRMED_CREATE_OBJECT, maxSegments, baEnum.BacnetMaxAdpu.MAX_APDU1476, invokeId, 0, 0); - baServices.encodeCreateProperty(buffer, objectId, valueList); + baServices.encodeCreateObject(buffer, objectId, valueList); baBvlc.encode(buffer.buffer, baEnum.BacnetBvlcFunctions.BVLC_ORIGINAL_UNICAST_NPDU, buffer.offset); transport.send(buffer.buffer, buffer.offset, address); addCallback(invokeId, function(err, data) { diff --git a/lib/bacnet-services.js b/lib/bacnet-services.js index b17de7d..a674cfe 100644 --- a/lib/bacnet-services.js +++ b/lib/bacnet-services.js @@ -1383,23 +1383,7 @@ module.exports.encodeDeleteObject = function(buffer, objectId) { baAsn1.encodeApplicationObjectId(buffer, objectId.type, objectId.instance); }; -// TODO: Implement decoding for following functions -module.exports.encodeIhaveBroadcast = function(buffer, deviceId, objectId, objectName) { - baAsn1.encodeApplicationObjectId(buffer, deviceId.type, deviceId.instance); - baAsn1.encodeApplicationObjectId(buffer, objectId.type, objectId.instance); - baAsn1.encodeApplicationCharacterString(buffer, objectName); -}; - -module.exports.encodeAlarmAcknowledge = function(buffer, ackProcessIdentifier, eventObjectIdentifier, eventStateAcked, ackSource, eventTimeStamp, ackTimeStamp) { - baAsn1.encodeContextUnsigned(buffer, 0, ackProcessIdentifier); - baAsn1.encodeContextObjectId(buffer, 1, eventObjectIdentifier.type, eventObjectIdentifier.instance); - baAsn1.encodeContextEnumerated(buffer, 2, eventStateAcked); - baAsn1.bacappEncodeContextTimestamp(buffer, 3, eventTimeStamp); - baAsn1.encodeContextCharacterString(buffer, 4, ackSource); - baAsn1.bacappEncodeContextTimestamp(buffer, 5, ackTimeStamp); -}; - -module.exports.encodeCreateProperty = function(buffer, objectId, valueList) { +module.exports.encodeCreateObject = function(buffer, objectId, valueList) { baAsn1.encodeOpeningTag(buffer, 0); baAsn1.encodeContextObjectId(buffer, 1, objectId.type, objectId.instance); baAsn1.encodeClosingTag(buffer, 0); @@ -1421,6 +1405,192 @@ module.exports.encodeCreateProperty = function(buffer, objectId, valueList) { baAsn1.encodeClosingTag(buffer, 1); }; +module.exports.decodeCreateObject = function(buffer, offset, apduLen) { + var len = 0; + var result; + var decodedValue; + var objectId; + var valuesRefs = []; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + if ((result.tagNumber === 0) && (apduLen > len)) { + apduLen -= len; + if (apduLen < 4) return; + decodedValue = baAsn1.decodeContextObjectId(buffer, offset + len, 1); + len += decodedValue.len; + objectId = {type: decodedValue.objectType, instance: decodedValue.instance}; + } else { + return; + } + if (baAsn1.decodeIsClosingTag(buffer, offset + len)) { + len++; + } + if (!baAsn1.decodeIsOpeningTagNumber(buffer, offset + len, 1)) return; + len++; + while ((apduLen - len) > 1) { + var newEntry = {}; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + if (result.tagNumber !== 0) return; + decodedValue = baAsn1.decodeEnumerated(buffer, offset + len, result.value); + len += decodedValue.len; + var propertyId = decodedValue.value; + var arraIndex = baAsn1.BACNET_ARRAY_ALL; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + if (result.tagNumber === 1) { + decodedValue = baAsn1.decodeUnsigned(buffer, offset + len, result.value); + len += decodedValue.len; + arraIndex += decodedValue.value; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + } + newEntry.property = {propertyId: propertyId, arrayIndex: arraIndex}; + if ((result.tagNumber === 2) && (baAsn1.decodeIsOpeningTag(buffer, offset + len - 1))) { + var values = []; + while (!baAsn1.decodeIsClosingTag(buffer, offset + len)) { + decodedValue = baAsn1.bacappDecodeApplicationData(buffer, offset + len, apduLen + offset, objectId.type, propertyId); + if (!decodedValue) return; + len += decodedValue.len; + values.push(decodedValue); + } + len++; + newEntry.value = values; + } else { + return; + } + valuesRefs.push(newEntry); + } + if (!baAsn1.decodeIsClosingTagNumber(buffer, offset + len, 1)) return; + len++; + return { + len: len, + objectId: objectId, + values: valuesRefs + }; +}; + +module.exports.encodeCOVNotify = function(buffer, subscriberProcessIdentifier, initiatingDeviceIdentifier, monitoredObjectIdentifier, timeRemaining, values) { + baAsn1.encodeContextUnsigned(buffer, 0, subscriberProcessIdentifier); + baAsn1.encodeContextObjectId(buffer, 1, baEnum.BacnetObjectTypes.OBJECT_DEVICE, initiatingDeviceIdentifier); + baAsn1.encodeContextObjectId(buffer, 2, monitoredObjectIdentifier.type, monitoredObjectIdentifier.instance); + baAsn1.encodeContextUnsigned(buffer, 3, timeRemaining); + baAsn1.encodeOpeningTag(buffer, 4); + values.forEach(function(value) { + baAsn1.encodeContextEnumerated(buffer, 0, value.property.propertyIdentifier); + if (value.property.propertyArrayIndex === baAsn1.BACNET_ARRAY_ALL) { + baAsn1.encodeContextUnsigned(buffer, 1, value.property.propertyArrayIndex); + } + baAsn1.encodeOpeningTag(buffer, 2); + value.value.forEach(function(v) { + baAsn1.bacappEncodeApplicationData(buffer, v); + }); + baAsn1.encodeClosingTag(buffer, 2); + if (value.priority === baAsn1.BACNET_NO_PRIORITY) { + baAsn1.encodeContextUnsigned(buffer, 3, value.priority); + } + // TODO: Handle to too large telegrams -> ADPU limit + }); + baAsn1.encodeClosingTag(buffer, 4); +}; + +module.exports.decodeCOVNotify = function(buffer, offset, apduLen) { + var len = 0; + var result; + var decodedValue; + if (!baAsn1.decodeIsContextTag(buffer, offset + len, 0)) return; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeUnsigned(buffer, offset + len, result.value); + len += decodedValue.len; + var subscriberProcessIdentifier = decodedValue.value; + if (!baAsn1.decodeIsContextTag(buffer, offset + len, 1)) return; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeObjectId(buffer, offset + len); + len += decodedValue.len; + var initiatingDeviceIdentifier = {type: decodedValue.objectType, instance: decodedValue.instance}; + if (!baAsn1.decodeIsContextTag(buffer, offset + len, 2)) return; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeObjectId(buffer, offset + len); + len += decodedValue.len; + var monitoredObjectIdentifier = {type: decodedValue.objectType, instance: decodedValue.instance}; + if (!baAsn1.decodeIsContextTag(buffer, offset + len, 3)) return; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeUnsigned(buffer, offset + len, result.value); + len += decodedValue.len; + var timeRemaining = decodedValue.value; + if (!baAsn1.decodeIsOpeningTagNumber(buffer, offset + len, 4)) return; + len++; + var values = []; + while ((apduLen - len) > 1 && !baAsn1.decodeIsClosingTagNumber(buffer, offset + len, 4)) { + var newEntry = {}; + newEntry.property = {}; + if (!baAsn1.decodeIsContextTag(buffer, offset + len, 0)) return; + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeEnumerated(buffer, offset + len, result.value); + len += decodedValue.len; + newEntry.property.propertyId = decodedValue.value; + if (baAsn1.decodeIsContextTag(buffer, offset + len, 1)) { + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeUnsigned(buffer, offset + len, result.value); + len += decodedValue.len; + newEntry.property.arrayIndex = decodedValue.value; + } else { + newEntry.property.arrayIndex = baAsn1.BACNET_ARRAY_ALL; + } + if (!baAsn1.decodeIsOpeningTagNumber(buffer, offset + len, 2)) return; + len++; + var properties = []; + while ((apduLen - len) > 1 && !baAsn1.decodeIsClosingTagNumber(buffer, offset + len, 2)) { + decodedValue = baAsn1.bacappDecodeApplicationData(buffer, offset + len, apduLen + offset, monitoredObjectIdentifier.type, newEntry.property.propertyIdentifier); + if (!decodedValue) return; + len += decodedValue.len; + properties.push(decodedValue); + } + newEntry.value = properties; + len++; + if (baAsn1.decodeIsContextTag(buffer, offset + len, 3)) { + result = baAsn1.decodeTagNumberAndValue(buffer, offset + len); + len += result.len; + decodedValue = baAsn1.decodeUnsigned(buffer, offset + len, result.value); + len += decodedValue.len; + newEntry.priority = decodedValue.value; + } else { + newEntry.priority = baAsn1.BACNET_NO_PRIORITY; + } + values.push(newEntry); + } + return { + len: len, + subscriberProcessIdentifier: subscriberProcessIdentifier, + initiatingDeviceIdentifier: initiatingDeviceIdentifier, + monitoredObjectIdentifier: monitoredObjectIdentifier, + timeRemaining: timeRemaining, + values: values + }; +}; + +// TODO: Implement decoding for following functions +module.exports.encodeIhaveBroadcast = function(buffer, deviceId, objectId, objectName) { + baAsn1.encodeApplicationObjectId(buffer, deviceId.type, deviceId.instance); + baAsn1.encodeApplicationObjectId(buffer, objectId.type, objectId.instance); + baAsn1.encodeApplicationCharacterString(buffer, objectName); +}; + +module.exports.encodeAlarmAcknowledge = function(buffer, ackProcessIdentifier, eventObjectIdentifier, eventStateAcked, ackSource, eventTimeStamp, ackTimeStamp) { + baAsn1.encodeContextUnsigned(buffer, 0, ackProcessIdentifier); + baAsn1.encodeContextObjectId(buffer, 1, eventObjectIdentifier.type, eventObjectIdentifier.instance); + baAsn1.encodeContextEnumerated(buffer, 2, eventStateAcked); + baAsn1.bacappEncodeContextTimestamp(buffer, 3, eventTimeStamp); + baAsn1.encodeContextCharacterString(buffer, 4, ackSource); + baAsn1.bacappEncodeContextTimestamp(buffer, 5, ackTimeStamp); +}; + module.exports.encodeAddListElement = function(buffer, objectId, propertyId, arrayIndex, valueList) { baAsn1.encodeContextObjectId(buffer, 0, objectId.type, objectId.instance); baAsn1.encodeContextEnumerated(buffer, 1, propertyId); @@ -1477,50 +1647,10 @@ module.exports.encodeLifeSafetyOperation = function(buffer, processId, requestin baAsn1.encodeContextObjectId(buffer, 3, targetObject.type, targetObject.instance); }; -module.exports.encodePrivateTransferConfirmed = function(buffer, vendorID, serviceNumber, data) { - baAsn1.encodeContextUnsigned(buffer, 0, vendorID); - baAsn1.encodeContextUnsigned(buffer, 1, serviceNumber); - baAsn1.encodeOpeningTag(buffer, 2); - buffer.Add(data, data.length); - baAsn1.encodeClosingTag(buffer, 2); -}; - -module.exports.encodePrivateTransferUnconfirmed = function(buffer, vendorID, serviceNumber, data) { - baAsn1.encodeContextUnsigned(buffer, 0, vendorID); - baAsn1.encodeContextUnsigned(buffer, 1, serviceNumber); - baAsn1.encodeOpeningTag(buffer, 2); - buffer.Add(data, data.length); - baAsn1.encodeClosingTag(buffer, 2); -}; - -module.exports.encodePrivateTransferAcknowledge = function(buffer, vendorID, serviceNumber, data) { +module.exports.encodePrivateTransfer = function(buffer, vendorID, serviceNumber, data) { baAsn1.encodeContextUnsigned(buffer, 0, vendorID); baAsn1.encodeContextUnsigned(buffer, 1, serviceNumber); baAsn1.encodeOpeningTag(buffer, 2); buffer.Add(data, data.length); baAsn1.encodeClosingTag(buffer, 2); }; - -module.exports.encodeCOVNotify = function(buffer, subscriberProcessIdentifier, initiatingDeviceIdentifier, monitoredObjectIdentifier, timeRemaining, values) { - baAsn1.encodeContextUnsigned(buffer, 0, subscriberProcessIdentifier); - baAsn1.encodeContextObjectId(buffer, 1, baEnum.BacnetObjectTypes.OBJECT_DEVICE, initiatingDeviceIdentifier); - baAsn1.encodeContextObjectId(buffer, 2, monitoredObjectIdentifier.type, monitoredObjectIdentifier.instance); - baAsn1.encodeContextUnsigned(buffer, 3, timeRemaining); - baAsn1.encodeOpeningTag(buffer, 4); - values.forEach(function(value) { - baAsn1.encodeContextEnumerated(buffer, 0, value.property.propertyIdentifier); - if (value.property.propertyArrayIndex === baAsn1.BACNET_ARRAY_ALL) { - baAsn1.encodeContextUnsigned(buffer, 1, value.property.propertyArrayIndex); - } - baAsn1.encodeOpeningTag(buffer, 2); - value.value.forEach(function(v) { - baAsn1.bacappEncodeApplicationData(buffer, v); - }); - baAsn1.encodeClosingTag(buffer, 2); - if (value.priority === baAsn1.BACNET_NO_PRIORITY) { - baAsn1.encodeContextUnsigned(buffer, 3, value.priority); - } - // TODO: Handle to too large telegrams -> ADPU limit - }); - baAsn1.encodeClosingTag(buffer, 4); -}; diff --git a/test/unit/bacnet-services.spec.js b/test/unit/bacnet-services.spec.js index def8e20..1346fda 100644 --- a/test/unit/bacnet-services.spec.js +++ b/test/unit/bacnet-services.spec.js @@ -1594,4 +1594,176 @@ describe('bacstack - Services layer', function() { }); }); }); + + describe('CreateObject', function() { + it('should successfully encode and decode', function() { + var buffer = utils.getBuffer(); + var date = new Date(1, 1, 1); + var time = new Date(1, 1, 1); + time.setMilliseconds(990); + baServices.encodeCreateObject(buffer, {type: 1, instance: 10}, [ + {property: {propertyIdentifier: 81, propertyArrayIndex: 0xFFFFFFFF}, value: [ + {type: 1, value: true}, + {type: 1, value: false}, + {type: 2, value: 1}, + {type: 2, value: 1000}, + {type: 2, value: 1000000}, + {type: 2, value: 1000000000}, + {type: 3, value: -1}, + {type: 3, value: -1000}, + {type: 3, value: -1000000}, + {type: 3, value: -1000000000}, + {type: 4, value: 0.1}, + {type: 5, value: 100.121212}, + {type: 6, value: [1, 2, 100, 200]}, + {type: 7, value: 'Test1234$'}, + {type: 8, value: {bitsUsed: 0, value: []}}, + {type: 8, value: {bitsUsed: 24, value: [0xAA, 0xAA, 0xAA]}}, + {type: 9, value: 4}, + {type: 10, value: date}, + {type: 11, value: time} + ], priority: 0}, + {property: {propertyIdentifier: 82, propertyArrayIndex: 0}, value: [ + {type: 12, value: {type: 3, instance: 0}} + ], priority: 0} + ]); + var result = baServices.decodeCreateObject(buffer.buffer, 0, buffer.offset); + delete result.len; + result.values[0].value[10].value = Math.floor(result.values[0].value[10].value * 1000) / 1000; + expect(result).to.deep.equal({ + objectId: { + type: 1, + instance: 10 + }, + values: [ + { + property: { + arrayIndex: 0xFFFFFFFF, + propertyId: 81 + }, + value: [ + {type: 1, value: true, len: 1}, + {type: 1, value: false, len: 1}, + {type: 2, value: 1, len: 2}, + {type: 2, value: 1000, len: 3}, + {type: 2, value: 1000000, len: 4}, + {type: 2, value: 1000000000, len: 5}, + {type: 3, value: -1, len: 2}, + {type: 3, value: -1000, len: 3}, + {type: 3, value: -1000000, len: 4}, + {type: 3, value: -1000000000, len: 5}, + {type: 4, value: 0.1, len: 5}, + {type: 5, value: 100.121212, len: 10}, + {type: 6, value: [1, 2, 100, 200], len: 5}, + {type: 7, value: 'Test1234$', encoding: 0, len: 12}, + {type: 8, value: {bitsUsed: 0, value: []}, len: 2}, + {type: 8, value: {bitsUsed: 24, value: [0xAA, 0xAA, 0xAA]}, len: 5}, + {type: 9, value: 4, len: 2}, + {type: 10, value: date, len: 5}, + {type: 11, value: time, len: 5} + ] + }, + { + property: { + arrayIndex: 0xFFFFFFFF, + propertyId: 82 + }, + value: [ + {type: 12, value: {type: 3, instance: 0}, len: 5} + ] + } + ] + }); + }); + }); + + describe('COVNotify', function() { + it('should successfully encode and decode', function() { + var buffer = utils.getBuffer(); + var date = new Date(1, 1, 1); + var time = new Date(1, 1, 1); + time.setMilliseconds(990); + baServices.encodeCOVNotify(buffer, 7, 443, {type: 2, instance: 12}, 120, [ + {property: {propertyIdentifier: 81, propertyArrayIndex: 0xFFFFFFFF}, value: [ + {type: 1, value: true}, + {type: 1, value: false}, + {type: 2, value: 1}, + {type: 2, value: 1000}, + {type: 2, value: 1000000}, + {type: 2, value: 1000000000}, + {type: 3, value: -1}, + {type: 3, value: -1000}, + {type: 3, value: -1000000}, + {type: 3, value: -1000000000}, + {type: 4, value: 0.1}, + {type: 5, value: 100.121212}, + {type: 6, value: [1, 2, 100, 200]}, + {type: 7, value: 'Test1234$'}, + {type: 8, value: {bitsUsed: 0, value: []}}, + {type: 8, value: {bitsUsed: 24, value: [0xAA, 0xAA, 0xAA]}}, + {type: 9, value: 4}, + {type: 10, value: date}, + {type: 11, value: time} + ], priority: 0}, + {property: {propertyIdentifier: 82, propertyArrayIndex: 0}, value: [ + {type: 12, value: {type: 3, instance: 0}} + ], priority: 8} + ]); + var result = baServices.decodeCOVNotify(buffer.buffer, 0, buffer.offset); + delete result.len; + result.values[0].value[10].value = Math.floor(result.values[0].value[10].value * 1000) / 1000; + expect(result).to.deep.equal({ + initiatingDeviceIdentifier: { + type: 8, + instance: 443 + }, + monitoredObjectIdentifier: { + type: 2, + instance: 12 + }, + subscriberProcessIdentifier: 7, + timeRemaining: 120, + values: [ + { + priority: 0, + property: { + arrayIndex: 0xFFFFFFFF, + propertyId: 81 + }, + value: [ + {type: 1, value: true, len: 1}, + {type: 1, value: false, len: 1}, + {type: 2, value: 1, len: 2}, + {type: 2, value: 1000, len: 3}, + {type: 2, value: 1000000, len: 4}, + {type: 2, value: 1000000000, len: 5}, + {type: 3, value: -1, len: 2}, + {type: 3, value: -1000, len: 3}, + {type: 3, value: -1000000, len: 4}, + {type: 3, value: -1000000000, len: 5}, + {type: 4, value: 0.1, len: 5}, + {type: 5, value: 100.121212, len: 10}, + {type: 6, value: [1, 2, 100, 200], len: 5}, + {type: 7, value: 'Test1234$', encoding: 0, len: 12}, + {type: 8, value: {bitsUsed: 0, value: []}, len: 2}, + {type: 8, value: {bitsUsed: 24, value: [0xAA, 0xAA, 0xAA]}, len: 5}, + {type: 9, value: 4, len: 2}, + {type: 10, value: date, len: 5}, + {type: 11, value: time, len: 5} + ] + }, + { + priority: 0, + property: { + arrayIndex: 0xFFFFFFFF, + propertyId: 82 + }, + value: [ + {type: 12, value: {type: 3, instance: 0}, len: 5} + ] + } + ] + }); + }); + }); });