node-red-contrib-ads-application
Version:
node-red-contrib-ads-client application extention
455 lines (393 loc) • 16.8 kB
JavaScript
const mdp = require( './MDP_Constants.js' );
const ads = require('ads-client');
module.exports = class Ads_BeckhoffDeviceManager {
/**
* Construct a new Beckhoff device manager with the give ads-client object
* @param {} client ads-client
*/
constructor(client){
//Reference to ads-client
this.client = client;
//Cached module list from the device manager
this.moduleList = [];
};
/**
* Get the ads client
*/
getClient(){
return client;
}
/**
* Set the ads client
* @param {} client ads-client
*/
setClient(client){
this.client = client;
}
/**
* @param {string|Array.<string>} [item] List of items to read. If empty all known properties are read from all modules. The format should be <ModuleType.ModuleName.Property>. "Property" is optional, but at least "ModuleType" or "ModuleName" should be provided.
* @return {object} Object containing all information
*/
async ReadDeviceManager(item){
//Read modulelist (can be cached)
try{await this.ReadModulelist()}
catch(err){throw err}
let reqList = []; //Requested values are grouped per module to be read in parallel
let data = {}; //Object to store data
/** Add module to the reqList if not existing
* @param {Module} module
* @return {object} reference to module in reqList
*/
function addModuleToReqList(module){
let moduleReq = reqList.find(o => o.module.moduleId === module.moduleId);
if(!moduleReq){
//Create new module object
moduleReq = {
'module': module,
'reqItemList': [],
'targetRef': null
}
//Create object to store module data and save reference
if(module.moduleName == module.moduleTypeStr){
data[module.moduleName] = {};
moduleReq.targetRef = data[module.moduleName];
}else{
data[module.moduleTypeStr] = data[module.moduleTypeStr] || {};
data[module.moduleTypeStr][module.moduleName] = {};
moduleReq.targetRef = data[module.moduleTypeStr][module.moduleName]
}
//Add module to reqList
reqList.push(moduleReq);
}
return moduleReq
}
/**
* @param {object} moduleReq Reference to the module request object
* @param {object} devDesc Reference to the devDesc of the specified module
* @param {string} propName Propertyname to add
* @param {boolean} [skipCheck] Set true to skip check if the property is defined in DEVICEDESC and isn't already in the list
*/
function addPropToReqList(moduleReq, devDesc, propName, skipCheck){
if(!skipCheck){
let prop = moduleReq.reqItemList.find(o => o.name === propName);
if(prop){
return; //Property already in request list
}
if(! propName in devDesc){
return; //Property name not known
}
}
moduleReq.reqItemList.push(
{
'desc': devDesc[propName],
'name': propName
}
)
}
//Fill reqList
if (!item || item==""){ //Empty item => read all modules and all properties
//Build request list for all modules
for (var i in this.moduleList){
let module = this.moduleList[i];
let devDesc = mdp.DEVICEDESC[module.moduleType];
//Type recognized
if (devDesc){
let reqModule = addModuleToReqList(module);
//Store all property request
for (var i in devDesc){
addPropToReqList(reqModule, devDesc, i, true);
}
}else{
//throw new Error(`Can't recognize module type 0x${module.moduleType.toString(16)}.`);
}
}
}else{ //Read only the requested properties
//Convert single item to array with single item
if(!Array.isArray(item)){
item = [item];
}
//Itterate over requested items and build request list
for(var i in item){
let tree = item[i].split(/(?<!\$)\./); //Split on unescaped "." (!="$.")
let devDesc = null;
let modules = this.moduleList;
let prop = null;
for(var i in tree){
tree[i] = tree[i].replace("$.","."); //Remove escape characters for .
}
if(tree.length > 0){
modules = modules.filter(o => o.moduleTypeStr === tree[0]);
//Handle module name = module type
if(modules.length == 1 && modules[0].moduleTypeStr == modules[0].moduleName && (tree[0] !== tree[1]||"")){
tree.splice(1, 0, modules[0].moduleTypeStr);
}
}
//Find matching modules and grap property (if defined)
switch(tree.length){
case 2: //Type + unique module name
modules = modules.filter(o => o.moduleName === tree[1]);
case 1: //Only type => all
modules = modules.filter(o => o.moduleTypeStr === tree[0]);
prop = null;
break;
case 3: //Type + (unique name) + prop
if(tree[1] === "" || tree[1] === "*"){ //Empty unique name
modules = modules.filter(o => o.moduleTypeStr === tree[0]);
}else{
modules = modules.filter(o => o.moduleName === tree[1] && o.moduleTypeStr === tree[0]);
}
prop = tree[2]
break;
}
//Grap module description (all modules should be the same time => use first)
if(modules.length > 0){
devDesc = mdp.DEVICEDESC[modules[0].moduleType]
if(!devDesc){
modules = []; //Module description not found => drop everything
}
}
//Add all matching modules and their properties to request list
for(var mod in modules){
let reqModule = addModuleToReqList(modules[mod]);
if(prop){
//Add single property of module
addPropToReqList(reqModule,devDesc,prop);
}else{
//Add all properties of module
for (var i in devDesc){
addPropToReqList(reqModule, devDesc, i, true);
}
}
}
}
}
//Execute request list = Read the requested properties (parallel) from each module (sequential)
for(i in reqList){
await Promise.all(reqList[i].reqItemList.map(async(item)=>{
try{
var out = await this._ReadMdpProperty(item.desc, reqList[i].module);
if(out !== null){
//Not null => add value
reqList[i].targetRef[item.name] = out;
}else{
if(!item.desc.Optional){
throw new Error(`Value not found and also not optional. Something went wrong.`)
}
}
}catch(err){
let oldMsg = err.message || "";
err.message = `Problem reading item '${item.name}' in module '${reqList[i].module.moduleName}': ${oldMsg}`
throw err
}
}));
}
return data;
}
/**
* Reads all module indexes from the devicemanager. This function needs to be called before any other function can be used.
* @param {boolean} force If force is set to true, it will alway read the modules from ads. If false or empty, it will try to use cached values if available.
*/
async ReadModulelist(force){
//Use cached values if it exists
if (!force && this.moduleList.length > 0){
return this.moduleList;
}
//Read module list length
try {
var IdListLen = await this._readDevManagData(0xF020,0,0,"UINT16");
} catch (err) {
throw err
}
//Clear previous list
this.moduleList = [];
//Create temp list to store result promise and only store when fully succesfull
let tempList = [];
//Read all modules
try {
let req = [];
for (var i = 1; i <= IdListLen; i++){
req.push(i)
}
await Promise.all(req.map(async(item)=>{
let mdpModule = await this._readDevManagData(0xF020,0,item,"UINT32");
let moduleId = mdpModule & 0xFFFF;
let moduleType = (mdpModule & 0xFFFF0000) / Math.pow(2,16);
let moduleTypeStr = await this._readDevManagData(0x8000,moduleId,0x02,"STRING(255)");
let moduleName = await this._readDevManagData(0x8000,moduleId,0x03,"STRING(255)");
let elem = {
'moduleType' : moduleType,
'moduleId' : moduleId,
'moduleTypeStr' : moduleTypeStr,
'moduleName' : moduleName,
};
tempList[item-1] = elem;
}))
//Fake general module
tempList[0] = {
'moduleType' : 0,
'moduleId' : 0,
'moduleTypeStr' : "General",
'moduleName' : "General",
}
this.moduleList = tempList;
return this.moduleList;
} catch (err) {
this.moduleList = []; //Clear list as it's not complete
throw err
}
}
//-----Internal functions------
/**
* Read a MDP property
* @param {MDPpropDescription|Array.<MDPpropDescription>} desc Property description. If it's an array of descriptions the first valid description is used
* @param {Module} module The module information
* @returns {any} The found value, null if not found
*/
async _ReadMdpProperty( desc, module){
//--"desc" is an array => iterrate primitive types until succesfull--
if(Array.isArray(desc) ){
for (var i in desc){
try{
let var_out = await this._readDevManagData(desc[i].Area, module.moduleId, desc[i].SubIndex, desc[i].Type, desc[i].Enum || null);
if( var_out !== null){
return var_out;
}
}catch(err){}
}
throw new Error(`Can't find a succeeding value`);
}
//--"Type" is an array => Read array of properties with a defined length
else if (Array.isArray(desc.Type)){
try{
//Type[0] = length; Type[1] = array type
//Read array length
let arr_length = await this._readDevManagData(desc.Area, module.moduleId, desc.SubIndex, desc.Type[0], null);
let arr = [];
//Build a request array (for parallel execution)
let reqarr = [];
for (var i = 1; i <= arr_length; i++){
reqarr.push({"subIndex": desc.SubIndex + i, "i": i-1});
}
//request array values
await Promise.all(reqarr.map(async(arr_item)=>{
let var_out = await this._readDevManagData(desc.Area, module.moduleId, arr_item.subIndex, desc.Type[1], desc.Enum || null);
arr[arr_item.i] = var_out;
}));
//If full array is empty, return null (not supported)
let empty = true;
arr.forEach((value)=>{if(value !== null){empty = false}});
if (empty && arr_length > 0){
return null;
}
return arr;
}catch(err){throw err}
//----Primitive type---
}else{
try{
//Get subindex (can be a pointer)
let subIndex = 0;
if (typeof desc.SubIndex === "object"){
//Subindex needs to be read from a pointer
let sub = desc.SubIndex;
subIndex = await this._readDevManagData(sub.Area, module.moduleId, sub.SubIndex, sub.Type);
subIndex += desc.Offset || 0;
if (subIndex === 0){
return null; //Pointer always start at 1, if zero the array that it points to is empty
}
}else{
subIndex = desc.SubIndex;
}
let var_out = await this._readDevManagData(desc.Area, module.moduleId, subIndex, desc.Type, desc.Enum || null);
return var_out;
}catch(err){throw err}
}
}
/**
* Reads a variable from the device manager and parse it to a correct javascript object
* @param {number} area Area code with table id included ex. Device area = 0xF, Table ID 0x1 => 0xF001.
* @param {number} module Module id ex. 0x20, is a dynamically value and should be read from the device area.
* @param {number} subindex Subindex from the table .
* @param {string} dataType Datatype Ex. BOOL/UINT/STRING(128)/...
* @param {string} enumDef Name of the enum definition in MDP_ENUMS
*/
async _readDevManagData( area, module, subindex, dataType, enumDef){
//Find datatype
let type = ads.ADS.BASE_DATA_TYPES.find(dataType);
if (type == null) {
throw new Error(`Base type ${type} not found from BaseDataTypes - If this should be found, report an issue`)
};
//Read data
try{
let IndexOffset = (((area | ( module*Math.pow(2,4) ) ) *Math.pow(2,16)) | subindex) >>> 0;
let devManData = await this.client.readRaw(mdp.INDEXGROUP,IndexOffset,type.size,10000);
if( type.adsDataType == ads.ADS.ADS_DATA_TYPES.ADST_STRING || type.adsDataType == ads.ADS.ADS_DATA_TYPES.ADST_WSTRING){
//Read string
let var_out = devManData.toString().replace(/\0+$/g, ''); //Replace nulls at the end
var_out = var_out.split("\0"); //Split on null termination (serperator in some cases)
if(var_out.length === 1){var_out = var_out[0]};
return var_out;
}else{
//Read single item
let var_out = type.fromBuffer(devManData, this.client.settings);
if (enumDef in mdp.MDP_ENUMS){
if (var_out in mdp.MDP_ENUMS[enumDef]){
var_out = mdp.MDP_ENUMS[enumDef][var_out];
}
}
return var_out;
}
} catch (err) {
let errCode = ((err||{}).adsErrorInfo||{}).adsErrorCode || -1; //Get ads error if it exists
if( this._isModuleError(errCode)){ //Ignore module errors like 0xECAF0608: "Com Port not set"
return null;
}
if( this._isMdpError(errCode) ){
//String and no data available => empty
if (errCode == 3970302213){//No data available
switch(type.adsDataType){
case ads.ADS.ADS_DATA_TYPES.ADST_STRING:
case ads.ADS.ADS_DATA_TYPES.ADST_WSTRING:
return "";
default:
return null;
}
}
return null; //Always return null
/*
//Not supported, not implemented, invalid index
if(errCode == 3970302208 || errCode == 3970306048 || errCode == 3970305792){
return null;
}
throw new Error(`Failed to read device manager data in area: ${area.toString(16)}, module: ${module.toString(16)}, subindex: ${subindex.toString(16)}, datatype: ${dataType}: MDP_error: ${this._MdpErrorCode(errCode)}`)
*/
}else{
throw new Error(`Failed to read device manager data in area: ${area.toString(16)}, module: ${module.toString(16)}, subindex: ${subindex.toString(16)}, datatype: ${dataType}: ${err} - ${errCode}`)
}
}
};
/**
* Check if error is a MDP error
* @param {} err error number
*/
_isMdpError(err){
return (err >>> 16)==0xECA6
}
/**
* Check if error is a Module specific error
* @param {} err error number
*/
_isModuleError(err){
return (err >>> 16)==0xECAF
}
/**
* Convert MPD error to string
* @param {} err error number
*/
_MdpErrorCode(err){
if( err in mdp.MDPERRORCODES){
return mdp.MDPERRORCODES[err];
}else{
return "Unknown MDP Error: " + err;
}
}
}