UNPKG

@p2olab/pimad-core

Version:

PiMAd (Process-industry-Modular-Automation-description) High level access to automation of modular plants.

584 lines (583 loc) 32.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TextPart = exports.ServicePart = exports.MTPPart = exports.HMIPart = void 0; const ModuleAutomation_1 = require("../../ModuleAutomation"); const Utils_1 = require("../../Utils"); const Backbone_1 = require("../../Backbone"); const CommunicationInterfaceData_1 = require("../../ModuleAutomation/CommunicationInterfaceData"); var PiMAdResponseVendor = Backbone_1.Backbone.PiMAdResponseVendor; var DataAssemblyVendor = ModuleAutomation_1.ModuleAutomation.DataAssemblyVendor; var DataAssemblyType = ModuleAutomation_1.ModuleAutomation.DataAssemblyType; const uuid_1 = require("uuid"); class AImporterPart { constructor() { this.responseVendor = new PiMAdResponseVendor(); } extract(data, callback) { const localResponse = this.responseVendor.buyErrorResponse(); localResponse.initialize('Not implemented yet!', {}); callback(localResponse); } /** * Extract a specific attribute from an Attribute Array. F.ex. the RefID-Attribute. * @param attributeName - The name of the attribute. * @param attributes - The attributes array. * @param callback - A callback function with an instance of the Response-Interface. */ getAttribute(attributeName, attributes, callback) { attributes.forEach((attribute) => { if (attribute.Name === attributeName) { const localResponse = this.responseVendor.buySuccessResponse(); localResponse.initialize('Success!', attribute); callback(localResponse); } }); } } class HMIPart extends AImporterPart { } exports.HMIPart = HMIPart; /** * Handles the 'MTPPart' of a ModuleTypePackage file. This means CommunicationSet, HMISet, ServiceSet, ... */ class MTPPart extends AImporterPart { constructor() { super(); this.communicationInterfaceDataVendor = new ModuleAutomation_1.CommunicationInterfaceDataVendor(); this.dataAssemblyVendor = new DataAssemblyVendor(); this.baseDataItemFactory = new ModuleAutomation_1.BaseDataItemFactory(); } /** * This method extracts data from the MTPPart of the ModuleTypePackage and converts it into different objects of the PiMAd-core-IM. * * Attention! Currently (31.07.2020) only data from the CommunicationSet is processed! * * <uml> * skinparam shadowing false * partition "extract" { * start * :extract communicationSet; * if (extracted data is valid?) is (true) * :callback the data\nwith SuccessResponse; * else (false) * :callback an\nErrorResponse; * endif * stop * } * </uml> * * @param data - The bare ModuleTypePackage-object of the MTP. Containing a CommunicationSet, HMISet, ServiceSet and TextSet. * @param callback - A callback function with an instance of the Response-Interface. The type of the response-content-object-attribute data is {@link ExtractDataFromCommunicationSetResponseType} */ extract(data, callback) { const communicationSet = this.extractDataFromCommunicationSet(data.CommunicationSet); if (communicationSet.ServerCommunicationInterfaceData.length === 0 && communicationSet.DataAssemblies.length === 0) { const localeResponse = this.responseVendor.buyErrorResponse(); localeResponse.initialize('Could not parse the CommunicationSet!', {}); callback(localeResponse); } else { const localeResponse = this.responseVendor.buySuccessResponse(); localeResponse.initialize('Success!', communicationSet); callback(localeResponse); } } getRefBaseSystemUnitPathElement(communicationSet, refBaseSystemUnitPath, callback) { communicationSet.forEach((setElement) => { // First of all: typing const elementWithListType = setElement; if (elementWithListType.RefBaseSystemUnitPath === refBaseSystemUnitPath) { callback(elementWithListType); } }); } /** * This method extracts data from the ModuleTypePackage-CommunicationSet, evaluates it and then transfers it to the * PiMAd-core internal data model. The following diagram visualizes the general method flow: * * <uml> * skinparam shadowing false * partition "extractDataFromCommunicationSet" { * start * :extract InstanceList & \nSourceList; * if(extracted data seems valid) then (yes) * :parsing SourceList; * :parsing InstanceList; * :CommunicationInterfaceData: communicationInterfaceData, \nDataAssemblies: dataAssemblies ] * stop * else (no) * endif * :CommunicationInterfaceData: [], \nDataAssemblies: [] ] * stop * } * </uml> * * The following activity diagram shows the detailed steps for parsing the SourceList: * * <uml> * skinparam shadowing false * partition "extractDataFromCommunicationSet:parsing-SourceList" { * start * while (more InternalElements?) is (true) * if (element == OPCUAServer?) then (yes) * :extract opcua-\nserver interface; * :extract external interfaces; * else (no) * stop * endif * endwhile (no) * stop * } * </uml> * * The following activity diagram shows the detailed steps for parsing the InstanceList: * * <uml> * skinparam shadowing false * partition "buildCommunicationSet:parsing-InstanceList" { * start * while (more InternalElements\nin InstanceList?) is (true) * :setup local\ndata storage; * while (more Attributes\nin InternalElement?) is (true) * if(attributeDataType == xs:IDREF) * :merging attribute data\nwith external interface data; * :initialize DataItemModel with\nmerged data and push\nit to the local storage; * elseif (attributeDataType == xs:ID) * :save the RefID as\nDataAssembly-Identifier; * endif * endwhile (no) * :initialize DataAssembly with\nparsed data and push it\to the local storage; * endwhile (no) * stop * } * </uml> * * @param communicationSet - The bare CommunicationSet-object of the MTP. */ extractDataFromCommunicationSet(communicationSet) { // The following arrays will be continuously filled with data in the following lines. // const communicationInterfaceData: CommunicationInterfaceData[] = []; const serverCommunicationInterfaceData = []; const dataAssemblies = []; const localExternalInterfaces = []; // Extract InstantList and SourceList from communicationSet // TODO: I like this approach: const localProcedureDataAssembly: DataAssembly | undefined = dataAssemblies.find(dataAssembly => service.DataAssembly.Value === dataAssembly.getIdentifier()) let instanceList = {}; this.getRefBaseSystemUnitPathElement(communicationSet, 'MTPSUCLib/CommunicationSet/InstanceList', extractedInstanceList => { instanceList = extractedInstanceList; }); let sourceList = {}; this.getRefBaseSystemUnitPathElement(communicationSet, 'MTPSUCLib/CommunicationSet/SourceList', extractedSourceList => { sourceList = extractedSourceList; }); // Check instanceList/sourceList isn't empty. if (JSON.stringify(instanceList) == JSON.stringify({}) || JSON.stringify(sourceList) == JSON.stringify({})) { Utils_1.logger.error('Could not extract InstanceList and SourceList of the CommunicationSet. Aborting!'); return { DataAssemblies: [], ServerCommunicationInterfaceData: [] }; } /* Easier handling of 'single' and 'multiple' sources in one code section. Therefore a single source is transferred to an array with one entry. */ if (!(Array.isArray(sourceList.InternalElement))) { sourceList.InternalElement = [sourceList.InternalElement]; } // Handle the SourceList. Mainly extracting the server communication interfaces. sourceList.InternalElement.forEach((sourceListItem) => { // So far we only know MTPs with a OPCUAServer as source. switch (sourceListItem.RefBaseSystemUnitPath) { case 'MTPCommunicationSUCLib/ServerAssembly/OPCUAServer': { const localDataItem = this.baseDataItemFactory.create(); const sourceListElementAttribute = sourceListItem.Attribute; // Extract the server communication interface switch (sourceListElementAttribute.AttributeDataType) { // TODO: standard for endpoint MTP? static, dynamic? case ('xs:string'): if (localDataItem.initialize({ dataType: sourceListElementAttribute.AttributeDataType, defaultValue: sourceListElementAttribute.DefaultValue, description: sourceListElementAttribute.Description, name: sourceListElementAttribute.Name, pimadIdentifier: 'TODO', value: sourceListElementAttribute.Value })) { serverCommunicationInterfaceData.push(localDataItem); } else { Utils_1.logger.warn('Cannot extract source <' + sourceListItem.Name + '> need MTPFreeze-2020-01!'); } //case('xs:IDREF'): } /* Easier handling of 'single' and 'multiple' sources in one code section. Therefore a single source is transferred to an array with one entry. */ if (!(Array.isArray(sourceListItem.ExternalInterface))) { sourceListItem.ExternalInterface = [sourceListItem.ExternalInterface]; } /* Store the source specific 'ExternalInterface' data temporary. You need these in the next parsing step. */ sourceListItem.ExternalInterface.forEach((sourceListElementExternalInterfaceItem) => { localExternalInterfaces.push(sourceListElementExternalInterfaceItem); }); break; } default: Utils_1.logger.warn('Unknown RefBaseSystemUnitPath of source' + sourceListItem.Name + '! Skipping ...'); break; } }); // Handle the InstanceList. Merging data with SourceList and generating mainly PiMAd-core-DataAssemblies. if (!(Array.isArray(instanceList.InternalElement))) { instanceList.InternalElement = [instanceList.InternalElement]; } instanceList.InternalElement.forEach((instanceListElement) => { // like above these variables will be continuously filled with data in the following lines. const localDataItems = []; let dataAssemblyIdentifier = ''; // iterate through all attributes /* Easier handling of 'single' and 'multiple' attributes in one code section. Therefore a single attribute is transferred to an array with one entry. */ if (!(Array.isArray(instanceListElement.Attribute))) { instanceListElement.Attribute = [instanceListElement.Attribute]; } /* if(instanceListElement.RefBaseSystemUnitPath.includes('HealthStateView') || instanceListElement.RefBaseSystemUnitPath.includes('ServiceControl')){ return; }*/ instanceListElement.Attribute.forEach((instanceListElementAttribute) => { // These are treated differently depending on the attribute data type. const localDataItem = this.baseDataItemFactory.create(); switch (instanceListElementAttribute.AttributeDataType) { case 'xs:string': if (localDataItem.initialize({ dataType: instanceListElementAttribute.AttributeDataType, defaultValue: instanceListElementAttribute.DefaultValue, description: instanceListElementAttribute.Description, name: instanceListElementAttribute.Name, pimadIdentifier: 'TODO', value: instanceListElementAttribute.Value })) { localDataItems.push(localDataItem); } break; case 'xs:byte': if (localDataItem.initialize({ dataType: instanceListElementAttribute.AttributeDataType, defaultValue: instanceListElementAttribute.DefaultValue, description: instanceListElementAttribute.Description, name: instanceListElementAttribute.Name, pimadIdentifier: 'TODO', value: instanceListElementAttribute.Value })) { // push to the list of data items. later it will be aggregated in the data assembly object. localDataItems.push(localDataItem); } break; case 'xs:boolean': if (localDataItem.initialize({ dataType: instanceListElementAttribute.AttributeDataType, defaultValue: instanceListElementAttribute.DefaultValue, description: instanceListElementAttribute.Description, name: instanceListElementAttribute.Name, pimadIdentifier: 'TODO', value: instanceListElementAttribute.Value })) { // push to the list of data items. later it will be aggregated in the data assembly object. localDataItems.push(localDataItem); } break; case 'xs:IDREF': /* First we need the element with the correct ID from the ExternalInterfaceList. Safe some time with Array.prototype.some(). Therefore the 'strange' syntax in the last part. */ localExternalInterfaces.some((localeExternalInterface) => { // TODO: Extract description via new switch case scenario if (localeExternalInterface.ID === instanceListElementAttribute.Value) { /* Merging attribute and external interface data to one communication interface. */ // TODO: Aktuell gehen wir immer von opcua nodes aus. Kann sich in Zukunft ändern! const opcuaNodeCommunication = this.communicationInterfaceDataVendor.buy(CommunicationInterfaceData_1.CommunicationInterfaceDataEnum.OPCUANode); // ... continuously filled ... let identifier = -1; let namespace = ''; let access = ''; /* There are again attributes... looping and extracting */ /* Easier handling of 'single' and 'multiple' attributes in one code section. Therefore a single attribute is transferred to an array with one entry. */ //TODO: is this neccessary? if (!(Array.isArray(localeExternalInterface.Attribute))) { localeExternalInterface.Attribute = [localeExternalInterface.Attribute]; } localeExternalInterface.Attribute.forEach((localeInterfaceAttribute) => { switch (localeInterfaceAttribute.Name) { case ('Identifier'): identifier = localeInterfaceAttribute.Value; break; case ('Namespace'): namespace = localeInterfaceAttribute.Value; break; case ('Access'): //TODO: Do we need access? access = localeInterfaceAttribute.Value; break; default: Utils_1.logger.warn('The opcua-node-communication object contains the unknown attribute <' + instanceListElementAttribute.Name + '>! Ignoring ...'); break; } // in the last loop circle initialize the communication interface. if (localeInterfaceAttribute == localeExternalInterface.Attribute[localeExternalInterface.Attribute.length - 1]) { if (opcuaNodeCommunication.initialize({ dataSourceIdentifier: localeExternalInterface.ID, name: instanceListElementAttribute.Name, interfaceDescription: { macrocosm: namespace, microcosm: identifier }, metaModelRef: localeExternalInterface.RefBaseClassPath, pimadIdentifier: 'TODO' })) { Utils_1.logger.info('Successfully add opcua-communication <' + instanceListElementAttribute.Name + '> to DataAssembly <' + instanceListElement.Name + '>'); } else { Utils_1.logger.warn('Could not add opcua-communication <' + instanceListElementAttribute.Name + '> to DataAssembly <' + instanceListElement.Name + '>'); } } }); // Create and initialize the data item. const localDataItem = this.baseDataItemFactory.create(); if (localDataItem.initialize({ ciData: opcuaNodeCommunication, dataSourceIdentifier: localeExternalInterface.ID, defaultValue: instanceListElementAttribute.DefaultValue, description: instanceListElementAttribute.Description, dataType: instanceListElementAttribute.AttributeDataType, metaModelRef: localeExternalInterface.RefBaseClassPath, name: instanceListElementAttribute.Name, pimadIdentifier: 'TODO' })) { /* no error -> pushing it to the list of data items. later it will be aggregated in the data assembly object. */ localDataItems.push(localDataItem); } else { // logging } // Array.prototype.some() stuff... return true; } else { //Array.prototype.some() stuff... return false; } }); break; /* Now the attributes are id's and doesn't referencing to an DataAssembly/DataItemModel. In this case the id connect different data items in the MTP. (why messing up the system above with ID & RefIDs in the DataItems?)*/ case 'xs:ID': switch (instanceListElementAttribute.Name) { /* Take this data for the data assembly identifier. Why? The services f.ex. referencing on this id instead of the AML-ID. */ case 'RefID': dataAssemblyIdentifier = instanceListElementAttribute.Value; break; default: break; } break; default: break; } }); // All data for creating a DataAssembly has now been collected. Now creating ... const localDataAssembly = this.dataAssemblyVendor.buy(DataAssemblyType.BASIC); // ... and initializing. if (localDataAssembly.initialize({ tag: instanceListElement.Name, description: 'inline TODO above', dataItems: localDataItems, dataSourceIdentifier: dataAssemblyIdentifier, metaModelRef: instanceListElement.RefBaseSystemUnitPath, pimadIdentifier: uuid_1.v4() // TODO: Maybe check if uuid maybe already exists? })) { // initializing successful -> push the new data assembly to the return variable. dataAssemblies.push(localDataAssembly); localDataAssembly.getName((response, name) => { Utils_1.logger.info('Add DataAssembly <' + name + '>'); }); } else { Utils_1.logger.warn('Cannot extract all data from DataAssembly <' + instanceListElement.Name + '> need MTPFreeze-2020-01! Skipping ...'); } }); // We are done. Return the extracted Data. return { DataAssemblies: dataAssemblies, ServerCommunicationInterfaceData: serverCommunicationInterfaceData }; } } exports.MTPPart = MTPPart; /** * Handles the 'ServicePart' of the ModuleTypePackage file. */ class ServicePart extends AImporterPart { constructor() { super(); this.baseProcedureFactory = new ModuleAutomation_1.BaseProcedureFactory(); } /** * This method extracts data from the service part of the ModuleTypePackage and converts it into an intermediate * format very similar to the {@link Service} interface of the PiMAd core IM. * * <uml> * skinparam shadowing false * partition "extract" { * start * while (more services?) is (yes) * :take the next service; * :extract and store\nfirst level data of\nthe service; * while (more InternalElements?) is (yes) * :take the next element; * if (element == ServiceProcedure) is (true) * :extract the data\nof the service procedure; * :push procedure to service; * else (no) * :Skipping this element; * endif * endwhile (no) * :push service to storage; * endwhile (no) * :callback the extracted\nservices with a\nSuccessResponse; * stop * } * </uml> * * @param data - All service data as object. * @param callback - A callback function with an instance of the Response-Interface. */ extract(data, callback) { /* One big issue: In the ServicePart of the MTP are not all data to build a PiMAd-core ServiceModel. There are references to DataAssemblies extracted via the MTPPart. Therefore this one extracts the data like a quasi service. Later one the Importer merges the data of quasi service and the referenced DataAssembly to one PiMAd-core ServiceModel. */ const extractedServiceData = []; // will be the content of the response. // typing let servicePartInternalElementArray = data.InternalElement; // looping through all elements of the array. /* Easier handling of 'single' and 'multiple' attributes in one code section. Therefore a single attribute is transferred to an array with one entry. */ if (!(Array.isArray(servicePartInternalElementArray))) { servicePartInternalElementArray = [servicePartInternalElementArray]; } servicePartInternalElementArray.forEach((amlServiceInternalElement) => { // TODO > Better solution possible? // TODO > Why no check? RefBaseSystemUnitPath // Skip ServiceModel Relation in FirstPlace if (amlServiceInternalElement.RefBaseSystemUnitPath.includes('ServiceRelation')) return; let localAMLServiceInternalElementAttributes = []; if (!Array.isArray(amlServiceInternalElement.Attribute)) { localAMLServiceInternalElementAttributes.push(amlServiceInternalElement.Attribute); } else { localAMLServiceInternalElementAttributes = amlServiceInternalElement.Attribute; } // will be continuously filled while in the loop circle const localService = {}; localService.Attributes = []; localService.Identifier = amlServiceInternalElement.ID; localService.MetaModelRef = amlServiceInternalElement.RefBaseSystemUnitPath; localService.Name = amlServiceInternalElement.Name; localService.Parameters = []; localService.Procedures = []; /* extract the 'RefID'-Attribute. It's important! and referencing to the DataAssembly of the service which stores all the interface data to the hardware. */ this.getAttribute('RefID', localAMLServiceInternalElementAttributes, (response) => { if (response.constructor.name === this.responseVendor.buySuccessResponse().constructor.name) { localService.DataAssembly = response.getContent(); } }); // extract and store all other attributes this.extractAttributes(localAMLServiceInternalElementAttributes, (response => { localService.Attributes = response.getContent(); })); // extract all Procedures, etc // const internalElementArray = amlService.InternalElement.constructor === Object? [amlService.InternalElement as object as DataItemInstanceList] : amlService.InternalElement; let internalElementArray = amlServiceInternalElement.InternalElement; /* Easier handling of 'single' and 'multiple' attributes in one code section. Therefore a single attribute is transferred to an array with one entry. */ if (!(Array.isArray(internalElementArray))) { internalElementArray = [internalElementArray]; } // do foreach internalElementArray.forEach((amlServiceInternalElementItem) => { if (!amlServiceInternalElementItem) { return; } switch (amlServiceInternalElementItem.RefBaseSystemUnitPath) { case 'MTPServiceSUCLib/ServiceProcedure': { /* like the services above the data of the procedures in the MTP-ServiceSet is insufficient. Therefore use again a quasi procedure. The importer will later merge the quasi procedure and the referenced DataAssembly. */ const localProcedure = {}; localProcedure.Attributes = []; localProcedure.Identifier = amlServiceInternalElementItem.ID; localProcedure.MetaModelRef = amlServiceInternalElementItem.RefBaseSystemUnitPath; localProcedure.Name = amlServiceInternalElementItem.Name; localProcedure.Parameters = []; this.getAttribute('RefID', amlServiceInternalElementItem.Attribute, (response) => { if (response.constructor.name === this.responseVendor.buySuccessResponse().constructor.name) { localProcedure.DataAssembly = response.getContent(); } }); // extract all the other Attributes this.extractAttributes(amlServiceInternalElementItem.Attribute, (response => { localProcedure.Attributes = response.getContent(); })); localService.Procedures.push(localProcedure); // TODO: Missing ProcedureModel-Parameters break; } //case 'TODO: Missing ServiceModel-Parameters' default: Utils_1.logger.warn('Unknown >InternalElement< in service <' + amlServiceInternalElement.Name + '> Ignoring!'); break; } }); extractedServiceData.push(localService); }); const localResponse = this.responseVendor.buySuccessResponse(); localResponse.initialize('Successfully extracting the ServicePart!', extractedServiceData); callback(localResponse); } /** * Transforming AML-Attributes into AML-Attributes. Ignoring specific one. f. ex. RefID. Needs a Refactor -\> PiMAd * needs an attribute interface too! * @param attributes - The attributes array. * @param callback - A callback function with an instance of the Response-Interface. */ extractAttributes(attributes, callback) { const responseAttributes = []; /* Easier handling of 'single' and 'multiple' attributes in one code section. Therefore a single attribute is transferred to an array with one entry. */ if (!(Array.isArray(attributes))) { attributes = [attributes]; } attributes.forEach((attribute) => { switch (attribute.Name) { case 'RefID': break; default: responseAttributes.push(attribute); } if (JSON.stringify(attribute) === JSON.stringify(attributes[attributes.length - 1])) { const localeResponse = this.responseVendor.buySuccessResponse(); localeResponse.initialize('Success!', responseAttributes); callback(localeResponse); } }); } } exports.ServicePart = ServicePart; class TextPart extends AImporterPart { } exports.TextPart = TextPart;