UNPKG

@widesky/node-bacstack

Version:

The BACnet protocol library written in pure JavaScript.

511 lines (464 loc) 21.5 kB
'use strict'; /** * This script will discover all devices in the network and read out all * properties and deliver a JSON as device description * * If a deviceId is given as first parameter then only this device is discovered */ const Bacnet = require('../index'); const process = require('process'); // Map the Property types to their enums/bitstrings const PropertyIdentifierToEnumMap = {}; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.OBJECT_TYPE] = Bacnet.enum.ObjectType; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.SEGMENTATION_SUPPORTED] = Bacnet.enum.Segmentation; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.EVENT_STATE] = Bacnet.enum.EventState; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.UNITS] = Bacnet.enum.EngineeringUnits; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.RELIABILITY] = Bacnet.enum.Reliability; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.NOTIFY_TYPE] = Bacnet.enum.NotifyType; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.POLARITY] = Bacnet.enum.Polarity; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.PROTOCOL_SERVICES_SUPPORTED] = Bacnet.enum.ServicesSupported; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.PROTOCOL_OBJECT_TYPES_SUPPORTED] = Bacnet.enum.ObjectTypesSupported; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.STATUS_FLAGS] = Bacnet.enum.StatusFlags; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.LIMIT_ENABLE] = Bacnet.enum.LimitEnable; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.EVENT_ENABLE] = Bacnet.enum.EventTransitionBits; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.ACKED_TRANSITIONS] = Bacnet.enum.EventTransitionBits; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.SYSTEM_STATUS] = Bacnet.enum.DeviceStatus; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.SYSTEM_STATUS] = Bacnet.enum.DeviceStatus; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.ACK_REQUIRED] = Bacnet.enum.EventTransitionBits; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.LOGGING_TYPE] = Bacnet.enum.LoggingType; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.FILE_ACCESS_METHOD] = Bacnet.enum.FileAccessMethod; PropertyIdentifierToEnumMap[Bacnet.enum.PropertyIdentifier.NODE_TYPE] = Bacnet.enum.NodeType; // Sometimes the Map needs to be more specific const ObjectTypeSpecificPropertyIdentifierToEnumMap = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_INPUT] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_INPUT][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_INPUT][Bacnet.enum.PropertyIdentifier.MODE] = Bacnet.enum.BinaryPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.ANALOG_INPUT] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.ANALOG_INPUT][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryPV; //???? ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.ANALOG_OUTPUT] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.ANALOG_OUTPUT][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryPV; //???? ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_OUTPUT] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_OUTPUT][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_OUTPUT][Bacnet.enum.PropertyIdentifier.RELINQUISH_DEFAULT] = Bacnet.enum.BinaryPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_VALUE] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_VALUE][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_VALUE][Bacnet.enum.PropertyIdentifier.RELINQUISH_DEFAULT] = Bacnet.enum.BinaryPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_LIGHTING_OUTPUT] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_LIGHTING_OUTPUT][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryLightingPV; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BITSTRING_VALUE] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.BINARY_VALUE][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.BinaryPV; // ??? ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.LifeSafetyState; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT][Bacnet.enum.PropertyIdentifier.TRACKING_VALUE] = Bacnet.enum.LifeSafetyState; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT][Bacnet.enum.PropertyIdentifier.MODE] = Bacnet.enum.LifeSafetyMode; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT][Bacnet.enum.PropertyIdentifier.ACCEPTED_MODES] = Bacnet.enum.LifeSafetyMode; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT][Bacnet.enum.PropertyIdentifier.SILENCED] = Bacnet.enum.LifeSafetyState; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_POINT][Bacnet.enum.PropertyIdentifier.OPERATION_EXPECTED] = Bacnet.enum.LifeSafetyOperation; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_ZONE] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_ZONE][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.LifeSafetyState; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LIFE_SAFETY_ZONE][Bacnet.enum.PropertyIdentifier.MODE] = Bacnet.enum.LifeSafetyMode; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LOAD_CONTROL] = {}; ObjectTypeSpecificPropertyIdentifierToEnumMap[Bacnet.enum.ObjectType.LOAD_CONTROL][Bacnet.enum.PropertyIdentifier.PRESENT_VALUE] = Bacnet.enum.ShedState; // For Objects we read out All properties if cli parameter --all is provided const propSubSet = (process.argv.includes('--all')) ? Object.values(Bacnet.enum.PropertyIdentifier) : [ /* normally supported from all devices */ Bacnet.enum.PropertyIdentifier.OBJECT_IDENTIFIER, Bacnet.enum.PropertyIdentifier.OBJECT_NAME, Bacnet.enum.PropertyIdentifier.OBJECT_TYPE, Bacnet.enum.PropertyIdentifier.PRESENT_VALUE, Bacnet.enum.PropertyIdentifier.STATUS_FLAGS, Bacnet.enum.PropertyIdentifier.EVENT_STATE, Bacnet.enum.PropertyIdentifier.RELIABILITY, Bacnet.enum.PropertyIdentifier.OUT_OF_SERVICE, Bacnet.enum.PropertyIdentifier.UNITS, /* other properties */ Bacnet.enum.PropertyIdentifier.DESCRIPTION, Bacnet.enum.PropertyIdentifier.SYSTEM_STATUS, Bacnet.enum.PropertyIdentifier.VENDOR_NAME, Bacnet.enum.PropertyIdentifier.VENDOR_IDENTIFIER, Bacnet.enum.PropertyIdentifier.MODEL_NAME, Bacnet.enum.PropertyIdentifier.FIRMWARE_REVISION, Bacnet.enum.PropertyIdentifier.APPLICATION_SOFTWARE_VERSION, Bacnet.enum.PropertyIdentifier.LOCATION, Bacnet.enum.PropertyIdentifier.LOCAL_DATE, Bacnet.enum.PropertyIdentifier.LOCAL_TIME, Bacnet.enum.PropertyIdentifier.UTC_OFFSET, Bacnet.enum.PropertyIdentifier.DAYLIGHT_SAVINGS_STATUS, Bacnet.enum.PropertyIdentifier.PROTOCOL_VERSION, Bacnet.enum.PropertyIdentifier.PROTOCOL_REVISION, Bacnet.enum.PropertyIdentifier.PROTOCOL_SERVICES_SUPPORTED, Bacnet.enum.PropertyIdentifier.PROTOCOL_OBJECT_TYPES_SUPPORTED, Bacnet.enum.PropertyIdentifier.OBJECT_LIST, Bacnet.enum.PropertyIdentifier.MAX_APDU_LENGTH_ACCEPTED, Bacnet.enum.PropertyIdentifier.SEGMENTATION_SUPPORTED, Bacnet.enum.PropertyIdentifier.APDU_TIMEOUT, Bacnet.enum.PropertyIdentifier.NUMBER_OF_APDU_RETRIES, Bacnet.enum.PropertyIdentifier.DEVICE_ADDRESS_BINDING, Bacnet.enum.PropertyIdentifier.DATABASE_REVISION, Bacnet.enum.PropertyIdentifier.MAX_INFO_FRAMES, Bacnet.enum.PropertyIdentifier.MAX_MASTER, Bacnet.enum.PropertyIdentifier.ACTIVE_COV_SUBSCRIPTIONS, Bacnet.enum.PropertyIdentifier.ACTIVE_COV_MULTIPLE_SUBSCRIPTIONS ]; const debug = process.argv.includes('--debug'); /** * Retrieve all properties manually because ReadPropertyMultiple is not available * @param address * @param objectId * @param callback * @param propList * @param result * @returns {*} */ function getAllPropertiesManually(address, objectId, callback, propList, result) { if (!propList) { propList = propSubSet.map((x) => x); // Clone the array } if (!result) { result = []; } if (!propList.length) { return callback({ values: [ { objectId: objectId, values: result } ] }); } const prop = propList.shift(); // Read only object-list property bacnetClient.readProperty(address, objectId, prop, (err, value) => { if (!err) { if (debug) { console.log('Handle value ' + prop + ': ', JSON.stringify(value)); } const objRes = value.property; objRes.value = value.values; result.push(objRes); } else { // console.log('Device do not contain object ' + Bacnet.enum.getEnumName(Bacnet.enum.PropertyIdentifier, prop)); } getAllPropertiesManually(address, objectId, callback, propList, result); }); } /** * Reads ou one bit out of an buffer * @param buffer * @param i * @param bit * @returns {number} */ function readBit(buffer, i, bit) { return (buffer[i] >> bit) % 2; } /** * sets a bit in a buffer * @param buffer * @param i * @param bit * @param value */ function setBit(buffer, i, bit, value) { if (value === 0) { buffer[i] &= ~(1 << bit); } else { buffer[i] |= (1 << bit); } } /** * Parses a Bitstring and returns array with all true values * @param buffer * @param bitsUsed * @param usedEnum * @returns {[]} */ function handleBitString(buffer, bitsUsed, usedEnum) { const res = []; for (let i = 0; i < bitsUsed; i++) { const bufferIndex = Math.floor(i / 8); if (readBit(buffer, bufferIndex, i % 8)) { res.push(Bacnet.enum.getEnumName(usedEnum, i)); } } return res; } /** * Parses a property value * @param address * @param objId * @param parentType * @param value * @param supportsMultiple * @param callback */ function parseValue(address, objId, parentType, value, supportsMultiple, callback) { let resValue = null; if (value && value.type && value.value !== null && value.value !== undefined) { switch (value.type) { case Bacnet.enum.ApplicationTag.NULL: // should be null already, but set again resValue = null; break; case Bacnet.enum.ApplicationTag.BOOLEAN: // convert number to a real boolean resValue = !!value.value; break; case Bacnet.enum.ApplicationTag.UNSIGNED_INTEGER: case Bacnet.enum.ApplicationTag.SIGNED_INTEGER: case Bacnet.enum.ApplicationTag.REAL: case Bacnet.enum.ApplicationTag.DOUBLE: case Bacnet.enum.ApplicationTag.CHARACTER_STRING: // datatype should be correct already resValue = value.value; break; case Bacnet.enum.ApplicationTag.DATE: case Bacnet.enum.ApplicationTag.TIME: case Bacnet.enum.ApplicationTag.TIMESTAMP: // datatype should be Date too // Javascript do not have date/timestamp only resValue = value.value; break; case Bacnet.enum.ApplicationTag.BIT_STRING: // handle bitstrings specific and more generic if (ObjectTypeSpecificPropertyIdentifierToEnumMap[parentType] && ObjectTypeSpecificPropertyIdentifierToEnumMap[parentType][objId]) { resValue = handleBitString(value.value.value, value.value.bitsUsed, ObjectTypeSpecificPropertyIdentifierToEnumMap[parentType][objId]); } else if (PropertyIdentifierToEnumMap[objId]) { resValue = handleBitString(value.value.value, value.value.bitsUsed, PropertyIdentifierToEnumMap[objId]); } else { if (parentType !== Bacnet.enum.ObjectType.BITSTRING_VALUE) { console.log('Unknown value for BIT_STRING type for objId ' + Bacnet.enum.getEnumName(Bacnet.enum.PropertyIdentifier, objId) + ' and parent type ' + Bacnet.enum.getEnumName(Bacnet.enum.ObjectType, parentType)); } resValue = value.value; } break; case Bacnet.enum.ApplicationTag.ENUMERATED: // handle enumerations specific and more generic if (ObjectTypeSpecificPropertyIdentifierToEnumMap[parentType] && ObjectTypeSpecificPropertyIdentifierToEnumMap[parentType][objId]) { resValue = Bacnet.enum.getEnumName(ObjectTypeSpecificPropertyIdentifierToEnumMap[parentType][objId], value.value); } else if (PropertyIdentifierToEnumMap[objId]) { resValue = Bacnet.enum.getEnumName(PropertyIdentifierToEnumMap[objId], value.value); } else { console.log('Unknown value for ENUMERATED type for objId ' + Bacnet.enum.getEnumName(Bacnet.enum.PropertyIdentifier, objId) + ' and parent type ' + Bacnet.enum.getEnumName(Bacnet.enum.ObjectType, parentType)); resValue = value.value; } break; case Bacnet.enum.ApplicationTag.OBJECTIDENTIFIER: // Look up object identifiers // Some object identifiers should not be looked up because we end in loops else if (objId === Bacnet.enum.PropertyIdentifier.OBJECT_IDENTIFIER || objId === Bacnet.enum.PropertyIdentifier.STRUCTURED_OBJECT_LIST || objId === Bacnet.enum.PropertyIdentifier.SUBORDINATE_LIST) { resValue = value.value; } else if (supportsMultiple) { const requestArray = [{ objectId: value.value, properties: [{id: 8}] }]; bacnetClient.readPropertyMultiple(address, requestArray, (err, resValue) => { //console.log(JSON.stringify(value.value) + ': ' + JSON.stringify(resValue)); parseDeviceObject(address, resValue, value.value, true, callback); }); return; } else { getAllPropertiesManually(address, value.value, result => { parseDeviceObject(address, result, value.value, false, callback); }); return; } break; case Bacnet.enum.ApplicationTag.OCTET_STRING: // It is kind of binary data?? resValue = value.value; break; case Bacnet.enum.ApplicationTag.ERROR: // lookup error class and code resValue = { errorClass: Bacnet.enum.getEnumName(Bacnet.enum.ErrorClass, value.value.errorClass), errorCode: Bacnet.enum.getEnumName(Bacnet.enum.ErrorCode, value.value.errorCode) }; break; case Bacnet.enum.ApplicationTag.OBJECT_PROPERTY_REFERENCE: case Bacnet.enum.ApplicationTag.DEVICE_OBJECT_PROPERTY_REFERENCE: case Bacnet.enum.ApplicationTag.DEVICE_OBJECT_REFERENCE: case Bacnet.enum.ApplicationTag.READ_ACCESS_SPECIFICATION: //??? resValue = value.value; break; case Bacnet.enum.ApplicationTag.CONTEXT_SPECIFIC_DECODED: parseValue(address, objId, parentType, value.value, supportsMultiple, callback); return; case Bacnet.enum.ApplicationTag.READ_ACCESS_RESULT: // ???? resValue = value.value; break; default: console.log('unknown type ' + value.type + ': ' + JSON.stringify(value)); resValue = value; } } setImmediate(() => callback(resValue)); } /** * Parse an object structure * @param address * @param obj * @param parent * @param supportsMultiple * @param callback */ function parseDeviceObject(address, obj, parent, supportsMultiple, callback) { if (debug) { console.log('START parseDeviceObject: ' + JSON.stringify(parent) + ' : ' + JSON.stringify(obj)); } if(!obj) { console.log('object not valid on parse device object'); return; } if (!obj.values || !Array.isArray(obj.values)) { console.log('No device or invalid response'); callback({'ERROR': 'No device or invalid response'}); return; } let cbCount = 0; let objDef = {}; const finalize = () => { // Normalize and remove single item arrays Object.keys(objDef).forEach(devId => { Object.keys(objDef[devId]).forEach(objId => { if (objDef[devId][objId].length === 1) { objDef[devId][objId] = objDef[devId][objId][0]; } }); }); // If (standard case) only one device was in do not create sub structures) if (obj.values.length === 1) { objDef = objDef[obj.values[0].objectId.instance]; } if (debug) { console.log('END parseDeviceObject: ' + JSON.stringify(parent) + ' : ' + JSON.stringify(objDef)); } callback(objDef); }; obj.values.forEach(devBaseObj => { if (!devBaseObj.objectId) { console.log('No device Id found in object data'); return; } if (devBaseObj.objectId.type === undefined || devBaseObj.objectId.instance === undefined) { console.log('No device type or instance found in object data'); return; } if (!devBaseObj.values || !Array.isArray(devBaseObj.values)) { console.log('No device values response'); return; } const deviceId = devBaseObj.objectId.instance; objDef[deviceId] = {}; devBaseObj.values.forEach(devObj => { let objId = Bacnet.enum.getEnumName(Bacnet.enum.PropertyIdentifier, devObj.id); if (devObj.index !== 4294967295) { objId += '-' + devObj.index; } if (debug) { console.log('Handle Object property:', deviceId, objId, devObj.value); } devObj.value.forEach(val => { if (JSON.stringify(val.value) === JSON.stringify(parent)) { // ignore parent object objDef[deviceId][objId] = objDef[deviceId][objId] || []; objDef[deviceId][objId].push(val.value); return; } cbCount++; parseValue(address, devObj.id, parent.type, val, supportsMultiple, parsedValue => { if (debug) { console.log('RETURN parsedValue', deviceId, objId, devObj.value, parsedValue); } objDef[deviceId][objId] = objDef[deviceId][objId] || []; objDef[deviceId][objId].push(parsedValue); if (!--cbCount) { finalize(); } }); }); }); }); if (cbCount === 0) { finalize(); } } let objectsDone = 0; /** * Print result info object * @param deviceId * @param obj */ function printResultObject(deviceId, obj) { objectsDone++; console.log(`Device ${deviceId} (${objectsDone}/${Object.keys(knownDevices).length}) read successfully ...`); console.log(JSON.stringify(obj)); console.log(); console.log(); if (objectsDone === Object.keys(knownDevices).length) { setTimeout(() => { bacnetClient.close(); console.log('closed transport ' + Date.now()); }, 1000); } } let limitToDevice = null; if (process.argv.length === 3) { limitToDevice = parseInt(process.argv[2]); if (isNaN(limitToDevice)) { limitToDevice = null; } } // create instance of Bacnet const bacnetClient = new Bacnet({apduTimeout: 4000, interface: '0.0.0.0'}); // emitted for each new message bacnetClient.on('message', (msg, rinfo) => { console.log(msg); if (rinfo) console.log(rinfo); }); // emitted on errors bacnetClient.on('error', (err) => { console.error(err); bacnetClient.close(); }); // emmitted when Bacnet server listens for incoming UDP packages bacnetClient.on('listening', () => { console.log('sent whoIs ' + Date.now()); // discover devices once we are listening bacnetClient.whoIs(); }); const knownDevices = []; // emitted when a new device is discovered in the network bacnetClient.on('iAm', (device) => { // address object of discovered device, // just use in subsequent calls that are directed to this device const address = device.header.sender; //discovered device ID const deviceId = device.payload.deviceId; if (knownDevices.includes(deviceId)) return; if (limitToDevice !== null && limitToDevice !== deviceId) return; console.log('Found Device ' + deviceId + ' on ' + JSON.stringify(address)); knownDevices.push(deviceId); const propertyList = []; propSubSet.forEach(item => { propertyList.push({id: item}); }); const requestArray = [{ objectId: {type: 8, instance: deviceId}, properties: propertyList } ]; bacnetClient.readPropertyMultiple(address, requestArray, (err, value) => { if (err) { console.log(deviceId, 'No ReadPropertyMultiple supported:', err.message); getAllPropertiesManually(address, {type: 8, instance: deviceId}, result => { parseDeviceObject(address, result, {type: 8, instance: deviceId}, false, res => printResultObject(deviceId, res)); }); } else { console.log(deviceId, 'ReadPropertyMultiple supported ...'); parseDeviceObject(address, value, {type: 8, instance: deviceId}, true, res => printResultObject(deviceId, res)); } }); });