Making ios readable

This commit is contained in:
Martin Karkowski 2020-12-31 13:18:24 +01:00
parent cc6d72e433
commit b45e34bd4b
5 changed files with 375 additions and 98 deletions

View File

@ -0,0 +1,237 @@
/**
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-12-31 12:47:20
* @modify date 2020-12-31 13:14:52
* @desc [description]
*/
import { injectable } from "inversify";
import { interval, Observable } from "rxjs";
import { debounce } from "rxjs/operators";
import {
exportMethod,
exportProperty
} from "../../../lib/decorators/moduleDecorators";
import { dynamicSort } from "../../../lib/helpers/arrayMethods";
import { getNopeLogger } from "../../../lib/logger/getLogger";
import { InjectableNopeBaseModule } from "../../../lib/module/BaseModule.injectable";
import { NopeObservable } from "../../../lib/observables/nopeObservable";
import { INopeObservable } from "../../../lib/types/nope/nopeObservable.interface";
import {
IInternalIoDetails,
IModuleConfig,
IPLCModule,
IReadableIos
} from "../../generic-plc/type/interfaces";
const DEFAULT_DEBOUNCE_TIME = 500;
@injectable()
export class BasePlcModule
extends InjectableNopeBaseModule
implements IPLCModule {
/**
* Flag to disable writing updates to the plc
*/
@exportProperty({
mode: "subscribe",
topic: "writeUpdates",
schema: {
type: "boolean"
}
})
public writeUpdates = new NopeObservable<boolean>();
/**
* Flag to disable listening to updates from the plc
*
* @memberof IPLCModule
*/
@exportProperty({
mode: "subscribe",
topic: "listenForUpdates",
schema: {
type: "boolean"
}
})
public listenForUpdates = new NopeObservable<boolean>();
/**
* Containing all contained IO-Definitions of the Module
*
* @memberof IPLCModule
*/
@exportProperty({
mode: "publish",
topic: "readableIos",
schema: {
type: "array",
items: {
type: "object",
properties: {
name: {
type: "string"
}
}
}
}
})
public readableIos = new NopeObservable<IReadableIos[]>();
protected _readableIos: Observable<IReadableIos[]>;
@exportProperty({
mode: "publish",
topic: "config",
schema: {}
})
public config = new NopeObservable<IModuleConfig>();
public ioPathes = new Set<string>();
public ioDefinitions = new Map<string, IInternalIoDetails>();
/**
* Element containing all configured Elements
*
* @type {({
* [index: string]: INopeObservable<boolean | string | number>;
* })}
* @deprecated
* @memberof BasePlcModule
*/
public ios: {
[index: string]: INopeObservable<boolean | string | number>;
} = {};
/**
* Helper Function to reset all Ports of the PLC.
*/
@exportMethod({
paramsHasNoCallback: true
})
public async reset(): Promise<void> {
/** Dispose the old Config */
for (const element of this.ioDefinitions.values()) {
this._logger.debug(
"resetting " + element + " to " + element.defaultValue
);
element.observable.setContent(element.defaultValue);
}
}
public name: string;
/**
* Dispose Function, which will shutdown everything correctly.
* All Subscription will be unsubscribed, the Client will be closed
* etc...
*/
public async dispose(): Promise<void> {
await super.dispose();
}
/**
* Forces the Plc to update the IOs
*/
@exportMethod({
paramsHasNoCallback: true
})
public async forceState(): Promise<void> {
/** Dispose the old Config */
for (const element of this.ioDefinitions.values()) {
element.observable.forcePublish();
}
}
/**
* The Logger of the Module.
*
* @private
* @memberof BeckhoffPlc
*/
protected _logger = getNopeLogger("base-plc-module");
/**
* Flag showing, if the Element has been connected or not.
*
* @memberof BeckhoffPlc
*/
@exportProperty({
mode: "publish",
topic: "connected",
schema: {}
})
public connected = new NopeObservable<boolean>();
/**
* flag showing, that the element has been initalized.
*
* @memberof BeckhoffPlc
*/
@exportProperty({
mode: "publish",
topic: "initialized",
schema: {}
})
public initialized = new NopeObservable<boolean>();
public async init(options: {
UiUpdateDelay?: number;
waitForInitialized?: boolean;
}): Promise<void> {
// Initialize the default values:
this.initialized.setContent(false);
this.listenForUpdates.setContent(true);
this.writeUpdates.setContent(true);
// Create a debounced publishing of readable Ios.
// 1. Create an observable.
const _this = this;
this._readableIos = new Observable((subscriber) => {
_this._updateReadableIos = () => {
subscriber.next(
Array.from(this.ioDefinitions.values())
.map((io) => {
// The IO has to be transformed
return {
currentValue: io.observable.getContent(),
dataType: io.dataType,
defaultValue: io.defaultValue,
orginalName: io.orginalName,
path: io.path,
type: io.type
};
})
.sort(dynamicSort("orginalName"))
);
};
});
// 2. Subscribe to the RXJS observale and
// debounce it. Use the debounced observable
// to update the Nope-Observable.
this._readableIos
.pipe(
debounce(() => interval(options.UiUpdateDelay || DEFAULT_DEBOUNCE_TIME))
)
.subscribe((value) => {
_this.readableIos.setContent(value);
});
// Call init function of superior module.
await super.init();
// If required wait for the initialized module.
if (options.waitForInitialized) {
// Wait for the System to be ready.
await this.initialized.waitFor((value) => value === true);
}
}
/**
* Internal Funcion to update the Readable IOs.
*
* @protected
* @memberof BeckhoffPlc
*/
protected _updateReadableIos: () => void;
}

View File

@ -2,7 +2,7 @@
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-03-03 17:28:33
* @modify date 2020-12-30 15:07:21
* @modify date 2020-12-31 12:41:02
* @desc [description]
*/
@ -66,6 +66,23 @@ export type IPLCConfig = {
};
};
export interface IIoDetails {
orginalName: string;
path: string;
dataType: "boolean" | "number" | "string";
type: "input" | "output";
defaultValue: number | string | boolean;
}
export interface IReadableIos extends IIoDetails {
currentValue: number | string | boolean;
}
export interface IInternalIoDetails extends IIoDetails {
module: IPLCModule;
observable: INopeObservable<number | string | boolean>;
}
export interface IPLCModule {
/**
* Flag indicating, whether the Element has been initialized or not
@ -89,16 +106,7 @@ export interface IPLCModule {
*
* @memberof IPLCModule
*/
containedIos: INopeObservable<
{
orginalName: string;
// module: IPLCModule;
path: string;
dataType: "boolean" | "number" | "string";
type: "input" | "output";
defaultValue: number | string | boolean;
}[]
>;
readableIos: INopeObservable<IReadableIos[]>;
/**
* Function to Reset the Values of a Buffer
@ -137,7 +145,7 @@ export interface IInternalPLCModule extends IPLCModule {
/**
* Element containing all configured Elements
*
* @deprecated
*/
ios: { [index: string]: INopeObservable<boolean | number | string> };
@ -150,38 +158,23 @@ export interface IInternalPLCModule extends IPLCModule {
* Element, for reading all IO-Pathes
*/
readonly ioPathes: Set<string>;
readonly ioDefinitions: Map<
string,
{
orginalName: string;
module: IPLCModule;
path: string;
dataType: "boolean" | "number" | "string";
type: "input" | "output";
defaultValue: number | string | boolean;
}
>;
readonly ioDefinitions: Map<string, IInternalIoDetails>;
readonly bufferPath: string;
/**
* Function to Reset the Values of a Buffer
*
* Disables the online Mode again. Changes wont be written
* to the desired module. Acts like the system is offline.
*/
reset(): Promise<void>;
/**
* Function to Dispose the Buffer
*/
dispose(): Promise<void>;
disbaleSendingUpdates(): void;
enableSendingUpdates(): void;
/**
* Helper Function to Force sending an Update to the PLC
* Enable the online Mode again. Changes will be written
* if they occour
*
* @memberof IInternalPLCModule
*/
forceState(): void;
enableSendingUpdates(): void;
}
export interface IRawPLCModule extends IPLCModule {

View File

@ -2,23 +2,30 @@
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-03-02 11:37:50
* @modify date 2020-12-30 15:23:18
* @modify date 2020-12-31 12:41:05
* @desc [description]
*/
import { injectable } from "inversify";
import * as Logger from "js-logger";
import * as ads from "node-ads";
import { Observable } from "rxjs";
import {
exportMethod,
exportProperty
} from "../../../lib/decorators/moduleDecorators";
import { deepClone, SPLITCHAR } from "../../../lib/helpers/objectMethods";
import { dynamicSort } from "../../../lib/helpers/arrayMethods";
import { getNopeLogger } from "../../../lib/logger/getLogger";
import { InjectableNopeBaseModule } from "../../../lib/module/BaseModule.injectable";
import { NopeObservable } from "../../../lib/observables/nopeObservable";
import { INopeObservable } from "../../../lib/types/nope/nopeObservable.interface";
import { IModuleConfig, IPLCModule } from "../../generic-plc/type/interfaces";
import {
IInternalIoDetails,
IModuleConfig,
IPLCModule,
IReadableIos
} from "../../generic-plc/type/interfaces";
import { generateAdsOptions } from "../helpers/gen.options";
import { IAdsOptions, IBeckhoffSymbol } from "../type/interfaces";
import {
@ -58,6 +65,14 @@ export class BeckhoffPlc
})
public listenForUpdates = new NopeObservable<boolean>();
protected _changes = new Observable<
Array<{
name: string;
path: string;
value: number | boolean | string;
}>
>();
/**
* Containing all contained IO-Definitions of the Module
*
@ -65,7 +80,7 @@ export class BeckhoffPlc
*/
@exportProperty({
mode: "publish",
topic: "containedIos",
topic: "readableIos",
schema: {
type: "array",
items: {
@ -78,15 +93,7 @@ export class BeckhoffPlc
}
}
})
public containedIos = new NopeObservable<
{
orginalName: string;
path: string;
dataType: "boolean" | "number" | "string";
type: "input" | "output";
defaultValue: number | string | boolean;
}[]
>();
public readableIos = new NopeObservable<IReadableIos[]>();
@exportProperty({
mode: "publish",
@ -96,22 +103,13 @@ export class BeckhoffPlc
public config = new NopeObservable<IModuleConfig>();
public ioPathes = new Set<string>();
public ioDefinitions = new Map<
string,
{
orginalName: string;
module: IPLCModule;
path: string;
dataType: "number" | "boolean" | "string";
type: "input" | "output";
defaultValue: number | string | boolean;
}
>();
public ioDefinitions = new Map<string, IInternalIoDetails>();
/**
* Element containing all configured Elements
*
* @type {{[index:string]= new NopeObservable<any>}}
* @deprecated
* @memberof SmartBufferBase
*/
public ios: {
@ -378,7 +376,13 @@ export class BeckhoffPlc
this.state.subscribe((state) => {
if (_this.connected.getContent() && state === "RUN") {
if (_this.listenForUpdates.getContent()) {
_this._getConfig(_consider, _getIoType, _getDefaultValue, 10);
_this._getConfig(
_consider,
_getIoType,
_getDefaultValue,
options.preventPublishingOutputs,
10
);
}
// Subscribe to changes of the Tables and of the state
@ -388,7 +392,7 @@ export class BeckhoffPlc
const name = handle.symname;
// An Input / Output has been updated
if (_this.ios[name]) {
if (_this.ioDefinitions.has(name)) {
if (this._logger.enabledFor(Logger.DEBUG)) {
_this._logger.debug(
"updating io " +
@ -398,14 +402,26 @@ export class BeckhoffPlc
"\""
);
}
_this.ios[name].setContent(handle.value, _this.identifier);
// Forward the content to the element.
_this.ioDefinitions
.get(name)
.observable.setContent(handle.value, _this.identifier);
// Update the Readable Ios:
_this._updateReadableIos();
}
} else {
// Only if the Symbol Table has been updated
// request the new Symbol-Table.
if (!start && _this.listenForUpdates.getContent()) {
// A New Configuration has been used => Update the Variables.
_this._getConfig(_consider, _getIoType, _getDefaultValue, 10);
_this._getConfig(
_consider,
_getIoType,
_getDefaultValue,
options.preventPublishingOutputs,
10
);
}
start = false;
}
@ -444,10 +460,16 @@ export class BeckhoffPlc
getDefaultValue: (
symbol: IBeckhoffSymbol
) => Promise<number | string | boolean>,
preventPublishingOutputs: boolean,
maxRetries: number
): void {
const _this = this;
this.getConfiguration(consider, getIoType, getDefaultValue).catch((err) => {
this.getConfiguration(
consider,
getIoType,
getDefaultValue,
preventPublishingOutputs
).catch((err) => {
if (maxRetries > 0) {
this._logger.warn(
"Failed getting the Configuration. Retry left " +
@ -476,7 +498,8 @@ export class BeckhoffPlc
getIoType: (symbol: IBeckhoffSymbol) => Promise<"input" | "output">,
getDefaultValue: (
symbol: IBeckhoffSymbol
) => Promise<number | string | boolean>
) => Promise<number | string | boolean>,
preventPublishingOutputs: boolean
): Promise<
{
name: string;
@ -538,6 +561,12 @@ export class BeckhoffPlc
// Subscribe only Inputs
_this._logger.debug("Defined", handle.symname, "as input");
this._client.notify(handle);
} else if (!preventPublishingOutputs) {
// Subscribe to the output as well.
// This enables multi-value acessing,
// as well as observing the ports
// during the runtime.
this._client.notify(handle);
}
// Extract the Name, that will be used
@ -552,6 +581,7 @@ export class BeckhoffPlc
_this.ioDefinitions.set(name, {
orginalName: name,
observable: obs,
module: _this,
dataType,
path,
@ -561,7 +591,17 @@ export class BeckhoffPlc
// Now mark this Observable as dynamic Observable
_this.registerProperty(path, obs, {
mode: type === "output" ? "subscribe" : "publish",
mode:
// Based on the Flag, whether outputs should be
// subscribed (and then published), define the
// mode. If the Port is an input => "publish"
// only, otherwise ["publish", "subscribe"]
// or "subscribe" based on the prefered settings
type === "output"
? !preventPublishingOutputs
? ["publish", "subscribe"]
: "subscribe"
: "publish",
topic: path,
isDynamic: true,
// Define the schema based on the Provided Definition
@ -572,7 +612,7 @@ export class BeckhoffPlc
});
// Assign the specified Default Value:
_this.ios[name].setContent(defaultValue);
obs.setContent(defaultValue);
// Log the Assign ment in Debugging mode:
if (this._logger.enabledFor(Logger.DEBUG)) {
@ -641,18 +681,8 @@ export class BeckhoffPlc
}
}
// Assign the contained IOS.
_this.containedIos.setContent(
Array.from(_this.ioDefinitions.values()).map((item) => {
return {
dataType: item.dataType,
defaultValue: item.defaultValue,
orginalName: item.orginalName,
path: item.path,
type: item.type
};
})
);
// Update the Readable Ios
this._updateReadableIos();
_this._logger.info("Received Symbols. Should be ready.");
@ -672,4 +702,28 @@ export class BeckhoffPlc
}
});
}
/**
* Internal Funcion to update the Readable IOs.
*
* @protected
* @memberof BeckhoffPlc
*/
protected _updateReadableIos(): void {
this.readableIos.setContent(
Array.from(this.ioDefinitions.values())
.map((io) => {
// The IO has to be transformed
return {
currentValue: io.observable.getContent(),
dataType: io.dataType,
defaultValue: io.defaultValue,
orginalName: io.orginalName,
path: io.path,
type: io.type
};
})
.sort(dynamicSort("orginalName"))
);
}
}

View File

@ -2,7 +2,7 @@
* @author Martin Karkowski
* @email m.karkowski@zema.de
* @create date 2020-09-08 11:18:20
* @modify date 2020-12-03 13:45:18
* @modify date 2020-12-31 12:02:52
* @desc [description]
*/
@ -35,11 +35,9 @@ export interface IBeckhoffPLC extends IPLCModule {
}
export interface IAdsOptions extends IBeckhoffOptions {
path: string;
preventPublishingOutputs?: boolean; // Flag to prevent publishing (and internally subscribing the outputs as well)
maxDelay?: number; // -> Latest time (in ms) after which the event has finished
cycleTime?: number; // -> Time (in ms) after which the PLC server checks whether the variable has changed,
inputName?: string; // -> Value to Filter Inputs.
outputName?: string; // -> Value to Filter Outputs.
}
export interface IBeckhoffPLCManager {

View File

@ -20,6 +20,7 @@ import {
import { INopeDispatcher } from "../../lib/types/nope/nopeDispatcher.interface";
import { INopeModule } from "../../lib/types/nope/nopeModule.interface";
import { INopeObserver } from "../../lib/types/nope/nopeObservable.interface";
import { IReadableIos } from "../../modules/generic-plc/type/interfaces";
import { IBeckhoffPLC } from "../../modules/mod-Beckhoff-PLC-Interface/type/interfaces";
import Toolbar from "../../resources/ui/layout/toolbar";
@ -29,16 +30,8 @@ class BeckhoffComponent extends React.Component<
intialized: boolean;
connected: boolean;
remote;
inputs: {
orginalName: string;
dataType: string;
value: string;
}[];
outputs: {
name: string;
dataType: string;
value: string;
}[];
inputs: IReadableIos[];
outputs: IReadableIos[];
}
> {
constructor(props) {
@ -55,19 +48,20 @@ class BeckhoffComponent extends React.Component<
observers: INopeObserver[] = [];
componentDidMount() {
console.log(this.props.instance);
if (this.props.instance) {
const _this = this;
console.log(_this.props.instance.adsOptions);
// Subscribe to the Oberservers
// which will influence the visuals.
this.observers.push(
this.props.instance.connected.subscribe((connected) => {
console.log(_this.props.instance);
_this.setState({ connected });
})
);
this.observers.push(
// Wait for the System to Initialize.
// If done, define the Remote-String.
this.props.instance.initialized.subscribe((intialized) => {
console.log(_this.props.instance);
_this.setState({
intialized,
remote:
@ -77,6 +71,7 @@ class BeckhoffComponent extends React.Component<
});
})
);
// Initially define the Remote.
this.setState({
remote:
this.props.instance.adsOptions.getContent()?.host +
@ -115,7 +110,7 @@ class BeckhoffComponent extends React.Component<
<tr key={idx}>
<td>{io.orginalName}</td>
<td>{io.dataType}</td>
<td>{io.value}</td>
<td>{io.currentValue}</td>
</tr>
))}
</tbody>