Prevent local variable retention - codesys

Suppose I have a function block POU1 which has local variables val1: INT and val2: INT, e.g.
FUNCTION_BLOCK POU1
VAR
val1: INT := 1;
val2: INT := 1;
END_VAR
Now suppose the user of the FB declares it as RETAIN, e.g.
VAR RETAIN
p1: POU1;
END_VAR
p1.val1 := 2;
p1.val2 := 2;
This will result in both val1 and val2 retaining the value of 2 in case of a warm reset, but what if I don't want that to happen to say val2 i.e. I want val1 to retain it's current value, but val2 to be reset in case of a warm reset (if the user declares my FB as RETAIN, otherwise I want both to reset)
How can I achieve this? (Also. same question goes for PERSISTENT)
PS. I tried {attribute 'init_on_onlchange'} and {attribute 'no_copy'} but they did nothing (maybe I used them wrong?). I also tried creating an additional FB with {attribute 'no_instance_in_retain'} and adding it as a local variable of POU1 but that resulted in a build error.

One way I just found is to implement FB_Exit explicitly and reset those variable in it:
METHOD FB_Exit: BOOL
VAR_INPUT
bInCopyCode: BOOL; // TRUE: the exit method is called in order to leave the instance which will be copied afterwards (online change).
END_VAR
val2 := 1; // reset all variables you don't want retained to their defaults
This seems to work, but not sure if this might have other consequences. e.g. Is FB_Exit called in case of a power failure?

The problem with FB_Exit or FB_Init and VAR PERSISTENT/RETAIN is that I couldn't find a consistent behaviour across platforms (Twincat/Codesys)
And yes there are cases where fb_exit is not called, for example in Twincat when you do a cold reset.
My approach on this would be different.
I would neither use attributes nor fb_exit or fb_init which under certain circumstances could be difficult to debug.
Instead I would use a simple global FB like this one:
FUNCTION_BLOCK FB_System
VAR
bInit : BOOL;
nCycleCount : UINT;
END_VAR
VAR CONSTANT
cINIT_AFTER_CYCLE : UINT := 2;
END_VAR
IF NOT bInit
THEN
nCycleCount := nCycleCount + 1;
END_IF
IF nCycleCount >= cINIT_AFTER_CYCLE
THEN
bInit := TRUE;
END_IF
METHOD isInit : BOOL
isInit := bInit;
Now, add an input to your retain/persistent FB:
VAR_INPUT
bSystemInit : BOOL;
END_VAR
And call it like this:
fbRetain(bSystemInit := fbSystem.isInit());
Initialize your values if the system is not initialized.
Add this check in your FB implementation:
IF NOT bSystemInit THEN
anIntVar := 0;
//or call a reset() method where you put all your variables that need to be initialized
END_IF
If you have many FBs that need this kind of initialization, you can extend them with an FB that has this example code in it.
By doing so you can reuse your code efficiently.
Having said that, I must warn you that I had many problems with persistent data in the past.
It happened to me repeatedly that persistent data became corrupt causing production problems or even preventing the runtime to start.
If I had to design a system from scratch I would use the XML-server from Beckhoff or the XML-utility from codesys to store relevant production data in an xml-file and retrieve this data at runtime start.

Related

Casting function block

I'm using Wago PFC200 for my home automation. I got base function block:
FUNCTION_BLOCK ABSTRACT Room
and two the interface:
INTERFACE IBlinds
- BlindsUp
- BlindsDown
and
ILights
-TurnOffLights
-TurnOnLights
My room's instances looks like this:
FUNCTION_BLOCK Garage EXTENDS Room IMPLEMENTS ILights, IBlinds
In my PLC_PRG I've all instances of my rooms:
PROGRAM PLC_PRG
VAR
Bedroom: Bedroom;
Garage: Garage;
Hall: Hall;
Boilerroom: Boilerroom;
...
END_VAR
Under the PLC_PRG I've some methods to e.g.: automate blids:
METHOD MoveBlindsToMorningPosition
VAR CONSTANT
siCount: SINT := 5;
END_VAR
VAR_INPUT
xMoveSignal: BOOL;
END_VAR
VAR
_siIndex: SINT;
_rooms: ARRAY[0..siCount] OF POINTER TO IBlinds := [ADR(Livingroom), ADR(Diningroom), ADR(Kitchen), ADR(Toilet), ADR(Boilerroom), ADR(Garage)];
END_VAR
FOR _siIndex := 0 TO siCount DO
_rooms[_siIndex]^.MoveBlindsToMorningPosition(xMove := xMoveSignal);
END_FOR
But I got the following compilation errors in the _rooms array: C0032: Cannot convert type 'POINTER TO Garage' to type 'POINTER TO IBlinds'
My function blocks actually implement IBlinds. Is there a way to cast function block?
First of all, an interface is already a reference to a function block:
CODESYS always treats variables declared with the type of an interface as references.
So there shouldn't be a need to use pointers.
Secondly, to cast an function block into an interface, personally I'd recommend using a dedicated method inside a function block. For example:
INTERFACE inter1
- ...
- ToInter1
INTERFACE inter2
- ...
- ToInter2
and implement them inside MyObject like:
ToInter1 := THIS^;
ToInter2 := THIS^;
And then you can:
myObj: MyObject;
i1: inter1 := myObj.ToInter1();
i2: inter2 := myObj.ToInter2();
Or
arr: ARRAY[x..y] OF inter1;
arr[z] := myObj.ToInter1();
At least this is what I do to solve this

No matching FB_Init method found when it's in a block

Running Ecostruxure machine expert which is CodeSys 3.5
I've got the following program structure:
Main: two blocks, Init and Step0 linked by a transition.
In the vars of that main:
VAR
My_Encoder : ENC_REF_M262;
...
END_VAR
So then in my Init block I have My_Encoder.FB_Init(...)
But the error message I get points to the variable declaration in Main which says "C0138 No Matching FB_Init method found fo this instantiation of ENC_REF_M262".
Try using the no_init attribute above your instantiated FB.
VAR
{ attribute 'no_init'}
My_Encoder : ENC_REF_M262;
...
END_VAR
Whenever a function block is instantiated, a matching implementation of the FB_Init method is expected to occur (even inside a wrapping fb).
N.B. You will need to explicitly run the init code (My_Encoder.fb_Init()) somewhere if it has critical functionality
Using the 'no_init' attribute will fix the compile error; however, (IMHO) this may not be the best solution. First, because you'll probably want to initialize the values and secondly because calling FB_Init explicitly, is strongly discouraged. Therefore I would like to explain why the error message occurs as well as giving some insight in how to solve the error.
The error is (AFAIK) due to either one of the two reason listed below.
When you declare and initialize a POU (e.g. an FB between the VAR and END_VAR keywords) and you directly initialize a value to one of it's inputs while this input is not included as VAR_INPUT of the FB_Init of the POU.
I'll clarify this with some code:
VAR
myFunctionBlock: FB_FunctionBlock(myInput:= 1);
END_VAR
.. Above we're initializing input "myInput" of object/instance "myFunctionBlock" with the value 1 and this requires that "myInput" is included in the FB_Init of POU "FB_FunctionBlock":
METHOD FB_init : BOOL
VAR_INPUT
bInitRetains : BOOL;
bInCopyCode : BOOL;
myInput : INT; // Include the input or else you'll get the error
END_VAR
In the VAR_INPUT-block of an FB_Init you can include inputs of the POU to which the FB_Init belongs. When you do this your are required to initialize these inputs when you declare and initialize the POU. (this is probably the case in your example)
Again I'll clarify this with some code:
METHOD FB_init : BOOL
VAR_INPUT
bInitRetains : BOOL;
bInCopyCode : BOOL;
myInput : INT;
END_VAR
.. Above input "myInput" is included in the FB_Init of POU "FB_FunctionBlock" and therefore when we create an object/instance of POU "FB_FunctionBlock" then we're required to initialize the input "myInput".
VAR
myFunctionBlock: FB_FunctionBlock(myInput:= 1);
// Include "myInput:= 1" or else you'll get the error
END_VAR

What are the semantics of input variables passed by reference?

Beckhoff's TwinCat-3, as well as Codesys 3 it's based on, adds references as an extension to IEC-61131-3 languages. I'm wondering what is the exact grammar and semantics of this non-standard addition (that's the problem with them: nobody bothers documenting them as well as a standard would).
It the following F_IsNonEmpty function valid and doing what one would expect, when invoked from the F_Test test function below?
FUNCTION F_IsNonEmpty : BOOL
VAR_INPUT
text : REFERENCE TO STRING;
END_VAR
F_IsNonEmpty := LEN(text) > 0;
END_FUNCTION
FUNCTION F_Test1
VAR
testMessage : STRING := '123';
END_VAR
IF F_IsNonEmpty(text := testMessage) THEN
{...}
END_IF
END_FUNCTION
Given that the target of the reference must be initialized using the REF= operator (v.s. e.g. C++ where reference targets are immutable), I'd have expected that the following invoking code would be correct instead - but it doesn't even compile:
FUNCTION F_Test2
VAR
testMessage : STRING := '123';
END_VAR
IF F_IsNonEmpty(text REF= testMessage) THEN
{...}
END_IF
END_FUNCTION
It seems that F_Test1 works correctly, but I'd like someone who actually uses Codesys 3 or TwinCat-3 REFERENCE TO feature to confirm.
When you use a REFERENCE in a VAR_INPUT, it's as if you were using a VAR_IN_OUT variable.
Otherwise if you declare your REFERENCE in the VAR section, you need to use REF= when assigning another variable to it (or get an exception).
In essence, REFERENCE (like a VAR_IN_OUT var) is a more convenient and "safe" pointer because the dereference operator ^ is not needed and because the type is checked at compile time.

Delphi: How to allow setting a TClass-property of a TCollectionItem at design time

I'm developing a component that works on several classes.
In order to allow adding the list of managed classes, I've written a TCollection's inherited class in which each item (inherited from TCollectionItem) defines a published "TargetClassName" property.
The "TargetClassName" property's setter function, calls the following function in order to find the corrisponding TClass:
function FindAnyClass(const Name: string): TClass;
var
ctx: TRttiContext;
typ: TRttiType;
list: TArray<TRttiType>;
begin
Result := nil;
ctx := TRttiContext.Create;
list := ctx.GetTypes;
for typ in list do
begin
if typ.IsInstance and (EndsText(Name, typ.Name)) then
begin
Result := typ.AsInstance.MetaClassType;
break;
end;
end;
ctx.Free;
end;
(Thanks to Dalija Prasnikar for writing the function Get class by its name in Delphi).
Now, I'm wondering if there's a better way to allow adding classes to a TCollectionItem at design time.. What do you think about it?
Hope to read interesting solutions!
Thanks to all.
in creation on TCollection You need To introduce Collation Class
it's Posible in two way
1 : hard coded in create time X := TMycollation.Create(TMyCollationClass)
2 : your solution X := TMycollation.Create(FindAnyClass('TMyCollationClass'));

Error When Editing Member Variable in Class

I'm trying to use a class in my program.
TStack = Class
Public
constructor Create; Overload;
Procedure Add(Frm:TForm);
Procedure Remove();
Procedure Do_Close;
Private
List : Array[1..Max_Forms] of Rec;
Count : Byte;
End;
Constructor:
constructor TStack.Create;
begin
Self.Count := 0;
end;
Procedure TStack.Add(Frm:TForm);
begin
Inc(Self.Count);
List[Count].Form := #Frm;
List[Count].Width := Frm.Width;
List[Count].Height := Frm.Height;
List[Count].left := Frm.Left;
List[Count].Top := Frm.Top;
end;
I can't change value of Count variable! It cause Run-Time error : Access violation....Write of address 000001E4
What's the problem?!
FOR MORE INFORMATION:
I'm trying to store a pointer to each form in a structure like this :
Rec = Record
Form : ^TForm;
Maximized : Boolean;
Width,
Height,
left,
Top : Integer;
End;
And then
Procedure TStack.Do_Close;
var
i : integer;
MyForm : TForm;
begin
i := .....some code here.......;
MyForm := #List[i].Form;
ShowMessage('I will close '+MyForm.Caption);
MyForm.Close;
end;
AND call constructor like this to initialize 'Count':
Stack.Create;
As described in comments you are attempting to create the object like this:
var
Stack: TStack;
....
Stack.Create;
This is a classic mistake, and one that we've all made. You are calling a method on an uninitialized instance variable.
In order to instantiate a class you need to write this:
Stack := TStack.Create;
On top of that I have the following comments:
Use zero-based indexing for arrays. That's the convention everywhere in Delphi apart from anachronistic strings. And even that is changing in newer versions.
Don't use static arrays for a stack unless you have a good reason for doing so. You'll just run the risk of running out of space. Or allocating more memory than you need. Use a dynamic array.
Rather than a dynamic array, you could use TList<T>.
Even so, one wonders why you are making your own stack class when there is the perfectly good TStack<T>.
You store the address of a local variable in your stack. In TStack.Add you add #Frm into the container. As soon as TStack.Add returns, #Frm is meaningless. That's because Frm is a local variable whose life ends when the function that owns it returns. I think you want to take a copy of Frm.
Picking up item 5 in more detail, your record is declared like this:
Rec = Record
Form : ^TForm;
Maximized : Boolean;
Width,
Height,
left,
Top : Integer;
End;
It is a mistake to use ^TForm. That is a pointer to a variable holding a pointer to an object. That's two levels of indirection, one too many. You must declare the Form field to be of type TForm. I suggest you revise the way Delphi object reference variables work. Delphi classes are what is known as reference types. A variable of type TMyClass where TMyClass is class(...) is already a pointer. The language automatically de-references the pointer when you use the . operator to access members.