/** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2020-03-10 14:50:13 * @modify date 2020-11-25 13:37:27 * @desc [description] */ import * as handlebars from "handlebars"; import { v4 as uuidv4 } from "uuid"; import { deepClone, rsetattr } from "../../../lib/helpers/objectMethods"; import { padString, replaceAll } from "../../../lib/helpers/stringMethods"; import { ILogicNetwork } from "../../ZISS-Network/type/ILogicNetwork"; import { IArcExpression } from "../../ZISS-Petri-Net/types/IArcAnnotation"; import { IEC_66113_ConvertType, IEC_66113_convertValues } from "../helpers/IEC66113_helpers"; import { IParseableLogic, IParseableTransitionDescription, IParsedLogic, IVariableDescription } from "../types/interface"; import { IParseableInstance } from "../types/plc.additional.interfaces"; import { LOGIC_TEMPLATES } from "./beckhoff.petri-net.templates"; import { AbstractParser } from "./default.parser"; interface IFunctionAnalyzerHelper { pathDone: string, doneRequired: boolean, pathError: string, errorRequired: boolean, } export interface IParseOptions { nameOfPlc: string, nameOfMainFb: string, nameOfStatesType: string, nameOfTransitionType: string, nameOfUsedTransitionsVariable: string, maxRecursion: number, maxIterations: number, additionalDelay: number, } export const defaultParseOptions: IParseOptions = { nameOfPlc: "plc-0001", nameOfMainFb: "FB_MAIN", nameOfStatesType: "STATES_DUT", maxRecursion: 15, maxIterations: 20, nameOfTransitionType: "TRANSITION_PARAMETERS_DUT", nameOfUsedTransitionsVariable: "TRANSTIONS_DUT", additionalDelay: 20 }; export class BeckhoffParser extends AbstractParser { protected _context: IParsedLogic; protected _parsableContext: IParseableLogic; public variableDefinition(context: IParsedLogic): string { throw new Error("Method not implemented."); } /** * Function which will generate Parameters for the desired Function * @param task The Taskobject. */ protected _generateParameters(task: IParseableTransitionDescription) { // Only Adapt a Copy const ret = deepClone(task.params); const pair = new Map(); // Adapt the Parameters. for (const updateDefinition of task.dynamicParams) { pair.set( updateDefinition.usedVariable, "\"" + updateDefinition.usedVariable + "\"" ); rsetattr( ret, updateDefinition.parameterPointer, updateDefinition.usedVariable ); } const obj = JSON.stringify(ret, undefined, 4); // Create The Text which will be used for the Parameters for (const [key, value] of pair.entries()) { replaceAll(obj, value, key); } return obj; } // Array, containing all Elements which should be replaced. protected _replace: { regex: RegExp; replacer: string; }[] = [ { regex: /(?<=[ \]\)])or(?=[ \[\(])/g, replacer: "OR" }, { regex: /(?<=[ \]\)])and(?=[ \[\(])/g, replacer: "AND" }, { regex: /(?<=[ \]\)])!=(?=[ \[\(])/g, replacer: "<>" }, { // (?<=[ \]\)]) regex: /not(?=[ \[\(])/g, replacer: "NOT" }, { regex: /(?<=[ \]\)])\|(?=[ \[\(])/g, replacer: "OR" }, { regex: /(?<=[ \]\)])\&(?=[ \[\(])/g, replacer: "AND" }, { regex: /(?<=[ \]\)])true(?=[ \[\(])/g, replacer: "TRUE" }, { regex: /(?<=[ \]\)])false(?=[ \[\(])/g, replacer: "FALSE" } ]; /** * Function, which will be used to adapt the Path. * @param value */ protected _adaptGuard( guard: string, simplifiedGuard: string, OriginalScope: { [index: string]: { source: string; schema: IJsonSchema; }; }, simplifiedScope: { [index: string]: { source: string; schema: IJsonSchema; }; }, mappingOriginalToSimplified: Map, mappingSimplifiedToOriginal: Map, mappingSimplifiedToAdapted: Map, mappingAdaptedToSimplified: Map, mappingVariableToRoot: Map ) { let adaptedGuard = simplifiedGuard; if (adaptedGuard === "true"){ return "TRUE"; } else if (adaptedGuard === "false"){ return "FALSE"; } // Firstly get rid off the Elements (Chars, ot replace in the Equation) for (const data of this._replace) { adaptedGuard = adaptedGuard.replace(data.regex, data.replacer); } // Adapt the used Names for (const [simple, adapted] of mappingSimplifiedToAdapted) { adaptedGuard = replaceAll(adaptedGuard, simple, adapted); } return adaptedGuard; } /** * Function, which will be used to adapt a Variable Name. * @param value The original Variable Name */ protected _adaptVariableName( variable: string, simplifiedName: string, path: string ) { return simplifiedName; } public functionBlocks: { [index: string]: { // Defaultname (Type Name e.g. TON) name: string, // Variables used on the PLC Side. plcInputs: { name: string, type: string }[], plcOutputs: { name: string, type: string }[], // Custom Flags for activating / deactivating the Block start: string, stop: string, // Code Snippets for testing errors and success error: string, done: string, // Mapping between the PLC inputs and outputs and system internal inputs / outputs. inputMapping: { // Index = Path of the Parameter, value = PLC-Input [index: string]: string, } outputMapping: { // Index = Path of the Parameter, value = PLC-Input [index: string]: string, }, // Optional Function to generate a custom Name. generateInstanceName?: ( transition: IParseableTransitionDescription, context: IParsedLogic, handlebars: any, name: string, ) => string // Optional Template, which should be used for the Generation of the Data, during triggering triggerTemplate?: string, // Optional Template, which should be used for the Generation of the Code during the Main-Process callTemplate?: string, // Flag indicating, whe requiresInstance: boolean, deactivateOnDone: boolean } } = { "timer.wait": { name: "TON", done: "{{name}}{{{selector}}}Q", error: "FALSE", inputMapping: { value: "{{name}}{{{selector}}}PT := INT_TO_TIME({{value}});" }, outputMapping: {}, plcInputs: [ { name: "IN", type: "BOOL" }, { name: "PT", type: "TIME" } ], plcOutputs: [ { name: "Q", type: "BOOL" }, { name: "ET", type: "TIME" } ], start: "{{name}}{{{selector}}}IN := TRUE;", stop: "{{name}}{{{selector}}}IN := FALSE;", requiresInstance: true, deactivateOnDone: false } } protected _defaultInstanceName( transition: IParseableTransitionDescription, context: IParsedLogic, handlebars: any, name: string, ) { return "FB_" + transition.id + "_" + name; } /** * Helperfunction, which will generate a Context for a Function. * * @protected * @param {IParseableTransitionDescription} transition * @returns * @memberof BeckhoffParser */ protected _getFunctionHelperContext(transition: IParseableTransitionDescription) { const _ret: IFunctionAnalyzerHelper = { // Path to the Done Variable, if available. pathDone: this._context.mappingSimplifiedToAdapted.get( this._context.mapPathToSimplifiedName.get( transition.id + ".function.output.done" ) ), // Flag, indicating, whether a Done is requested doneRequired: this._context.mapPathToSimplifiedName.has( transition.id + ".function.output.done" ), // Path to the Variable in which the corresponding error could be found. pathError: this._context.mappingSimplifiedToAdapted.get( this._context.mapPathToSimplifiedName.get( transition.id + ".function.output.error" ) ), // Flag, indicating, whether the Error of this Function is Checked or not. errorRequired: this._context.mapPathToSimplifiedName.has( transition.id + ".function.output.error" ) }; return _ret as any as T & IFunctionAnalyzerHelper; } protected _timeAdaption(placeId: string): string { const _this = this; // Get the Place matched to the ID const place = this._context.places.filter(p => p.id === placeId)[0]; // Extract all involved Transitions. const transitions: IParseableTransitionDescription[] = [ ... place.consumedBy.map(transId => _this._parsableContext.transitions.filter(trans => (trans.id === transId))[0]), ... place.producedBy.map(transId => _this._parsableContext.transitions.filter(trans => (trans.id === transId))[0]), ... place.requiredBy.map(transId => _this._parsableContext.transitions.filter(trans => (trans.id === transId))[0]), ... place.avoidedBy.map(transId => _this._parsableContext.transitions.filter(trans => (trans.id === transId))[0]) ]; const _context = { transitions }; // Create a Template to Test Execute the Function const _template = ` {{#each transitions}} {{methodName}}_ADAPT_TIMES(iCurrentTime); {{/each}} `; return handlebars.compile(_template)(_context); } /** * List containing Functions. This can be used to create a custom code generation-Function * per Function. Therefore the function will be linked by the index. * * @memberof BeckhoffParser */ public functions: { [index: string]: { generateFunctionTriggerCode: ( transition: IParseableTransitionDescription, context: IParsedLogic, handlebars: any ) => string, generateInstanceStructure?: ( transition: IParseableTransitionDescription, context: IParsedLogic, handlebars: any ) => IParseableInstance } } = { "plc.set": { generateFunctionTriggerCode: (transition, context, handlebars) => { // TODO: Update the Setters and Getters // Mark the Task as Done. // Test Object. Is used to update the Code const _context = this._getFunctionHelperContext<{ entries: any[] }>(transition); _context.entries = []; // Push all Ports to the Entries, which will be set. for (const port in transition.params) { _context.entries.push({ port: "GVL_IOS." + port, value: IEC_66113_convertValues(transition.params[port]) }); } // Although push all "dynamic" Variables to the Entries. for (const param of transition.dynamicParams) { _context.entries.push({ port: param.parameterPointer, value: this._adaptVariableName("", param.usedVariable, "") }); } // Use a Template, which will add const template = ` {{#if doneRequired}} {{pathDone}} := FALSE; {{/if}} {{#if errorRequired}} {{pathError}} := FALSE; {{/if}} {{#each entries}} {{port}} := {{value}}; {{/each}} {{#if doneRequired}} {{pathDone}} := TRUE; {{/if}} {{#if errorRequired}} {{pathError}} := FALSE; {{/if}} `; return handlebars.compile(template)(_context); }, }, "math.calculate": { generateFunctionTriggerCode: (transition, context, handlebars) => { return "calc"; }, } }; /** * Function, which will be used to generate Function Code. * @param taskData */ protected _getFunctionTriggerCode( _transitionDescription: IParseableTransitionDescription ) { const transition = deepClone(_transitionDescription); if (transition.functionName && this.functions[transition.functionName]) { const _func = this.functions[transition.functionName].generateFunctionTriggerCode; return _func(transition, this._context, handlebars); } else if (this.functionBlocks && this.functionBlocks[transition.functionName]) { const _func = this.functionBlocks[transition.functionName]; // Generate the Name. const _name = _func.generateInstanceName ? _func.generateInstanceName(transition, this._context, handlebars, _func.name) : this._defaultInstanceName(transition, this._context, handlebars, _func.name); // Generate a Helper Context Element. const _context = this._getFunctionHelperContext<{ entries: any[], doneGetter: string, errorGetter: string, start: string, startReset: string, stop: string, stopReset: string, }>(transition); _context.entries = []; // Push all Ports to the Entries, which will be set. for (const param in transition.params) { if (_func.inputMapping[param]) { _context.entries.push({ mapping: handlebars.compile(_func.inputMapping[param])({ value: transition.params[param], name: _name, selector: "_" }) }); } } // Add all Dynamic Values to the Test. for (const param of transition.dynamicParams) { if (_func.inputMapping[param.parameterPointer]) { _context.entries.push({ mapping: handlebars.compile(_func.inputMapping[param.parameterPointer])({ value: param.usedVariable, name: _name, selector: "_" }) }); } } // Extend the Context. _context.doneGetter = handlebars.compile(_func.done)({ name: _name, selector: "." }); _context.errorGetter = handlebars.compile(_func.done)({ name: _name, selector: "." }); _context.start = _func.start ? handlebars.compile(_func.start)({ name: _name, selector: "_" }) : ""; _context.startReset = _func.start ? handlebars.compile(_func.start)({ name: _name, selector: "_" }) : ""; _context.stop = _func.stop ? handlebars.compile(_func.stop)({ name: _name, selector: "_" }) : ""; _context.stopReset = _func.stop ? handlebars.compile(_func.stop)({ name: _name, selector: "_" }) : ""; // Use a Template, which will add const template = _func.triggerTemplate || ` {{#if doneRequired}} // Mark execution as Active {{{pathDone}}} := FALSE; {{/if}} {{#if errorRequired}} // Reset the Error {{{pathError}}} := FALSE; {{/if}} // Stop the Block. {{{stopReset}}} updateInstances(); // Activate the Block {{{start}}} // Parameter assignment {{#each entries}} {{{mapping}}} {{/each}} updateInstances(); {{#if doneRequired}} // Update the State of the Execution of the Block {{{pathDone}}} := {{{doneGetter}}}; {{#if deactivateOnDone}} // Deactivate the Function-Block: IF {{{pathDone}}} THEN {{{stop}}} END_IF; {{/if}} {{/if}} {{#if errorRequired}} // Update the Error Flag {{{pathError}}} := {{{errorGetter}}}; {{/if}} `; return handlebars.compile(template)(_context); } return "// Unkown Function. ==> Using remote Broker"; } /** * Function, which will be used to generate Function Code. * @param taskData */ protected _getFunctionCallCode( _transitionDescription: IParseableTransitionDescription ): IParseableInstance | null { const transition = deepClone(_transitionDescription); if (transition.functionName && this.functions[transition.functionName] && this.functions[transition.functionName].generateInstanceStructure) { const _func = this.functions[transition.functionName].generateInstanceStructure; return _func(transition, this._context, handlebars); } else if (this.functionBlocks && this.functionBlocks[transition.functionName] && this.functionBlocks[transition.functionName].requiresInstance) { const _func = this.functionBlocks[transition.functionName]; // Generate the Name. const _name = _func.generateInstanceName ? _func.generateInstanceName(transition, this._context, handlebars, _func.name) : this._defaultInstanceName(transition, this._context, handlebars, _func.name); // Generate a Helper Context Element. const _context = this._getFunctionHelperContext<{ inputs: { instanceVar: string, accessor: string, }[], outputs: { instanceVar: string, accessor: string, }[], hasInputs: boolean, hasOutputs: boolean, stop: string, doneGetter: string }>(transition); _context.hasInputs = _func.plcInputs ? _func.plcInputs.length > 0 : false; _context.hasOutputs = _func.plcOutputs ? _func.plcOutputs.length > 0 : false; _context.inputs = []; _context.outputs = []; // Extend the Context. _context.doneGetter = handlebars.compile(_func.done)({ name: _name, selector: "_" }); _context.stop = _func.stop ? handlebars.compile(_func.stop)({ name: _name, selector: "_" }) : ""; for (const input of _func.plcInputs) { _context.inputs.push({ accessor: _name + "_" + input.name, instanceVar: _name + "." + input.name }); } for (const output of _func.plcOutputs) { _context.outputs.push({ accessor: _name + "_" + output.name, instanceVar: _name + "." + output.name }); } // Use a Template, which will add const template = _func.callTemplate || (` {{#if hasInputs}} // Set the Inputs {{#each inputs}} {{instanceVar}} := {{accessor}}; {{/each}} {{/if}} {{#if hasOutputs}} // Set the Outputs provided by the FB {{#each outputs}} {{accessor}} := {{instanceVar}}; {{/each}} {{/if}} {{#if doneRequired}} // Update the State of the Execution of the Block {{{pathDone}}} := {{{doneGetter}}}; {{#if deactivateOnDone}} // Deactivate the Function-Block: IF {{{pathDone}}} THEN {{{stop}}} END_IF; {{/if}} {{/if}} {{#if errorRequired}} // Update the Error Flag {{{pathError}}} := {{{errorGetter}}}; {{/if}} `); const _ret: IParseableInstance = { fBName: _func.name, callCode: handlebars.compile(template)(_context), inputs: _func.plcInputs.map(item => { return { name: _name + "_" + item.name, type: item.type }; }), outputs: _func.plcOutputs.map(item => { return { name: _name + "_" + item.name, type: item.type }; }), instanceName: _name, transitionName: transition.id, }; return _ret; } return null; } protected _convertInput(definition: { // Amount of Tokens that will be consumed (Result must be greater the minTokens) consumed: IArcExpression; placeId: string; type: | "expression" | "inhibitor" | "multiexpression" | "test" | "value" | "variable"; minTokens: number; }) { return { placeId: definition.placeId, minTokens: definition.minTokens, tokensToRemove: 1 }; } protected _convertOutput(definition: { // Amount of Tokens that will be produced produced: IArcExpression; placeId: string; type: | "expression" | "inhibitor" | "multiexpression" | "test" | "value" | "variable"; maxTokens: number; }): { // Amount of Tokens that will be produced placeId: string; maxTokens: number; tokensToAdd: number; } { return { placeId: definition.placeId, maxTokens: definition.maxTokens, tokensToAdd: 1 }; } protected _getAccessorOfGuardVariable(data: IVariableDescription): string { // The Input of the Path, based on a Variable is defined as // PLC-Name.[input/output].[module].[name] // plc_0003.input.GVL_IOS.bInput24VMotorenIO const splitted = data.path.split(".").slice(2); return splitted.join("."); } protected _templates: { [index: string]: string; } = LOGIC_TEMPLATES; /** * Function, which will generate the MainLoop. * @param network */ mainLoop(network: ILogicNetwork): string { this.linkTemplates(network); const render = handlebars.compile(this._templates.mainTemplate); return render(this._parsableContext); } public linkTemplates(network: ILogicNetwork, ...args): void { super.linkTemplates(); // Reference to this object. const _this = this; // Register Functions handlebars.registerHelper("getFunctionTriggerCode", (taskData) => _this._getFunctionTriggerCode(taskData) ); // Register Functions handlebars.registerHelper("uuid", () => "{" + uuidv4() + "}"); handlebars.registerHelper("getAccessorOfGuardVariable", (data) => _this._getAccessorOfGuardVariable(data) ); handlebars.registerHelper("timeAdaption", (placeId) => _this._timeAdaption(placeId) ); // Generate the Context this._context = this.parseLogic(network); this._parsableContext = this.getParseableLogic(network); } protected _parseLogic: IParsedLogic /** * Helper Function, to Generate the desired Logic. * @param _network */ parseLogic(_network: ILogicNetwork): IParsedLogic { if (this._network != _network) { const ret = super.parseLogic(_network); for (const _var of ret.subscribedVariables){ _var.type = IEC_66113_ConvertType(_var.type) as any; } for (const _var of ret.variables){ _var.type = IEC_66113_ConvertType(_var.type) as any; } this._parseLogic = ret; } return this._parseLogic; } /** * Function to create Project related Files. * * @param {ILogicNetwork} network The Network containing the Logic * @param {IParseOptions} [options=defaultParseOptions] Additional Options for Parser * @returns * @memberof BeckhoffParser */ public createProjectFiles(network: ILogicNetwork, options: IParseOptions = defaultParseOptions) { // Reference to the Own Object. const _this = this; // Internally the Templates this.linkTemplates(network); const codeFiles: {path: string, code: string}[] = [ { path: "POUs/"+options.nameOfMainFb + ".TcPOU", code: this.generateMainFB(network,options), }, { path: "DUTs/"+options.nameOfStatesType + ".TcDUT", code: this.generateStatesType(network,options), }, { path: "DUTs/"+options.nameOfTransitionType + ".TcDUT", code: this.generateTransitionType(network,options), }, { path: "DUTs/"+options.nameOfUsedTransitionsVariable + ".TcDUT", code: this.generateTransitionsVariable(network,options), } ]; // Array, cotaining all Files. These will be packed into a ZIP-File Later on. const files: {path: string, code: string}[] = [ ... codeFiles, { path: "PlcTask.TcTTO", code: handlebars.compile(this._templates.plcTask)(this.getParseableLogic(network)) }, { path: "Testcode.plcproj", code: handlebars.compile(this._templates.plcProject)({codeFiles: codeFiles.map(item => { return { path: replaceAll(item.path, "/", "\\") }; })}) } ]; return files; } protected _network: ILogicNetwork; protected _options: IParseOptions protected _getParseableLogic: IParseableLogic; public getParseableLogic(network: ILogicNetwork, options: IParseOptions = defaultParseOptions): IParseableLogic{ if (this._network != network && this._options != options) { this._network = network; this._options = options; const ret = super.getParseableLogic(network); // Name of the Function Name. (ret as any).options = options; (ret as any).instances = []; const _this = this; const _adapt = (trans, length) => { // Adding a Vaild Method Name. (trans as any).methodName = "T" + padString(trans.idx,length,true) + "_" +trans.id; (trans as any).hasInputs = trans.consumed.length > 0; (trans as any).hasOutputs = trans.produced.length > 0; (trans as any).testOutputs = trans.produced.filter(place => place.maxTokens > 0).map(item => Object.assign(deepClone(options), item)); (trans as any).hasTestOutputs = (trans as any).testOutputs.length > 0; (trans as any).hasTriggers = trans.triggers.length > 0; (trans as any).hasLocks = trans.locks.length > 0; (trans as any).hasReleases = trans.releases.length > 0; // Transmit the Options (trans as any).options = options; for (const name of ["avoided", "produced", "testOutputs", "consumed", "required", "triggers"]){ trans[name] = trans[name].map(item => Object.assign({options},item)); } // Test if Instance Code must be generated if (trans.functionName && ( (_this.functions && _this.functions[trans.functionName] && _this.functions[trans.functionName].generateInstanceStructure) || (_this.functionBlocks && _this.functionBlocks[trans.functionName] && _this.functionBlocks[trans.functionName].requiresInstance) ) ) { // Instance Code must be generated (ret as any).instances.push(_this._getFunctionCallCode(trans)); } }; for (const [idx,trans] of ret.transitions.entries()){ _adapt(trans,ret.transitions.length); } ret.transitionsSortedByPriority = ret.transitions.sort(dynamicSort("priority")); for (const [idx,trans] of ret.actions.entries()){ _adapt(trans,ret.transitions.length); } // Transmit the Options for (const name of ["places", "initialMarking", "subscribedVariables", "transitions", "variables"]){ ret[name] = ret[name].map(item => Object.assign({options},item)); } this._getParseableLogic = ret; } return this._getParseableLogic; } public generateMainFB(network: ILogicNetwork, options: IParseOptions = defaultParseOptions): string { this.linkTemplates(network); return handlebars.compile(this._templates.mainTemplate)(this.getParseableLogic(network, options)); } public generateStatesType(network: ILogicNetwork, options: IParseOptions = defaultParseOptions): string { this.linkTemplates(network); return handlebars.compile(this._templates.statesStructTemplate)(this.getParseableLogic(network, options)); } public generateTransitionsVariable(network: ILogicNetwork, options: IParseOptions = defaultParseOptions): string { this.linkTemplates(network); return handlebars.compile(this._templates.transitionVariableTemplate)(this.getParseableLogic(network, options)); } public generateTransitionType(network: ILogicNetwork, options: IParseOptions = defaultParseOptions): string { this.linkTemplates(network); return handlebars.compile(this._templates.transitionStructTemplate)(this.getParseableLogic(network, options)); } }