diff --git a/lib/asn1.js b/lib/asn1.js index a1c3d286..a1772b4c 100644 --- a/lib/asn1.js +++ b/lib/asn1.js @@ -484,8 +484,10 @@ const bacappEncodeApplicationData = module.exports.bacappEncodeApplicationData = case baEnum.ApplicationTags.READ_ACCESS_SPECIFICATION: encodeReadAccessSpecification(buffer, value.value); break; + case undefined: + throw new Error('Cannot encode a value if the type has not been specified'); default: - throw 'Unknown type'; + throw 'Unknown ApplicationTags type: ' + baEnum.getEnumName(baEnum.ApplicationTags, value.type); } }; diff --git a/lib/client.js b/lib/client.js index eebc2d46..2e5ba359 100644 --- a/lib/client.js +++ b/lib/client.js @@ -13,7 +13,6 @@ const baNpdu = require('./npdu'); const baBvlc = require('./bvlc'); const baEnum = require('./enum'); -const DEFAULT_HOP_COUNT = 0xFF; const BVLC_HEADER_LENGTH = 4; /** @@ -1190,5 +1189,43 @@ class Client extends EventEmitter { 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.ApplicationTags.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: bitsUsed, + }; + } + } module.exports = Client; diff --git a/lib/enum.js b/lib/enum.js index 842b5702..6846fa1c 100644 --- a/lib/enum.js +++ b/lib/enum.js @@ -1,5 +1,29 @@ 'use strict'; +/** + * Turn an enum into a string suitable for debugging. + * + * @param object group + * Enum group, e.g. bacnet.enum.ConfirmedServiceChoice. + * + * @param Number value + * Enum value, e.g. 1. Note that this *must* be an integer value, so you may + * need to use parseInt(). Non-integer values will result in an exception. + * + * @example + * const s = bacnet.enum.getEnumName( + * bacnet.enum.PropertyIdentifier, + * bacnet.enum.PropertyIdentifier.PRESENT_VALUE + * ); + * console.log(s); // "PRESENT_VALUE(85)" + */ +module.exports.getEnumName = function(group, value) { + if (!Number.isInteger(value)) { + throw new Error('getEnumName() can only be passed an integer value, was given "' + value + '"'); + } + return Object.keys(group).find(key => group[key] === value) + '(' + value + ')'; +} + module.exports.PDU_TYPE_MASK = 0xF0; module.exports.ASN1_MAX_OBJECT = 0x3FF; module.exports.ASN1_INSTANCE_BITS = 22; diff --git a/lib/npdu.js b/lib/npdu.js index e4c8f971..43f315bb 100644 --- a/lib/npdu.js +++ b/lib/npdu.js @@ -2,6 +2,7 @@ const baEnum = require('./enum'); +const DEFAULT_HOP_COUNT = 0xFF; const BACNET_PROTOCOL_VERSION = 1; const BacnetAddressTypes = { NONE: 0, @@ -103,7 +104,7 @@ module.exports.encode = (buffer, funct, destination, source, hopCount, networkMs } if (hasDestination) { - buffer.buffer[buffer.offset++] = hopCount; + buffer.buffer[buffer.offset++] = hopCount || DEFAULT_HOP_COUNT; } if ((funct & baEnum.NpduControlBits.NETWORK_LAYER_MESSAGE) > 0) { diff --git a/lib/transport.js b/lib/transport.js index 721574d9..b676aecc 100644 --- a/lib/transport.js +++ b/lib/transport.js @@ -3,12 +3,14 @@ const createSocket = require('dgram').createSocket; const EventEmitter = require('events').EventEmitter; +const DefaultBACnetPort = 47808; + class Transport extends EventEmitter { constructor(settings) { super(); this._settings = settings; this._server = createSocket({type: 'udp4', reuseAddr: true}); - this._server.on('message', (msg, rinfo) => this.emit('message', msg, rinfo.address)); + this._server.on('message', (msg, rinfo) => this.emit('message', msg, rinfo.address + (rinfo.port === DefaultBACnetPort ? '' : ':' + rinfo.port))); this._server.on('error', (err) => this.emit('message', err)); } @@ -21,7 +23,8 @@ class Transport extends EventEmitter { } send(buffer, offset, receiver) { - this._server.send(buffer, 0, offset, this._settings.port, receiver); + const [address, port] = receiver.split(':'); + this._server.send(buffer, 0, offset, port || DefaultBACnetPort, address); } open() { diff --git a/test/unit/client.spec.js b/test/unit/client.spec.js new file mode 100644 index 00000000..ea801761 --- /dev/null +++ b/test/unit/client.spec.js @@ -0,0 +1,38 @@ +'use strict'; + +const expect = require('chai').expect; +const utils = require('./utils'); +const baEnum = require('../../lib/enum'); +const client = require('../../lib/client'); + +describe('bacstack - client', () => { + it('should successfuly encode a bitstring > 32 bits', () => { + const result = client.createBitstring([ + baEnum.ServicesSupported.CONFIRMED_COV_NOTIFICATION, + baEnum.ServicesSupported.READ_PROPERTY, + baEnum.ServicesSupported.WHO_IS, + ]); + expect(result).to.deep.equal({ + value: [2, 16, 0, 0, 4], + bitsUsed: 35, + }); + }); + it('should successfuly encode a bitstring < 8 bits', () => { + const result = client.createBitstring([ + baEnum.ServicesSupported.GET_ALARM_SUMMARY, + ]); + expect(result).to.deep.equal({ + value: [8], + bitsUsed: 4, + }); + }); + it('should successfuly encode a bitstring of only one bit', () => { + const result = client.createBitstring([ + baEnum.ServicesSupported.ACKNOWLEDGE_ALARM, + ]); + expect(result).to.deep.equal({ + value: [1], + bitsUsed: 1, + }); + }); +});