@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
JavaScript
;
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;