/*-------------------------------------------------------------------------------------------------------------- * Copyright (c) insite-gmbh. All rights reserved. * Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------------------------*/ import { Component, ElementRef, Directive, Input, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core'; import { InaxPlcService, DataChangeEvent, IPlcEventProxy, PlcValidator } from '../../plc'; import { InaxSignalR, Guid } from '../../common'; import { PlcItem } from '../../../app/components/plcviewer/plcviewer.component'; import { InaxLoggerService } from '../../logger/src/logger'; export enum PlcInputType { readonly = 0, readwrite = 1 } export enum PlcAddressType { symbolic = 0, absolute = 1 } /** * Baseline class to derive Input-Type Components for plcs from */ export abstract class PlcInput implements OnInit, OnDestroy { protected _proxy: IPlcEventProxy; protected _value: string = 'INAX: Connecting...'; protected _writeperm: PlcInputType = PlcInputType.readonly; protected _addressType: PlcAddressType = PlcAddressType.symbolic; protected _valueType: string = ''; protected _arraysize: number = 0; protected _abs: boolean; public subscribed: PlcItem; public guid: string = Guid.newGuid(); public get hasPlcConnection():boolean{ return this._inaxPlc.isHubConnected; } /** * Share metadata to use it in derived classes */ static metaData = { inputs: ["writepermInput","plcId","addressTypeInput","group","address","mapping","variable","class"], outputs: [] }; constructor(private _inaxPlc: InaxPlcService) { } /** * define (and initialize) shared @Input() variables */ @Input() writepermInput: string; @Input() plcId: string = ''; @Input() addressTypeInput: string; @Input() group: string = ''; @Input() address: string; @Input() mapping: string; @Input() variable: string; @Input() class: string = 'plc-input-table'; public abstract get Value(): string; public abstract set Value(v: string); /** * called when component is initialized * calls initializing functions and then subscribe to create a proxy */ abstract ngOnInit(): void; /** * when the component is terminated, the proxy is disposed */ abstract ngOnDestroy(): void; /** * initializes the properties that are shared by all plc-input components * - gets address from mapping and variable or vice-versa, depending on what has been defined * - sets a new GUID (Globally Unique IDentifier) as group if group was not defined to avoid interference * - sets enum types for write-permission and address-type from the @Input() string * - sets a boolean that to signal whether the address is absolute or symbolic (depending on addressType) */ public initializeDefaultProperties(): void { if (this.address !== undefined && (this.mapping === undefined || this.variable === undefined)) { if (this.address.split('.').length < 2) { throw Error('invalid address!'); } let separator = this.address.indexOf('.'); this.mapping = this.address.substring(0,separator);// mapping is address up to first "." this.variable = this.address.substring(separator+1); // rest of address after first "." is variable } else { this.address = this.mapping + '.' + this.variable; } if (this.group.length < 1) { this.group = Guid.newGuid(); // set unique id if none was specified } this._writeperm = (this.writepermInput === 'readwrite') ? PlcInputType.readwrite : PlcInputType.readonly; this._addressType = (this.addressTypeInput === "absolute") ? PlcAddressType.absolute : PlcAddressType.symbolic; this._abs = (this._addressType === PlcAddressType.absolute); } /** * Defined in each derived component to initialize its specific properties * Always call after initializeDefaultProperties to make sure, these properties can be accessed */ public abstract initialize(): void; /** * public function to set this._value */ public abstract updateValue(value: any): void; /** * subscribe to the given variable and call updateWatcher() */ public subscribe(): void{ if (this.variable !== undefined) { let item = new PlcItem(this.variable, this._value); this.subscribed = item; item.isSubscribed = true; this.updateWatcher(); } else { console.warn('invalid address: no variable given!'); } } /** * remove subscribed item (call OnDestroy to dispose proxy when component is terminated) */ public unsubscribe(): void { this.subscribed.isSubscribed = false; this.updateWatcher(); } /** * disposes any old proxy to avoid interference * if an item is subscribed to, creates an EventProxy for the group, id, mapping and variable * of its instance and logs the status. * if no item is subscribed to, only the old proxy is disposed but no new one is created */ public updateWatcher(): void { if(this._proxy !== undefined) { this._proxy.dispose(); } if (this.subscribed.isSubscribed) { try { this._proxy = this._inaxPlc.createEventProxy(this.group, this.plcId, this.mapping, [this.subscribed.name], this._abs); this._proxy.ChangeEvent.subscribe( (dce: DataChangeEvent) => this.dataUpdated(dce), (error: any) => { this.subscribed.isSubscribed = false; console.warn("Attempt to join channel failed!", error); } ); } catch(error) { console.warn("Attempt to join channel failed!", error); } } else { console.log('not subscribed to plc'); } } /** * is called on a ChangeEvent * @param ev ChangeEvent with properties Mapping and Data. * Mapping should be the same as this.mapping, Data should be an Object containing * a value accessible by this.variable * handles DataChangeEvent when a value was updated and tries to resolve the response */ private dataUpdated(ev: DataChangeEvent): void { let updated = this.subscribed; try { if (updated != null) { console.warn(`onEvent - ${ev.Mapping}.${updated.name}`); eval("updated.value = ev.Data." + updated.name); this.resolveData(ev.Data, this.variable); } } catch (error) { console.warn(`Error updating data! ${error}`); } } /** * @deprecated because a subscription to the proxy will return the data without having to manually call it * can be called to manually read a value from the plc (without needing a ChangeEvent) * not necessary or implemented as of now but may come in handy for development/testing */ public read(): void { try { this._inaxPlc.readSingle(this.plcId, this.mapping, this.subscribed.name).subscribe((data: any) => { try { if (data !== null && data[this.variable] !== null) { // console.log(JSON.stringify(data)); console.log(`successfully read data from ${this.mapping}`); this.resolveData(data, this.variable); } else { console.warn("no data read! (data = null)"); } } catch (error) { console.warn("no data read!"); } }); } catch (error) { console.error(error); } } /** * resolves an object containing data (e.g. .Data of a DataChangeEvent) * @param data Object that needs to contain a value for the variable (=> variable describes the location of a valid element in data) * @param variable the address to the desired value in data, usually this.variable * Depending on the type of value that is at the end of data[variable]: * - Type == String and describes a date (=> is a valid argument for new Date() ): valueType is set to 'date' * - Type one of [String, Number, Boolean]: the respective type will be set as valueType * - Type == Array: If the elements of the Array are Strings with a length <= 1, 'chararray' is set as valueType and the Array's length is stored * - else, a warning describing the type of error is logged and the value is set to 'ERROR' */ private resolveData(data: any, variable: string): void { // console.log('resolving data: ' + JSON.stringify(data)); //replace all left Array brackets with dots and remove right ones (to dig through the object recursively) let subvars = variable.replace(/\[/g,'.').replace(/\]/g,'').split('.'); // if variable consists of several property-names (i.e. subobject1.something.actual_variable) // then dissolve layers recursively if (subvars.length > 1) { for (let prop in data) { if (prop === subvars[0]) { let nextLayer = data[subvars[0]]; // remove outer layer of object subvars.shift(); // remove subvars[0] let nextVariable = subvars.join('.'); // create new Variable string out of remaining parts this.resolveData(nextLayer, nextVariable); return; // end function because recursive call will handle data } } // catch if prop was not found console.warn('Error: Invalid address at: ' + variable); return; } else { // otherwise resolve the value(s) stored in data (if data has property variable) for (let property in data) { if (property === variable) { data = data[variable]; // console.log(data); break; } } } if (Array.isArray(data)) { this._valueType = 'chararray'; this._arraysize = 0; for (let index in data) { // if data is an array but not an array of chars, throw exception if (typeof(data[index]) !== 'string') { console.warn('Error: Invalid Data (Array)! Please Select Bottom-Level Address (char-array or single value)'); this.updateValue('ERROR'); return; } } // join data to string and replace NUL chars (\u0000) to avoid incorrect string-sizes let value = data.join('').replace(/\0/g,''); this._arraysize = data.length; this.updateValue(value); } else { // if returned Data is not either a single variable or an array of chars, check if it is bottom-level object, else return if (typeof(data) === 'object') { // chararray-elements are objects when called separately, so check if this is the case. // if it is, resolve it with the same variable if (Object.getOwnPropertyNames(data).indexOf(variable) === -1) { // if valueType has not been set yet, address must have been invalid // (because change-event is always fired after subscribing) // otherwise, value has not changed and event is fired in response to different change if (this._valueType.length > 0) { return; } console.warn('Error: Invalid Data (Object)! Please Select Bottom-Level Address (char-array or single value)'); console.log(data); this.updateValue('ERROR'); return; } else { // value at address is single char of chararray which is returned as object with string of length 1 let val = data[variable]; if (typeof(val) !== 'string' || val.length > 1) { console.warn('Error: Invalid Data (Object)! Please Select Bottom-Level Address (char-array or single value)'); console.log(data); this.updateValue('ERROR'); return; } else { this._valueType = 'chararray'; this._arraysize = 1; this.updateValue(val); } } } else { if (new Date(data).getSeconds() !== NaN) { this._valueType = 'date'; } else { this._valueType = typeof(data); // console.log('Read data of type: ' + typeof(data)); } let value = String(data); this.updateValue(value); } } } /** * checks for writing permission, then formats & validates the value to be written and tries to write it to the plc * errors are catched and displayed in the console * @param value the value to be written, as a String type, usually coming from the user with the element * * @returns a boolean equal to whether the writing operation was successful or not */ public write(value: string): boolean { if (this._writeperm !== PlcInputType.readwrite){ console.warn('no writing permissions!'); return false; } else { let formattedValue = PlcValidator.formatAndValidate(value, this._valueType, this._arraysize); if (typeof(formattedValue) === 'function') { console.warn('Invalid Value: ' + value + ' is not of type ' + this._valueType); return false; } console.log('writing: ' + value + ' (type=' + typeof(value) + ')'); return this.internalWrite(formattedValue); } } /** * called by the public write() function * depending on the address type, performs a write-operation with an absolute or a symbolic address * @param value the value to be written in the correct type (as it is expected in the plc) * * @returns the result of the write-operation as a boolean or false if an error occurs */ private internalWrite(value: any): boolean { if(this._abs){ this._inaxPlc.writeAbs(this.plcId, this.mapping, this.variable, value) .subscribe(res => { this.logConfirmation('writing value \"' + value + '\"', res); return res; }, res => { return false; }); } else { this._inaxPlc.write(this.plcId, this.mapping, this.variable, value) .subscribe(res => { this.logConfirmation('writing value \"' + value + '\"', res); return res; }, res => { return false; }); } // default return value return false; } /** * used to easily log a confirmation to the console */ public logConfirmation(operation: string, data: boolean) { console.log(operation + (data ? ' successful!' : ' failed!')); } }