UNPKG

@halsystems/red-bacnet

Version:
597 lines (541 loc) 21.7 kB
'use strict' require('./_alias.js'); const EventEmitter = require('events'); const bacnet = require('@root/ext/node-bacstack/dist/index.js') const baEnum = bacnet.enum; const { EVENT_ERROR, EVENT_OUTPUT } = require('@root/common/core/constant.js') const { concurrentTasks } = require('@root/common/core/concurrent.js') const { getErrMsg } = require('@root/common/func.js') // ---------------------------------- type def ---------------------------------- /** * @typedef {import('@root/ext/node-bacstack/dist/index.js').Client} BacnetClient */ // ---------------------------------- export ---------------------------------- module.exports = { /** * Reads the object list of a device using optimized batch reading. * First tries readPropertyMultiple for faster performance, falls back to sequential reading if failed. * @param {BacnetClient} client * @param {object} device * eg:{ * deviceId: 123, * network: null, * ipAddress: "192.168.1.104", * macAddress: null, * segmentation: 0, * maxApdu: 1476, * vendorId: 36, * deviceName: 'BMS' * } * @returns array of objects * eg: [{type: 12, value: {type: 8, instance: 123}, ...], * @async */ readObjectList: async function (client, device) { const objectId = { type: baEnum.ObjectType.DEVICE, instance: device.deviceId }; const propertyId = baEnum.PropertyIdentifier.OBJECT_LIST; let result = await module.exports.readPropertyMultipleReturnArr(client, device, objectId, propertyId); if (result.length == 0) { result = await module.exports.readPropertyReturnArr(client, device, objectId, propertyId); } return result; }, /** * Reads a property that returns an array using readPropertyMultiple for better performance. * Attempts to read the property in batches to minimize round trips. * @param {BacnetClient} client * @param {object} device * @param {object} objectId * @param {number} propertyId * @returns array of objects * @async */ readPropertyMultipleReturnArr: async function (client, device, objectId, propertyId) { let addressSet = device.ipAddress; if (device.macAddress != null && device.network != null) { addressSet = { ip: device.ipAddress, adr: device.macAddress, net: device.network }; } const result = []; let healthy = true; // helper for single batch request async function readBatch(arrayIndex) { const reqArr = [{ objectId: objectId, properties: [{ id: propertyId, index: arrayIndex != null ? arrayIndex : baEnum.ASN1_ARRAY_ALL }] }]; return new Promise((resolve, reject) => { client.readPropertyMultiple( addressSet, reqArr, { maxApdu: device.maxApdu }, (err, value) => { if (err) return reject(err); resolve(value); } ); }); } // try reading all at once first try { const value = await readBatch(baEnum.ASN1_ARRAY_ALL); if (value?.values?.[0]?.values?.[0]?.value) { return value.values[0].values[0].value; } } catch (err) { void err healthy = false; } // try using index read if (!healthy) { for (let i = 0; ; i++) { try { const res = await readBatch(i); const item = res?.values?.[0]?.values?.[0]?.value?.[0]; if (item?.type === 105) break result.push(item); } catch (err) { void err break; } } } return result; }, /** * Smart read multiple properties from a BACnet device with concurrent read feature and various read methods * readMethod * 0: use readProperty only * 1: try readPropertyMultiple, reduce query size if failed, and fallback to readProperty if query size reduced to 1 * @param {BacnetClient} client * @param {object} device * eg:{ * deviceId: 123, * network: null, * ipAddress: "192.168.1.104", * macAddress: null, * segmentation: 0, * maxApdu: 1476, * vendorId: 36, * deviceName: 'BMS' * } * @param {array} reqArr * eg:[{ * objectId: { type: 2, instance: 1 }, * properties: [ { id: 1 }, ... ] * }, * ...] * @param {number} readMethod * 0:single read only; 1:multi read fallback single; 2:2 x multi read fallback * @param {number} maxConcurrentSinglePointRead * maximum concurrent point to read in single read mode * @param {number} singleReadFailedRetry * retry times for single read failed * @param {number} concurrentTaskDelay * delay between concurrent tasks * @returns array of objects * eg: [{type: 12, value: {type: 8, instance: 123}, ...], * @async */ smartReadProperty: async function ( client, device, reqArr, readMethod = 1, maxConcurrentSinglePointRead = 5, singleReadFailedRetry = 5, concurrentTaskDelay = 50 ) { /* reqArr example [{ objectId: { type: 2, instance: 1 }, properties: [ { id: 1 }, ... ] }, ...] */ let batchSizes = new Set(); let reqArrIndexNext = 0 let reqArrIndex = 0 let result = []; let success = false if (readMethod > 0) { // calculate batch sizes if (device.maxApdu == null) // default batch size if null or undefined batchSizes.add(20); else { const perValueBytes = [30] // typical numeric point is 17 byte for (let x = 0; x < perValueBytes.length; x++) { let batchSize = Math.trunc(device.maxApdu / perValueBytes[x]) if (batchSize > 0) batchSizes.add(batchSize) } } } for (const batchSize of batchSizes) { let healthy = true let first = true let reqArrBatch let batchCount let currBatchSize = batchSize do { // use current batch size until query failed // get ready for next batch reqArrBatch = [] batchCount = 0 // ensure reqArrBatch at least have one item to query if (first && reqArrIndexNext < reqArr.length) { reqArrBatch.push(reqArr[reqArrIndexNext]) batchCount += reqArr[reqArrIndexNext].properties.length reqArrIndexNext++ first = false } // add subsequent items until next properties count is > batch size const batchFull = false do { if (reqArrIndexNext >= reqArr.length) break if (batchCount + reqArr[reqArrIndexNext].properties.length > currBatchSize) break reqArrBatch.push(reqArr[reqArrIndexNext]) batchCount += reqArr[reqArrIndexNext].properties.length reqArrIndexNext++ } while (!batchFull) // read batch block const value = await module.exports.readPropertyMultple(client, device, reqArrBatch) .catch(() => { // Reduce batch size by half (minimum 1) currBatchSize = Math.max(1, Math.floor(currBatchSize / 2)) // If batch size is too small, mark as unhealthy if (currBatchSize <= 1) { healthy = false } reqArrIndexNext = reqArrIndex healthy = false }) if (!healthy) break if (value == null) continue result.push(...value.values); reqArrIndex = reqArrIndexNext // read completed if (reqArrIndexNext >= reqArr.length) { success = true break } } while (healthy) // read completed, otherwise try other batch size if (success) break } // fallback to readProperty if read property multiple failed / readMethod === 0 if (!success) { let failedCount = 0 let result_single = [] const dummyEventEmitter = new EventEmitter(); dummyEventEmitter.on(EVENT_OUTPUT, () => { }); dummyEventEmitter.on(EVENT_ERROR, () => { }); const tasks = reqArr.slice(reqArrIndexNext).flatMap((req, x) => req.properties.map((prop, y) => ({ id: `${x}-${y}`, task: async () => { try { let value = await module.exports.readProperty(client, device, req.objectId, prop.id); if (value == null) { value = { objectId: req.objectId, property: { id: prop.id, index: 4294967295 }, values: { errorClass: baEnum.ErrorClass.OBJECT, errorCode: baEnum.ErrorCode.UNKNOWN_OBJECT } }; } result_single.push(value); return value; } catch (err) { failedCount++; // if (readMethod < 1 && failedCount >= singleReadFailedRetry) if (failedCount >= singleReadFailedRetry) throw err } } })) ); try { await concurrentTasks(dummyEventEmitter, tasks, maxConcurrentSinglePointRead, concurrentTaskDelay); } catch (err) { void err } // convert result single to result compatible format let transform = {} for (let x = 0; x < result_single.length; x++) { const key = `type${result_single[x].objectId.type}inst${result_single[x].objectId.instance}` if (key in transform) { transform[key].values.push({ id: result_single[x].property.id, index: result_single[x].property.index, value: result_single[x].values }); } else { transform[key] = { objectId: { type: result_single[x].objectId.type, instance: result_single[x].objectId.instance }, values: [{ id: result_single[x].property.id, index: result_single[x].property.index, value: result_single[x].values }] } } } result.push(...Object.values(transform)) } return result }, /** * Smart write multiple properties to a BACnet device with concurrent write feature * @param {BacnetClient} client * @param {object} device * eg:{ deviceId: 123, network: null, ipAddress: "192.168.1.104", macAddress: null, * segmentation: 0, maxApdu: 1476, vendorId: 36, deviceName: 'BMS'} * @param {array} writePoints * eg:{'BacServer.VavMode': { * id: 'BacServer.VavMode', deviceName: 'BacServer', bacType: 19, bacInstance: 1, * bacProp: 85, priority: 10, value: 1, valueType: 2}...} * @param {EventEmitter} eventEmitter * @param {number} maxConcurrentWrite * maximum concurrent point to write * @param {number} concurrentTaskDelay * delay between concurrent tasks * @async */ smartWriteProperty: async function ( client, device, writePoints, eventEmitter, maxConcurrentWrite, concurrentTaskDelay = 50 ) { const entries = Object.entries(writePoints); // current writePropertyMultiple will throw ERR_TIMEOUT if any of the write fails // makes it hard to pinpoint which write failed // therefore writeProperty is used instead for better error handling // may need to extend to support writePropertyMultiple in future? const tasks = entries .map(([id, point]) => ({ id: id, task: async () => { try { await module.exports.writeProperty( client, device, { type: point.bacType, instance: point.bacInstance }, point.bacProp, [{ type: point.valueType, value: point.value }], point.priority ); } catch (err) { eventEmitter.emit(EVENT_ERROR, { id: id, error: getErrMsg(err) }); } return } })); await concurrentTasks(eventEmitter, tasks, maxConcurrentWrite, concurrentTaskDelay); // write properties multiples example // const values = [ // { // objectId: { type: 2, instance: 101 }, // values: [{ // property: { id: baEnum.PropertyIdentifier.PRESENT_VALUE, index: bacnet.enum.ASN1_ARRAY_ALL }, // value: [{ type: bacnet.enum.ApplicationTags.REAL, value: randomVal(12, 30) }], // priority: 9 // }] // }, // ]; // await client.writePropertyMultiple(address, values, (err) => { // if (err) { // console.log('error: ', err); // } else { // console.log('written'); // } // // close client // client.close(); // }); }, /** * Reads a single property from a BACnet device. * @param {BacnetClient} client * @param {object} device * eg:{ deviceId: 123, network: null, ipAddress: "192.168.1.104", macAddress: null, * segmentation: 0, maxApdu: 1476, vendorId: 36, deviceName: 'BMS'} * @param {object} objectId * eg:{ type: 2, instance: 1 } * @param {number} propertyId * eg: 85 * @returns object * eg: { len: 11, objectId: { type: 19, instance: 1 }, * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]} * @async */ readProperty: async function (client, device, objectId, propertyId) { let addressSet = device.ipAddress if (device.macAddress != null && device.network != null) { addressSet = { ip: device.ipAddress, adr: device.macAddress, net: device.network }; } return new Promise((resolve, reject) => { client.readProperty( addressSet, objectId, propertyId, { maxApdu: device.maxApdu }, (err, value) => { if (err) reject(err); else resolve(value); }); }); }, /** * Reads a single property from a BACnet device. BACnet device returns an array of objects. * @param {BacnetClient} client * @param {object} device * eg:{ deviceId: 123, network: null, ipAddress: "192.168.1.104", macAddress: null, * segmentation: 0, maxApdu: 1476, vendorId: 36, deviceName: 'BMS'} * @param {object} objectId * eg:{ type: 2, instance: 1 } * @param {number} propertyId * eg: 85 * @returns array of objects * eg: [{type: 12, value: {type: 8, instance: 123}, ...], * @async */ readPropertyReturnArr: async function (client, device, objectId, propertyId) { const result = []; let addressSet = device.ipAddress if (device.macAddress != null && device.network != null) { addressSet = { ip: device.ipAddress, adr: device.macAddress, net: device.network }; } async function readPropertyPart(index) { return new Promise((resolve, reject) => { client.readProperty(addressSet, objectId, propertyId, { maxApdu: device.maxApdu, arrayIndex: index }, (err, value) => { if (err) { if (index === 1) reject(err); else // If it fails after reading some parts, resolve with what we have resolve(result); } else { if (value.values[0].type === baEnum.ApplicationTags.NULL) resolve(result); else { result.push(...value.values); // Read the next part readPropertyPart(index + 1).then(resolve).catch(reject); } } }); }); } return await readPropertyPart(1); }, /** * Reads multiple properties of an object in a device. BACnet device returns an array of objects. * @param {BacnetClient} client * @param {object} device * eg:{ deviceId: 123, network: null, ipAddress: "192.168.1.104", macAddress: null, * segmentation: 0, maxApdu: 1476, vendorId: 36, deviceName: 'BMS'} * @param {array} reqArr * eg:[{ * objectId: { type: 2, instance: 1 }, * properties: [ { id: 85 }, ... ] * }, * ...] * @returns object * eg: {"len":1937,"values":[{ * "objectId":{"type":3,"instance":0}, * "values":[{"id":85,"index":4294967295,"value":[{"value":1,"type":9}]}, ...] * ...}]} * @async */ readPropertyMultple: async function (client, device, reqArr) { let addressSet = device.ipAddress if (device.macAddress != null && device.network != null) { addressSet = { ip: device.ipAddress, adr: device.macAddress, net: device.network }; } return new Promise((resolve, reject) => { client.readPropertyMultiple( addressSet, reqArr, { maxApdu: device.maxApdu }, (err, value) => { if (err) reject(err); else resolve(value); }); }); }, /** * Writes a single property to a BACnet device. * @param {BacnetClient} client * @param {object} device * eg:{ deviceId: 123, network: null, ipAddress: "192.168.1.104", macAddress: null, * segmentation: 0, maxApdu: 1476, vendorId: 36, deviceName: 'BMS'} * @param {object} objectId * eg:{ type: 2, instance: 1 } * @param {number} propertyId * eg: 85 * @param {array} writeValue * eg:[{type: 2, value: 3}] * @param {number} priority * eg: 8 * @returns true / undefined: true if success, else undefined * @async */ writeProperty: async function (client, device, objectId, propertyId, writeValue, priority) { let addressSet = device.ipAddress if (device.macAddress != null && device.network != null) { addressSet = { ip: device.ipAddress, adr: device.macAddress, net: device.network }; } return new Promise((resolve, reject) => { // Build options only if priority is provided const options = {}; if (priority !== null && priority !== undefined) { options.priority = priority; } client.writeProperty( addressSet, objectId, propertyId, writeValue, options, // may be empty if no priority (err) => { if (err) { reject(err); } else { resolve(true); } } ); }); }, }