In a general sense, programming deals with ways how to specify
algorithms, which will be executed by a computer. Programming has made
great progress since the origination of computer science, when it was
necessary to put individual instructions into the computer memory,
which were than interpreted by the machine. Although present-day
computers are not, in principle, able to do more than just execute
sequences of simple instructions stored in the memory, the ways of
creating these sequences — programs — have dramatically changed.
If you drag the meter virtual instrument from the
instrument palette, drop it to the application workspace and modify
its properties to measure and display each second some value, you
create a program. From the processor instructions point of view, it is
a very complex program, including hundreds of thousands of
instructions. The reason is simple — there is
not an instruction for displaying of a number or for drawing of the
meter's pointer. Neither are there instructions for drawing a line
(yes, perfectionists can object that specialized graphical processors
can draw a line with a single instruction, but it does not make any
difference to the principle of our explanation) nor for evaluation of
sine function necessary for calculation of co-ordinates of the line
end from a known angle. Processor instructions can do very little, for
instance, they can read the memory area into the processor's internal
register, add two registers or evaluate a condition and jump to
another part of the program according to the logical value of the
result.
Fortunately the development of programming brought an important
invention — high-level programming language. It
is a way of writing algorithms on a higher level than machine
instructions, by means of symbolic representations. If you want to
program the addition of two numbers in a high-level programming
language, it is not necessary to know the addresses at which these
numbers are stored in the memory, nor is it necessary to assign
processor registers to them, simply it is enough to write a + b.
How is it possible when the processor understands only its simple
instructions? Another program has to be involved in the
process — compiler. The compiler converts the
high-level language notation, which is usually intelligible to man,
into the form of machine instructions intelligible to the
computer.
Programs can therefore be made by means of several tools,
different in the level of abstraction and independence of a specific
platform.
The machine code represents direct instructions for the
processor of the given computer. Its use is very demanding and
inefficient, and the algorithm written by means of machine
instructions works only on a specific type of processor and
sometimes also on a specific type of computer. These are the main
reasons why the machine code (even if it is written using a symbolic
assembly-language) is used only exceptionally, mainly when
implementing core parts of operating systems. On the other hand, the
machine code is the most general means of expression and it can be
stated that if something cannot be programmed by means of machine
instructions it cannot be programmed at all.
The programming language is defined by a set of syntactic
rules, which must be followed when writing programs (language
grammar) and the source text is then more or less independent of the
environment in which the program is running. Compilation of the
source text into processor instructions is provided by another
program — compiler. There are many programming
languages and they differ in the level of abstraction — from hard to use languages close to the machine code
(e.g. C language) up to easily used modular languages (e.g. Pascal,
Modula-2, Oberon).
As a separate group we mention programming languages which,
besides having the ability to write any algorithms, are also able to
communicate with other components and can work as a glue
connecting individual components. This property is very important
for fast development of applications, because it resolves in the
most effective manner the issue of programming — how not to write the same piece of program twice.
Traditional languages resolve this problem by introduction of
libraries — blocks of the code, which can be
added to the program (e.g. calculation of sine function is a typical
example of a library code block, which is added to the program if
required). However, libraries have many restrictions and only
components really solve the problem of reusability. Typical examples
of such languages are Visual Basic or OCL (Object Control Language)
of the Control Web. Although components can also be
typically used from other languages, it usually has an adverse
impact on ease and directness of work.
Rapid application development tools represent the highest
level of abstraction from the principles of processor operation.
They usually make it possible to compose an application of
prepared, parameterized components. Control Web is
an example of such a system.
The motivation for implementing OCL into the Control Web is clear: while most applications work as a set of
prepared components, sometimes a tool is needed which reaches behind
the functionality of present components. OCL can use all instruments
(and thus it is not necessary to program the drawing of a measuring
instrument indicator) and at the same time it can realize any
algorithm and add the required functionality to the application.
Procedure or Method?
Despite the fact that code blocks (algorithm notations) in the
Control Web are called procedures, they are a
little different from what is called procedural programming
described in many classic textbooks. A considerable difference
consists especially in the absence of a main program,
which is started first and possibly calls subroutines. Why is it
so? The Control Web does not work in a
traditional batch manner. Control Web is an
event-driven system. There is no main program; instead
the system reacts to events and calls code blocks according to
given events. But what should we imagine under the term
event? For common user applications an event is, for
example, pressing the key or movement by mouse. However, the
Control Web is a real-time industrial system and
the set of events is expanded by events generated by the
input/output device drivers, virtual instruments or the timer.
Thus, the corresponding procedure is called when the given event
occurs.
An event never occurs by itself, without any relation
to an instrument. Every key pressing, mouse movement and also
every activation after a time interval or every exception caused
by the driver relates to a specific virtual instrument. For this
reason, procedures are always connected with a specific virtual
instrument too. Such a model is very close to the object-oriented
model, within which code blocks realizing object behavior are
called object methods. Therefore, procedures could perhaps rather
be called instrument methods. However, object-oriented
technologies cause complications which should be avoided in tools
such as the Control Web, and thus we stick to
procedures. One matter is crucial — OCL
procedures are always linked with some virtual instrument, they
never exist independently..
Procedure interface — parameters and
return value
As was said, procedures execute some code — algorithm notation. The algorithm processes data and
it would not be of any use unless it read data from outside and
returned the results. There can exist two manners of such data
exchange which of course can be combined:
The procedure may access data elements, which are defined
in application data sections (variables, channels, etc.) and
read or write their values. Such data elements are called
global. Global data elements are accessible from any
part of the application, from any virtual instrument and from
any procedure.
Another manner is definition of parameters of the
procedure and possibly their return values. Parameters and
return values represent a private interface and it is more
advantageous to use them. We will immediately explain
why.
There are several problems with access to global data. First of
all it would be necessary to define a global variable for each
data element to be handled by procedure code, and thus the number
of variables grows. Also, orientation in a big number of global
variables is considerably more difficult. There is also a risk of
the problem of synchronization — one
procedure will use a global variable and during a break in
execution of the code (caused by the instructions yield,
pause or wait — these instructions are described later) another
procedure overwrites its value.
For these reasons, each procedure may have the list of
parameters — variables and their types, for
instance:
procedure Calculate( Value : real; Flag : boolean );
Unlike the definition of global variables, variables
representing parameters of the procedure do not have an
initializing value — their value is always
defined by the calling code. If any procedure wants the procedure
Calculate to calculate something, it must provide two
values (or expression, which will be evaluated upon call), one is
the number of the type real, the other is the logical
value of the type boolean.
Therefore, what are the rules for access to variables
which appear as procedure parameters?
The parameter can have the same name as the already
defined global data element (regardless of the type). Then, it
is necessary to take into consideration that the use of such an
identifier always represents the parameter and the global
parameter is hidden, it is not possible to use it. If there
is, e.g., a global array of channels of the type real
with the name c1 and the procedure has a defined
parameter c1 of the type boolean, then
in the body of the procedure it is possible to use c1
only as a simple boolean variable.
If the parameters of the procedure are defined, these
parameters are visible only within the procedure. They
cannot be used outside the procedure.
No value can be stored in parameters which is to
survive between procedure calls. The value of the
parameters is always assigned when calling them.
When writing more parameters of the same type you need not
define each parameter type individually. You can separate
parameters by commas and define the type after the colon following
the last parameter.
procedure Check( fl1, fl2 : boolean );
Of course, both ways can be independently combined.
procedure TestFlProcedure return valueags( fl1, fl2 : boolean; fl3 : boolean );
If the procedure has no parameters, empty parentheses are
placed in the place of the list of parameters. Empty parentheses
must be used both in the definition of the procedure also in
procedure call notation — parentheses
identifies this is a procedure and not data element identifier of
something else.
procedure Go(); (* procedure definition *)
...
Go(); (* procedure call *)
Procedure return value
In addition to parameters, the procedure may also contain
return value. It is possible to meet a series of
built-in functions, e.g. function sin for
calculation of the sine of the respective angle etc., when
writing expressions in the Control Web. The
use of such a function is easy — it is
enough to write its name in the expression and to define the
parameter (or parameters) in parentheses after the name. When
evaluating the expression, the function is called and the
value returned by the function (in our case the value of the
sine of the respective angle) is used in its place.
Similarly as built-in functions, procedures may return
values. If any procedure returns a value, it is possible to
call it within any expression. Of course, the return value of
the procedure is of a certain type and its use in the
expression must corresponds to this type.
It is of course not possible to call the procedure, which
does not return any value, in the expression. On the contrary,
the procedure returning the value can be called as an
independent statement, its return value will be omitted.
If the procedure returns the value, it is necessary to
define return type behind the right parenthesis completing the
list of parameters and the colon:
procedure CheckValue( Value : real ): boolean;
The use of procedures with return value is simple. For
example, the procedure which tests whether the particular
number is in the interval 0..1 will look like this:
procedure Test( a : real ): boolean;
begin
return (a >= 0) and (a <= 1);
end_procedure;
It is possible to use the procedure Test any time in the
logical expression (it returns the value of the type
Boolean):
if Test( a ) then (* fulfilled if the variable a has the value between 0 and 1 *)
...
end;
...
Test( 0.5 ); (* syntactically correct call but without any effect – the return value will be omitted *)
Parameters passed by reference
What will happen if a procedure changes parameter
value? If parameters are variables, it is possible to write
into them in the body of the procedure (to assign them a new
value). Therefore, OCL introduces two calling conventions,
which distinguish between two kinds of behavior during
assignment into parameters inside the procedure:
Parameters passed by the value are copied
into variables, which represent parameters of the
procedures. Because the code of the procedure works with
copies of parameters, it is possible to write into them
without affecting the data elements passed as parameters.
Because the procedure works with local copies, it is
possible to transfer any expression of the respective type
as a parameter during the call of the procedure. The
expression is evaluated before calling of the procedure and
the resulting value is stored in the variable of the
parameter. Warning: In the previous Chapter we spoke
about restriction of the range of data and about manners of
clamping of numbers if they exceed the permitted range of
the data type. In the case of parameters of procedures
(unlike independently declared data elements) we do not have
the possibility to choose between saturating arithmetic and
modulo arithmetic and the system will always use saturating
arithmetic. If, for example, the value of 1000 is passed
into the parameter of the type shortcard, the
value 255 appears in the parameter (the maximum number which
can be stored in the shortcard
type).
Parameters passed by the reference work in
another manner. No new variable with the value of the
parameter is created inside the procedure, but the procedure
code works directly with the variable passed as the
parameter. This means that any changes in the value of this
parameter are performed directly in the data element, which
was passed, instead of the local copy during the call.
Several restrictions result from it — it is not possible to pass a constant
(the procedure would not be able to write the value into it)
or an expression to the reference parameter because
the expression does not represent one data element.
Parameters passed by the value disappear in the
body of the procedure and their possible changes will not be
reflected outside. In term of the procedure their represent
only input parameters. Changes in parameters passed
by the reference will appear after completion in the passed
data elements, so their represent input and output
parameters.
The manner of parameter passing is defined during the
declaration of the procedure. Simple notation of the parameter
name and its type define parameter passed by value. The
keyword var before the parameter name means a
parameter passed by the reference. For example, in the
declaration of the procedure:
procedure Calculate( var Value : real; Flag : boolean );
the parameter Value is passed by the
reference, the parameter Flag by the value. It is
not possible to pass a constant or expression in the place of
the first parameter, a simple data element is required.
Examples of correct and incorrect calling are:
Calculate( 2.5, true ); (* error, constant literal passed as var parameter *)
Calculate( 2 * a, flag1 & flag2 ); (* error, expression passed as var parameter *)
Calculate( a, ~flag1 ); (* correct, if the passed data element a is not a constant and is of numeric type *)
Warning: If a delay statement pause, yield
or wait (these statements are explained below) is
used in the procedure, the procedure returns from call with
reference parameter values at the time when the procedure
executes the delay instruction, not at the end of the
procedure.
Passing arrays to procedures
Because the passed variable is not copied into the local
variable during the reference passing of parameters, but only
the reference to this valuable is used, it is possible to
transfer the whole array as the var parameter. If the whole
array is to be passed, the parameter is introduced with
keywords array of. For example, the procedure
searching for the maximum element of the array may have the
header:
procedure FindMax( a : array of real ): real;
It is not possible to pass scalar data element into such a
procedure, but it is necessary to pass the whole array.
Because arrays in the Control Web may have
arbitrary size and may start by any index, two functions
returning minimal and maximal array indexes loindex
and hiindex were introduced. Both functions
require the array identifier (not one element of the array!)
as a parameter and return the lowest and the highest index of
the respective array. The code in the procedure may handle all
elements in the field with the use of these functions. The
above-mentioned procedure FindMax could be
implemented, for example, as follows:
procedure FindMax( a : array of real ): real;
var
i : longint;
max : real;
begin
max = a[ loindex( a ) ];
for i = loindex( a ) + 1 to hiindex( a ) do
if a[ i ] > max then
max = a[ i ];
end;
end;
return max;
end_procedure;
Warning: Parameters of type array of are
automatically passed by reference, even if there is no var
keyword in their definition. It is not possible to pass the
whole array by value (to copy the array into the local
variable). Any changes in values of array elements are
performed directly in the passed field.
Procedure signature
It is necessary to identify procedures (to mutually distinguish
them) in many cases. Procedure signature is used for this purpose.
For example, it is not possible to declare two identical
procedures within one instrument because then it would be
impossible to decide which procedure should be called.
The simplest way how to define procedure signature would be
declaring its name as its signature. Then it would be impossible
to declare two procedures of the same name. However, if you need
two procedures, which do the same thing, and only have different
parameters there is no need to create a new name. That is why
procedure signatures are created not only by the procedure name
but also by its parameters within the Control Web. Thus, you can define, for example, two
procedures:
procedure SetColor( color : cardinal );
procedure SetColor( red, green, blue : shortcard );
Although both procedures have the same name, they have
different parameters and thus it is not a problem to distinguish
them during calling. If you call SetColor with one
parameter, the first procedure will be called; if you call the
procedure with three parameters, the second one will be called. An
attempt to call SetColor with different number of
arguments or with arguments of different type will cause a
compilation error.
Warning: Automatic conversion of numeric types is
performed in Control Web. In practice, it
means that, we can for example assign variable of type real
to the variable of type integer (of course with the
risk of losing information, for details see the sub-chapter about
expressions) and it is possible to pass the variable of the type
cardinal into the procedure with the parameter of the
type real. Automatic conversion of numerical
types is very comfortable, but it has one consequence, which you
must bear in mind. It means that signatures of procedures with the
same number of numerical parameters (no matter whether they
differ) are exchangeable and thus they cannot be defined in one
instrument. procedure SetColor( color : integer );
procedure SetColor( color : real ); (* Error, "redefined procedure", numeric types are replaceable. *)
Procedure names are like all other identifiers in the
Control Web case sensitive. This means that
procedure SetColor( color : cardinal );
procedure setColor( color : cardinal ); (* name begins with a small letter, it concerns another procedure *)
are different procedures with different names.
Note that the signature consists only of the procedure name,
parameter types and the type of return value. The own names of
the parameter do not influence the signature. Therefore, the
procedures
procedure SetColor( color : cardinal );
procedure SetColor( c1 : cardinal );
have an identical signature.
Procedure local data
You already know that if a procedure has parameters, these
parameters can be referred to as a special kind of variables. The
specialty is that if the parameters have the same names as global
data elements, they have priority in use and global data elements
are made invisible. Another specialty is that parameter variables
are visible only from inside the procedure.
These properties are useful for many algorithms. For instance,
there is no reason why a variable used as loop counter should be
visible also in other procedures. Moreover, if execution of the
procedure pauses within the loop, some other instrument can change
the variable value and the algorithm will not work correctly. It
would definitely be advantageous to reserve some space inside the
procedure for such a variable, so that other instruments do not
see it and so that it does not occupy a globally visible
identifier, which can also be used otherwise. Therefore, it is
possible to declare local variables and constants within
procedures.
Definition of local variables and constants must appear in the
procedure notation before the proper procedure code (the following
sub-chapter deals with the manner of notation in detail). Local
variables and constants can be used in the same way as procedure
parameters, with the exception that they do not create the
procedure interface and their values cannot be, unlike parameters,
set or read outside the procedure. As in the case of parameters, a
global data element of the same name as local data element becomes
invisible.
Local variables considerably differ from parameters by their
value at the beginning of procedure execution. Whereas parameter
values are assigned by whoever is calling the procedure, local
variables will have their initialization value stated in the
declaration.
However, in many cases it is an advantage if the value of the
local variable between individual callings is not forgotten. For
example, if you wish to store a value for the whole period of the
application's running in the local variable, it would be wrong if
this value was set on the initial value upon each calling of the
procedure. Therefore, it is possible to declare the local variable
by means of two keywords — var
and static. Whereas the variable declared after the
word var will always be initialized at the beginning
of the procedure, the variable declared after the word static
will be static, the initializing value will be set during the
starting of the application and then the last assigned value will
be preserved.
Warning: If you want the value of the local variable to be
preserved also between individual callings of procedures, you must
declare it after the keyword static, not var.
Notation of procedure code
Procedure notation must correspond to the following grammar.
Terminal symbols are written in small letters, non-terminal
symbols in capital letters:
PROCEDURE -> procedure ( PARAMETERS ); { DECLARATION } begin BLOCK end_procedure ;
PARAMETERS -> [ name { , name } : TYPE ] { ; name { , name } : TYPE }
DECLARATION -> label LABEL_LIST ;
| const CONST_LIST ;
| var VAR_LIST ;
| static VAR_LIST ;
LABEL_LIST -> [ Identifier ] { , Identifier }
CONST_LIST -> [ Identifier = Value ] { ; Identifier = Value }
VAR_LIST -> [ Identifier : TYPE ] { ; Identifier : TYPE }
TYPE -> boolean
| shortreal
| real
| shortint
| integer
| longint
| shortcard
| cardinal
| longcard
| string
BLOCK -> [ STATEMENT ] { ; STATEMENT }
STATEMENT -> if EXPRESSION then BLOCK
{ elsif EXPRESSION then BLOCK }
[ else BLOCK ]
end
| switch EXPRESSION of
{ case CONST_EXPRESSION [ , CONST_EXPRESSION ] ; BLOCK }
[ else BLOCK ] end
| loop BLOCK end
| while EXPRESSION do BLOCK end
| repeat BLOCK until EXPRESSION
| for SimpleNumericVariable = EXPRESSION to EXPRESSION [ by CONST_EXPRESSION ] do BLOCK end
| goto Identifier
| pause EXPRESSION
| yield (* alias for pause 0 *)
| wait EXPRESSION
| commit
| send Identifier { , Identifier }
| sound EXPRESSION
| return [ EXPRESSION ]
| Identifier : [ STATEMENT ]
| DataElement = EXPRESSION
| move ArrayDataElement , ArrayDataElement, EXPRESSION
| MethodName ( PARAM_LIST )
| self . MethodName ( PARAM_LIST )
| ObjectName . MethodName ( PARAM_LIST )
| DataElement -> MethodName ( PARAM_LIST )
| (* empty statement *)
(* this rule is valid only in block between 'loop' and 'end' *)
STATEMENT -> exit
| continue
PARAM_LIST = [ PARAMETER ] { , PARAMETER }
PARAMETER = EXPRESSION
| DataElement
| Array
Each procedure starts by the declaration of the header
procedure ProcedureName( list_of_parameters );
procedure ProcedureName( list_of_parameters ): return_value_type;
The heading is followed by declarations of blocks separated by
the character ; (semicolon). In total there are five types of
blocks:
Block label declares the labels used in the
procedure.
Block const declares local
constants.
Block var declares local initializing
variables.
Block static declares local static
variables.
Block begin introduces code of the
procedure.
Blocks of declaration of labels label, constant
const, initialized variables var and
static variables static may be defined in any
sequence order and may arbitrarily repeat or they need not be
mentioned at all. But it is necessary to bear in mind that each
symbol must first be declared and then used. For example, the
notation
var
a : real {init_value = init_a}; (* error, symbol init_a is not known *)
const
init_a = 1;
is incorrect. The constant used for initialization of the
variable must be defined earlier.
const
init_a = 1;
var
a : real {init_value = init_a};
The block defining the own code of the procedure begin
must be always present, must be the only one and must be the last
one. This block always terminates with the keyword end_procedure;
which also closes the whole procedure notation.
Labels
If the goto instructions are used within the
program, it is necessary to declare all identifiers used as
labels after the keyword label first of all.
Individual identifiers are separated by commas and the whole
label declaration ends with a semicolon. The destination of
the jump is marked by using the label followed by a colon
before the statement, which is to be executed after the goto
statement.
Example:
procedure Test();
label
Error;
begin
if a < b then
goto Error;
end;
...
Error:
sound "ALARM.WAV";
...
end_procedure;
Declaration of local constants and variables
As mentioned above, constants are declared after the
keyword const, initialized variables after the
keyword var and static variables after the
keyword static. Notation of local constants and
variables is syntactically identical with the notation of
global constants and variables.
Constants are declared in the following form:
const
constant_name = value;
Constant type is not defined, it is derived automatically
from its value.
Variables are defined in the form:
var
variable_name : type { attributes };
Procedure body
The body of the procedure introduced by the keyword begin
contains individual statements separated by the character
; (semicolon). The body terminates with keyword
end_procedure and the end of the procedure body
is at the same time the end of the declaration of the whole
procedure. Statements can be divided into two types:
Control statements, which determine the execution of
other statements, e.g. conditions, loops or other procedure
calls.
Executive statements, which influence states of
variables, activate other instruments etc.
Statement if
The if statement serves for conditional
performing of certain parts of the program. After the keyword
if there must be a logical expression and the
keyword then. The following block of statements
is executed only if evaluation of the logical expression
returns the value true. The block of statements
ends in three ways:
By the keyword end. In the case of
non-fulfillment of the condition, the program continues with
the statement after this keyword.
By the keyword else. After else
there is a block of statements, which will be executed only
in the case of non-fulfillment of the condition. This block
of statements must end with the keyword end.
By the keyword elsif. After this keyword
there is the condition and the keyword then and
the block of commands, the same as after if.
This combination allows selection of a single variant in
dependence on various conditions. The block of statements
following the first fulfilled if or elsif
condition is executed and possible following elsif
conditions are not even tested. If no condition is fulfilled,
the block of statements after the keyword else is
executed (if present). If there is no else
section, the program continues execution after the keyword
end.
Example:
if i < 10 then
i = i + 1;
end;
if i < 0 then
di = 1;
i = 0;
else
di = -1;
i = 100;
end;
if ErrorCode = 0 then
ErrorMsg = 'Error in reading of the channel';
elsif ErrorCode = 1 then
ErrorMsg = 'Error in writing of the channel';
elsif ErrorCode = 2 then
ErrorMsg = 'Communication error';
else
ErrorMsg = 'Unknown error';
end;
Statement switch
The switch statement divides the execution of
the code on the basis of the value an expression. If the value
of one expression is tested, like for instance the value of
the variable ErrorCode in the previous example,
the use of the switch statement is easier and its
execution is faster then a series of commands if elsif.
switch ErrorCode of
case 0;
ErrorMsg = 'Error in reading of the channel;
case 1;
ErrorMsg = 'Error in writing of the channel';
case 2;
ErrorMsg = 'Communication error';
else
ErrorMsg = 'Unknown error';
end;
Similarly to the if statement, the else
block is not mandatory. If the tested expression does not
result in any of the values listed after the keywords case
and the branch else is missing, no branch of the
command switch will be performed.
It is not necessary to write only one value after the
keyword case, it is possible to list more values
separated by commas, e.g.
switch InputValue of
case 0, 1, 2, 3;
...
case 10, 100, 1000;
...
end;
Warning: Values after the keywords case must
be constant (the are calculated at compile time). If it is
necessary to compare the expression with general expressions,
it is necessary to use the if elsif
statement.
Statement loop
The loop statement implements general loops.
If you do not want this loop to be endless, you must finish it
by the exit statement. The keyword exit
is valid only inside the cycle loop (between
loop and end keywords). There is a
block of statements after the keyword loop ending
with the keyword end, which will be cyclically
executed.
The loop statement can be used if the loop
exiting condition should be evaluated inside the block of
statements or if there are more places for leaving the loop.
If the ending condition should be evaluated at the beginning
or the end of the cycle, it is better to use while
or repeat until loops. If the cycle
is to be repeated for a certain number of loops known in
advance, it is more convenient to use the for
loop.
If there is a loop located inside another
loop, the exit statement will cause the inner
loop is being left. It is also possible to use the continue
statement inside the loop. This statement causes
transferring of control at the beginning of the loop, that is
jumping over the rest of the statements in the loop. In the
case of more loops, the command continue relates
to the inner loop.
Example:
loop
a = b + c;
if a > 123 then
exit;
end;
i = i + 1;
if i > 100 then
exit;
end;
if i < 20 then
continue;
end;
j = i;
end; (* loop *)
Statement while
This statement is a special case of loop. There is the
logical expression after the keyword while
followed by the keyword do. If the logical
expression is evaluated to true, the following
block of statements is executed up to the keyword end.
If the condition is not fulfilled even for the first time,
the statements inside the loop will not be executed at
all.
Example:
i = 0;
while i < 10 do
a = a + c;
i = i + 1;
end; (* while *)
Statement repeat until
The keyword repeat introduces the beginning of
the block of statements. This block ends with the keyword
until followed by the logical expression. After
execution of the block of statements the program tests the
condition after until and if the result is false
it will start to execute the block of statements again.
If at the beginning the logical expression has the value
true the block of commands will be executed at
least once.
Example:
i = 0;
repeat
a = a + b;
i = i + 1;
until i >= 10;
Statement for
The for statement introduces the counted
cycle. There is numeric variable after the keyword for,
the assign symbol = and the numeric
expression. Then, there is the keyword to and the
next numeric expression followed by the keyword do
and the block of statements of the body of the loop ending
with the keyword end. When entering the cycle,
the numeric variable will be set to the value determined by
the expression following the equal sign. Then the value of
this variable will be compared with the value of the
expression following the keyword to. If the value
is smaller or equal to this value, the body of the cycle will
be executed up to the corresponding end keyword,
the control variable will be increased by one and the
comparison will be repeated. If the initializing value of the
control variable is greater than the expression after to,
the body of the cycle will not be executed.
Example:
for i = 1 to 10 do
a[ i ] := i * i;
end;
for i = 3 to 2 do
(* body of the cycle will not be executed *)
end;
It is not always required that the control variable should
increase by 1. Sometimes it is necessary to increase the step
or decrease the control variable. For this purpose, it is
possible to define the step of the control variable after the
keyword by before the keyword do.
Example:
for Alpha = 0 to 360 by 10 do
x = sin( Alpha / 180 * Pi );
end;
for x = 10 to 1 by -1 do
y = x * 10;
end;
Warning: The expression defining the step after keyword
by must always be constant. This means that it
must not contain any variables or channels.
Statement yield
As has already been said, the whole procedure body is
executed in a single time step, that is, with the same value
of input/output channels. However, sometimes it is necessary
to end the time step and enable the system to measure new
values of input/output channels. The command yield
will finish executing the procedure in the current time step
and provide time for the system to activate all other
instruments which are to be activated in the running time
step. Executing the procedure will continue in the nearest
system time step. This does not mean the next activation of
the instrument, which implemented this procedure, but the
nearest system tick.
Example:
while Level < 12.5 do (* waits for the achievement of the level *)
yield; (* termination of the time step and making possible communication for changing the new value *)
end; (* while *)
Control1 = true; (* closing the valve *)
pause 5; (* waiting for emptying the pump *)
Control2 = false; (* switching off of the pump *)
Statement pause
This statement serves for stopping of the activity of the
program for a certain period. There is a numeric expression
stating the number of seconds of interruption of the program
after the pause keyword. If the execution
approaches the pause statement, it will stop the
execution for the defined period and then continue by the next
statement. The statement pause 0 does not stop
the program, but it provides the system with time for possible
servicing of other instruments (the same function is performed
by the statement yield).
Example:
while Level < 12.5 do (* waiting for reaching the water level *)
pause 0; (* can be substituted with yield *)
end;
Control1 = true; (* closing the valve *)
pause 5; (* waiting for emptying of the pump *)
Control2 = false; (* switching off of the pump *)
Statement wait
This statement will interrupt the running of the program
until the fulfillment of the condition (boolean expression)
following the keyword wait. If the value of the
expression is true the program continues with the
next statement in the same time step. If the value of the
expression is false the program provides the
system with time for possible servicing of other instruments
and performing th I/O operations and tests the condition
again. An example of description of the yield
statement would be possible to write more efficiently:
Example:
wait Level >= 12.5; (* waiting for the achievement of the level *)
Control1 = true; (* closing the valve *)
pause 5; (* waiting for emptying of the pump *)
Control2 = false; (* switching off of the pump *)
Statement commit
The commit statement serves for initiation of
communication (writing and reading of channels) without the
loss of the time step. The use of the commit
statement has a similar effect to, e.g. the conditional
command containing the channel in the condition — channels indicated for reading are read and
channels with a written value are written into the driver.
A similar effect to commit have for instance
yield or pause statements, however,
these statements will end the execution of the time step and
will cause continuation of the algorithm in the next step. The
commit statement only initiates communications
and the algorithm continues within the same time step.
Details of the function of the command commit
are described in the sub-chapter Use of channels in procedures.
Statement return
The return statement has two forms depending
on whether the respective procedure returns some value or not.
If the procedure does not return value, the command return
will cause termination of the execution of the procedure and
return to the code, which called the respective procedure.
Another call to the procedure will start it again from the
beginning.
procedure Divide( a, b : real; var c : real );
begin
if b = 0 then
c = 0;
return;
end;
c = a / b;
end_procedure;
However, if the procedure returns some value (it may be
called within the expression), then there is an expression
whose value is passed as a return value of the procedure after
the return keyword.
procedure Divide( a, b : real; var c : real ): boolean;
begin
if b = 0 then
return false;
end;
c = a / b;
return true;
end_procedure;
Statement send
There is a list of identifiers separated by commas
indicating the names of instruments in the application (of
course, the list can be short and contain only one instrument)
after the keyword send. This command will ensure
activation of the specified instrument(s) immediately after
finishing the actual time step. If you want to activate the
same instrument as the one implementing the given procedure,
you can write its name or use the substitute identification
self. The statement send self means
activate itself in the nearest next system time step.
This activation will perform all actions (communication with
input/output devices and actualization of channel values is
executed before it) and it practically equals to inserting
another time step for all instruments listed in the send
statement.
There is an equivalent of the send statement
in some other instruments in the form of the property receivers
followed by the list of instruments. If more instruments have
the same name and this name is mentioned in the send
statement, all instruments with this name are activated.
Example:
Panel1Visible = (KeyInput = F1);
Panel2Visible = (KeyInput = F2);
send Panel1, Panel2;
A similar procedure can also be used for instruments using
the command receivers, e.g.:
multi_switch or panel with
active rectangles etc.
Statement sound
This statement ensures the playing of sound files of WAV
format. There is a string expression which represents the name
of the file containing the sampled sound after the keyword
sound. Of course, the functionality of this
statement is conditioned on the presence of a supported sound
card in the computer and the correct installation of their
drivers in the Windows environment. If sound support is not
correctly installed, the statement will be ignored.
Example:
switch Alarm of
case 1;
sound 'fire.wav';
case 2;
sound 'explosion.wav';
else
sound 'quiet.wav';
end;
Statement move
The move statement serves for mutual
assignment of arrays or their parts. The use of move
is always faster than assigning individual array elements e.g.
using the program loop. Considerable speeding up is reached
especially when transferring the channel array into the
variable array. When assigning array elements into variables
using the program loop, the program is always made to ask the
system core to measure each element of the channel separately,
because generally a new value of the measured element of the
channel array can influence the index of the subsequent
element of the array.
Statement syntax begins with the keyword move,
followed by the first element of the array, which is to be
read (source). The first element of the destination array
(target) is separated by comma. Another coma separates an
expression for the number of elements, which are to be
moved.
Example:
move source_array[ i ], destination_array[ j ], count;
This command moves in total count elements of
the array source_array starting from the element
with the index i to the array destination_array
from the element with the index j.
Assign statement
The assignment statement can occur anywhere in algorithm
notation. It has the form data_element = expression.
The target data element must be writable (it cannot be e.g.
constant). There is an arbitrary expression on the right side
of the equal sign, whose type must correspond to the data
element type on the left from the equal sign. A boolean
expression must correspond to boolean data element. A string
expression must correspond to string data element. If the data
element is numeric (the type shortint, integer,
longint, shortcard, cardinal,
longcard, shortreal, real),
it is possible to assign result of any numeric expression to
it.
Warning: If the type of data element is not able to take
the result of the expression, the information can be
lost. The decimal part of the number is cut away when
converting to integers; big numbers can be clamped at the
highest significant places.
Example:
(* numeric expressions *)
i = i + 1;
x[ 12 ] = ( k[ j ] + k[ j + 1 ] ) / ( atan2( theta, ksi ) * 2 );
a[ i ] = fi * cos( beta ) / PiBy2;
b[ i + 1 ] = fi * sin( alfa ) / PiBy2
(* logical expressions *)
result = value <= ( a + 1 );
ExitCondition = true;
(* string expressions *)
Message = 'Error: ' + ErrorString;
Title = 'Demonstration panel';
Calling of procedures
Previous examples were concentrated on how to write
procedures, how to control their execution, how to work with
local data and how to pass parameters into and from
procedures. The final thing you need to know is how to call
procedures. It is actually a closed issue, because procedures
are called from other procedures, and therefore notation of
this calling simultaneously belongs to description of the
procedure's internal parts.
If procedure calling was written in the previous text, it
was in the form
Go();
This calls the procedure Go() of the same
instrument or section which calls it. If the respective
instrument or section does not have the Go()
procedure, an error in compiling will occur. If you want to
call a procedure of another instrument or section, you have to
write the name of the instrument (section) implementing this
procedure before procedure name. It has already been said that
procedures cannot be implemented outside any instrument or
section.
InstrumentName.Go();
The procedure can be also called using instrument
pointers.
InstrumentPointer->Go();
If the pointer doesn't point to an existing instrument, or
points to an instrument that doesn't have the Go()
procedure, a critical error will occur and the execution of
the application will be halted.
If you are calling a procedure of an instrument which is in
another module, the module name must also be before the
instrument name
ModuleName.InstrumentName.Go();
Several facts must be taken into account when calling
procedures:
Unlike the activation of instruments by the statement
send, which practically represents insertion of
another time step following immediately after the actual
time step and including communication with input/output
devices, procedure call is always performed immediately
in the current time step and, hence, just the actual
values of channels are evaluated. Calling a procedure to
an instrument will not cause reading of a new value of the
evaluated channel of the input/output device, unless reading
is called by some other instrument evaluated within the same
time step.
Procedure call is a blocking operation. It
means that executing of the code is transferred from the
calling procedure to the called procedure and the calling
procedure will continue with the instruction following the
call statement as late as after finishing the called
procedure and returning from calling.
If the execution of the procedure is stopped by the
instruction pause, yield or wait
then the procedure returns from calling into the called
procedure and the calling procedure continues in its
running without the completion of the code of the called
procedure to the end. A such called procedure continues
in the nearest system time step (after yield),
after expiration of the defined period (after pause)
or after the fulfillment of the condition (after wait)
independently in the execution of its code, as if it were
activated, e.g. by the command send, i.e.
including measurement of channels, etc.
Procedures cannot be called recursively. A procedure
thus cannot call itself once again, not even through other
procedures! It is necessary to realize that the call chain
can be very complicated and recursion can occur after many
other embedded calls.
Comments
Comments can be used anywhere in procedure notation. The
comment is introduced by a comment parenthesis (*
and finished by the opposite parenthesis *).
Whatever is stated in these brackets is jumped over during
compilation. Thus, comments can be used not only for
explaining the meaning of procedures or individual statements,
but also for eliminating parts of code.
Comments can be nested in Control Web. The
depth of nesting is evaluated and the comment ends with the
last commentary parenthesis.
Example:
i = 0;
(* comment with the depth 1
(* depth of comments is 2 *)
the first commentary bracket is still valid *)
i = i + 1;
Of course, comment parentheses can be used wherever in the
text notation of the Control Web application.
With regard to the dual development environment, however,
comments cannot be preserved in graphical mode, for the
application source text totally lapses. Procedures are an
exception in this respect and store comments also when
changing over between text mode and graphical mode.
Always (un)true expressions in conditions
Syntax of many statements requires the presence of a
logical expression, which influences the behavior of the
statement (e.g. the while statement requires a
logical expression, which states whether the loop is performed
or not). Restriction of the range of data types, about which
we spoke in previous Chapter, may principally influence the
fulfillment (or non fulfillment) of conditional logical
expressions. Consider the following example:
var
c : cardinal;
begin
c = 10;
while c >= 0 do
...
c = c - 1;
end;
...
The while loop in this example will be
infinite, because the data element of the type cardinal
cannot contain a number smaller than 0. Similarly, the
condition:
var
a : shortint;
begin
if a > 200 then
...
will never be fulfilled because the maximum number in the
data element of type shortint is 127 and can
never be higher than 200. There are many examples of
expressions, which are permanently true or false also in other
examples repeat until, for to do,
wait, ...).
The compiler in some cases (e.g. if the data element is
compared with the constant) detects that the expression is
always true or false and writes a warning (not error in
compiling) during compilation. But it is not possible to
decide the unchangeability of the condition during compiling
in many cases, because it results from the logic of the
application. Therefore, it is necessary to remember this
possibility during debugging of the application or, better,
during its design.
Hint: The compiler performs many optimizations at
application startup only; optimizations cannot be performed
during togging of IDE mode or test compilations. One form of
optimization is calculation of constant sub-expressions. If
there are (non-)fulfilling conditions consisting of the data
element compared with the constant expression in the
application, a warning about permanent (non-)fulfillment of
the condition appears only at application startup, not during
IDE mode togging.
Event and user Procedures
As you already know, each procedure can be called from another
procedure. However, sometimes it is necessary to call at least
some procedures by another mechanism, otherwise the calling system
would never relive, no one would start with the first
calling.
Of course, such a mechanism exists and was mentioned in the
introductory part of this chapter. The Control Web does not have any main program, instead it
activates pieces of the code on the basis of actions of user or
system events. Procedures are also classified into this context
and many procedures are called when certain conditions are
fulfilled — of course, if you program these
procedures.
All procedures implemented by you within each instrument can be
subdivided into two groups:
Event procedures. If these procedures are
implemented, they are called directly by the system without the
assistance of other procedures when the corresponding event
occurs.
User procedures. The Control Web itself does not call these procedures. If these
procedures are to be activated, they must be called from some
other procedures, either user or event.
The specific instrument of the Control Web can
activate (call) an event procedure only if it recognizes it.
Event procedure must have a signature corresponding to event
signature defined by particular instrument to be called on event
occurrence (procedure signatures were described above). It
thus has to have a name (case-sensitive!) and parameter types
corresponding to the event procedure definition. Then it is
recognized by the instrument and called upon the occurrence of the
corresponding event.
Example:
switch sw1;
...
procedure OnOutput( state : boolean );
begin
(* this procedure will be called on each input action of the instrument switch *)
...
end_procedure;
...
end_switch;
Signatures of event procedures and conditions of their calling
are described with the respective instrument documentation. If you
open the inspector above the instrument and click on the
Procedures tab, the list of all event procedures of the
respective instrument will appear. Moreover, you have possibility
to complete any user procedures, but as was already said, you
yourself must take care of their calling.
There is a group of event procedures which are common to all
instruments. It does not mean that all instruments will call all
procedures from this group. However, if the given procedure has
sense for the given instrument, the instrument will call it. These
basic event procedures will be described in more detail when
describing the key event procedure OnActivate.
OnActivate() event procedure
OnActivate is an exceptional event procedure.
The first reason is that it is the only procedure always
called when the instrument is activated. Practically all
instruments carry out their activities within their
activation. If you for example define an expression for the
instrument meter, which will be calculated and
the result will be assigned to some data element, this
calculation, assignment and possible redrawing of the
meter instrument with the new value is carried
out within the activation. Within its activation the
alarm instrument tests the exceeding of limit
values and the archiver instrument records
data, the instrument draw animates its drawing,
etc.
Only instruments, which react to e.g. user actions or
communication events, perform some activity outside activation
intervals. For example the control instrument
writes the value into the data element after user action
without being activated. Similarly the httpd
instrument, working as a WWW server, sends data upon the
client's request, without waiting for activation.
Instruments can be activated in several ways. Activation
differs considerably in real-time applications and in data
driven applications.
In real-time applications the instrument can be
activated by these impulses:
Periodical timing by a value or timer.
Exception caused by another instrument (activation by
instrument).
Exception caused by a driver (activation by
driver).
In data driven applications periodic timing is
permitted only for some instruments, whose activity depends on
exact keeping of the time interval, e.g. for the instrument
archiver or pid_regulator.
Activation by exceptions from both instruments and drivers is
maintained. However, a new impulse is added:
If you declare a procedure with the following
signature:
procedure OnActivate();
this procedure will be called during each activation, more
exactly before each activation of the instrument. The
procedure OnActivate has the possibility to test
and modify data elements influencing instrument operation.
The second reason for the exceptionally of procedure
OnActivate is the fact that this procedure can correspond to
several signatures, even if only one procedure is in
question. The reason for this exception is simple. In
some situations it can be useful to know the reason for
procedure activation. Then you can write the procedure
OnActivate in the form:
procedure OnActivate( ByTimer, ByInstrument, ByDriver, ByData : boolean );
In this form the procedure OnActivate receives
four logical parameters stating the reason for activation. The
parameter ByData can be true only in
data driven applications. The code of the procedure OnActivate
can then test the cause of activation and perform an operation
only e.g. during periodical activation or when called by
exception from the driver etc.
Coincidence of several reasons for activation can occur in
certain situations. For example, an instrument activates
another instrument and within the same time step there will
also be time for the instrument's periodical timing. Then the
instrument is activated only once, the logical value true is
set in two parameters — ByTimer
and ByInstrument.
However, Control Web can provide much more
information to the instrument than just the reason for
activation during activation. Still, it is not possible to
pass all data as independent parameters. That is why this
information is passed in the form of the so-called bitmask.
The bitmask represents one number whose individual bits
(binary digits, having either the value 0 or 1) bear logical
values, the same as logical parameters. To find out whether
the corresponding bit of the respective number is set, it is
possible to use the function bitget. If, for
example, you need to execute some action in the procedure
OnActivate only during the activation of the
instrument from the driver may you write the procedure OnActivate
in two manners:
procedure OnActivate( ByTimer, ByInstrument, ByDriver, ByData : boolean );
begin
if ByDriver then
(* this code is executed only during the activation from the driver *)
end;
end_procedure;
procedure OnActivate( ActivateMode : longcard );
begin
if bitget( ActivateMode, 2 ) = 1 then
(* this code is also executed only during the activation from the driver *)
end;
end_procedure;
Although both ways of notation will work in the
same manner, the second way is a little more complicated
than the first one. So why the third form of OnActivate
exists? Because you can find more information by testing of
other bits in the parameter ActivateMode than
the reason for activation. The meaning of individual bits is
as follows: Object | Description |
---|
bit 0 | the instrument is activated by the timer | bit 1 | the instrument is activated by another
instrument | bit 2 | the instrument is activated by the driver | bit 3 | reserved, always 0 | bit 4 | the instrument is activated by change in data (only
in data driven applications) | bit 5 | the instrument is in time delay | bit 6 | the instrument is called startup and is
just activated as the first instrument of the
application | bit 7 | the instrument is called terminate and
is just activated as the last instrument of the
application | bit 8 | the application is in time delay (that is not
necessarily this instrument but any instrument in the
application) | bit 9 | the application terminates, the terminate
instrument is running | bit 10 | the instrument is activated from another
module | bit 11–31 | reserved for future use |
If you use the only parameter ActivateMode in
the OnActivate procedure, you may find out, for
example, when the application is in time slip or when it has
just started.
Summary:
The procedure OnActivate is the only
procedure called within instrument activation.
The calling of OnActivate always happens
before the application of the instrument, so it is
possible to change data elements processed within own
activation.
The procedure OnActivate can have three
signatures (parameter types), but it is always only one
procedure.
It is not possible to define more OnActivate
procedures within one instrument, even with other parameters
(different signatures).
Timing of procedures
Unless stated otherwise, the whole procedure algorithm (from
the beginning to the end) is executed in one time step, that is,
with identical measured values on the channels. This should be
realized especially when programming loops. It is possible to use
some of the delay instructions pause, yield
or wait. These instructions will cause interruption
of the procedure for a certain time, or until a condition is
fulfilled. The condition is tested in the following system time
step (and not in the time step of the instrument implementing the
procedure!) and if it is fulfilled, the procedure continues from
the statement following the condition, and not again from the
start. In this case, the values of read channels are measured
again.
More details about relations between calling and timing of
procedures and delay instructions can be found in the sub-chapter
Recursive calls and delay statements.
Warning: It is possible to implement an infinite (or at least a
very long) loop very simply. It is necessary to consider that such
a loop blocks passing to the next step and the whole application
waits for its completion.
Use of channels in procedures
If an instrument evaluates some expression within its time step
(e.g. meter) and this expression contains channels,
then the Control Web core will ensure measurement
of these channels before the evaluation of the expression (of
course, if it is possible during the time defined by the channel
parameter timeout). A similar situation will occur if
channels are used in the procedure OnActivate. Let's
have defined variables a1, a2 and a3
and the channels c1, c2 and c3.
Then the following part of the code:
a1 = c1;
a2 = c2;
a3 = c3;
will be executed after the system reads values of channels
c1, c2 and c3.
What will happen if the code looks as follows:
if c1 > 0 then
a2 = c2;
else
a3 = c3;
end;
There are all three channel used in the code notation, but the
construction of the algorithm rules out all three channels being
necessary at once. It will be necessary to use the channel c2
or c3 according to the value of the channel c1,
but not all three at the same time. In this case it appears to be
an unimportant discussion because the measurement of one channel
may not considerably cause delay of the application. However, if
there are not individual channels, but thousands or tens of
thousands of channels in individual branches, unnecessary
communications may represent a serious problem.
For these reasons, the execution of procedures is optimized in
the following manner: If branching in the program is found and it
is necessary to read some channel(s) for making a decision, the
algorithm specifying which channels are necessary to be read will
stop and the currently marked channels are read. This will enable
evaluation of the conditional expression specifying which part of
the procedure will be executed. Searching for the necessary
channels will continue only in the selected branch of the
procedure.
It is sometimes useful to enforce communication with peripheral
devices (reading of marked channels and sending of written
channels) within an algorithm. As already described, the simple
occurrence of some conditional command (e.g. if or
while) will ensure it. At the same time, the delay
instruction (yield, pause or wait)
will cause communication, but with the interruption of the
algorithm and continuation in the next system time step. However,
if it is necessary, e.g. to write more values in a sequence order
and, at the same time, not to lose the time step, it is possible
to use the statement commit. The occurrence of this
statement in the program does not influence execution of the
algorithm, it will only cause forced communication with the device
within one time step.
cmd = 'init';
commit; (* the string 'init' will be recorded in the channel *)
cmd = 'start';
commit; (* the string 'start' will be recorded in the channel *)
...
Two consecutive writes would cause overwriting of the string if
we did not use the commit statement.
Warning: There is a principal difference between yield
and commit statements, demonstrated in the following
examples. Let's consider part of the code a1 = c1;
commit;
a2 = c1;
commit; The commit statement will cause reading of the
channel c1. The assignment after commit
again requires reading c1, but because the algorithm
continues in the same time step, the system will not read the
channel c1 irrespective of the next commit,
because this channel has already been read. a1 = c1;
yield;
a2 = c1;
yield; In this case, the channel c1 will be really
read 2× because the algorithm will
be divided into more time steps. This restriction is not
valid for writing, which is performed the same number of times as
the number of occurrences of the algorithm.
Recursive calls and delay statements
It has already been emphasized that procedures cannot be called
recursively, meaning that if a procedure is unfinished and has not
yet run to the end, it cannot be explicitly recalled. Under normal
circumstances the system guarantees that no other procedure can be
started until the currently running procedure is accomplished.
Still, there exist several possibilities how recursive calling can
occur, for instance, a procedure called from another procedure
calls again the calling procedure. However, the calling procedure
is waiting for the finishing of calling of another procedure and
is thus unfinished. Such calling will be rejected and a warning
will be written into Log Window. It is important to
realize that the recursion does not have to be so simple, the
called procedure will call another procedure, which can call again
another procedure and the calling procedure can be called again as
late as on another level. This situation is also not
permitted.
The situation with recursive calling will be made much more
complicated if the delay statements pause, yield
or wait are used in procedures. Such instructions
will interrupt execution of the procedure and will bring execution
back into the calling procedure. The called procedure will remain
in unfinished status and cannot be called again until it runs to
the end.
The only exception is the implicitly called procedure OnActivate
which in this case continues in testing of the condition for
running and possibly continues from the place of interruption. If
the procedure OnActivate waits for some delay
statement and some driver activates the respective instrument, the
procedure OnActivate consumes this
activation for testing of the condition of the further run, not
for starting from the beginning and possible testing of the reason
for activation! If some delay statement is used in the procedure
OnActivate and, at the same time, the procedure
decides according to the condition of the activation, then
according to time relations there can be omission of treating of
the activation.
Frequent problems can originate especially when using delay
statements in event procedures. If execution of an event procedure
is interrupted and the given event occurs again, recursive calling
will happen again.
To avoid the above-mentioned problems with recursive calling,
OCL has a possibility to restrict the use of delay
instructions.
The delay instructions pause, yield
and wait can be used exclusively in the procedure
OnActivate. These instructions are not permitted in
any other event or user procedures.
The procedure OnActivate cannot be called
explicitly. It can only be called implicitly within instrument
activation.
Because this restriction can be very unpleasant in many cases,
you may take responsibility for not calling procedures recursively
and cancel this restriction by setting the parameter independent_procedure_execution
in the settings section to true. It is
possible to set this parameter in the tab Data
Inspectors — System in the integrated
development environment. You can use delay instructions and call
the procedure OnActivate without restriction in this
case.
Instrument local data
We have already mentioned the advantage of using local
variables and constants within the procedure. One of the key
advantages is that local data are visible only from inside the
procedure, they do not mess among global data elements
visible from all instruments.
However, if you define more procedures, which somehow cooperate
with the instrument, the mechanism of variables’ locality will
make them invisible even among procedures of the same instrument.
The problem is that procedures within one instrument need to share
variables invisible to other instruments. This can be to a certain
extent compensated for by storing necessary information in local
variables of the selected procedure and passing it to other
procedures as parameters. This process, besides being cumbersome,
will fail if event procedures, whose parameter list (signature) is
determined in advance, also need to access such variables.
Therefore it is possible to declare instrument local variables
and constants in blocks const, var and
static as within the body of the procedure. Thus, all
constants and variables declared within the instrument can be seen
by all procedures of the given instrument, but they are invisible
from outside.
Unlike the body of the procedure, where the block defining
constants and variables is automatically terminated by the
beginning of the other block, the instrument blocks const,
var and static must end in the
respective keywords end_const, end_var
and end_static.
Local data elements declared within the instrument can be used
also in notation of instrument expressions. However, if these are
used in an expression or procedure, the block where these elements
are declared must precede their use. Variables and constants
declared in this way are invisible from other instruments.
Warning: Whereas within the body of the procedure the compiler
monitors the sequence order of blocks and the block var
cannot be located after the block of commands introduced by the
keyword begin, it is not so in the case of the body
of the instrument. Thus, if there exist instrument local variables
with a name identical to the global data element, then the order
of declaration matters.
For example, the application
data
var
a : real { init_value = 1 };
end_var;
end_data;
...
instrument
meter m1;
expression = ch1 / a;
var
a : real;
end_var;
end_meter;
end_instrument;
will work well at first startup, although during compilation of
expression after the expression keyword the local
variable a does not exist. Therefore, the global
variable a initialized to 1 will be used for division
in the expression. However, after automatic generation of the body
of the meter instrument (e.g. after switching the
development environment from graphic mode to text mode), the block
of variables will be generated first
meter m1;
var
a : real;
end_var;
expression = ch1 / a;
end_meter;
and during next start the expression after the expression
keyword will be compiled with another (local) variable.
Moreover, this variable is initialized to the default value 0 and
it will cause the application runtime fatal error during division.
Therefore, if you write blocks of variables and constants in the
text mode, write them first (the graphical editor always generates
these blocks first).
What then is the process when searching for a data element of a
certain name? The first step depends on the fact whether the data
element is used in the expression inside the body of the
instrument (e.g. expression = a + b for the meter
instrument) or inside the instrument procedure. The next two steps
are the same.
First it is checked if there exists a local variable or
procedure parameter of the given name. Parameters and local
variables share one name space and that is why it is not
possible to give the same name to a parameter and a procedure
local variable. Of course, this rule will apply only to data
elements used inside procedures.
If an identifier is not found inside the procedure, it is
checked whether it is not declared inside the instrument as a
local variable or constant.
Only after the identifier has not been found in the list
of instrument local data is it searched for among global data
elements.
If the identifier has not been found even on the global
level, the system announces a compilation error.
Take into consideration that only names of data
elements are evaluated during compilation and their attributes
(kind and type) are ascertained according to names. Even if a
global and local data element are absolutely different in their
kind and type, they must always be used in compliance with the
lowest declaration. Consider the following example:
...
driver drv = vsource.dll, 'VSOURCE.DMF', 'VSOURCE.PAR'; end_driver;
data
channel { driver = drv; init_value = 0 };
a : real { direction = input; driver_index = 1 };
end_channel;
end_data;
instrument
meter m1;
expression = a; (* a used as global channel of the type real *)
end_meter;
string_display s1;
var
a : string;
end_var;
expression = a; (* a used as a local variable of the type string, global channel a is not available *)
procedure Go();
var
a : array [0..1] of boolean;
begin
...
if a[ 0 ] then (* a is the local array of variables of the type boolean *)
...
end;
...
end_procedure;
end_string_display;
end_instrument;
Note that on the local level (of both the instrument and the
procedure) only constants or variables can be declared. Channels,
scheduled elements and other complex data element kinds can be
declared on the global level only.
Like with local variables in the procedure, variables declared
within the block var will always be initialized at
the beginning of activation of the instrument, variables from the
block static will be initialized only on application
startup and then their content will be changed only by assignments
irrespective of the activation (timing) of the instrument. If the
value of the instrument variable changes in a strange way
to a certain value at application runtime, make sure it is located
in the block static and not in the block var.
Native procedures
There are many features of individual instruments, which can be
represented as properties only with difficulty or inefficiently,
even if they are expressed e.g. as an expression. These features
of individual instruments are therefore made accessible as
so-called native procedures.
Both definition and implementation of native procedures is
exclusively an issue of appropriate instrument class and has
absolutely no influence on its binary compatibility with the rest
of the system. Thus, instruments’ native procedures can be added
any time, without compromising the backward compatibility. Only
when an application program calls a procedure, which is not
available (e.g. in an attempt to compile a more recent program
using older versions of instrument DLLs etc.), the compilation
error is announced and the program must be modified.
However, the convention allows only to add new native
procedures and preserve names, types of arguments (signature), as
well as semantics of the already existing native procedures.
Keeping to this convention will ensure upward compatibility of all
applications, meaning that the more recent version of the
Control Web is able to compile all older
application programs.
The typical example of using native procedures is panel
visibility control. Each panel has the possibility to define the
expression controlling its visibility.
visibility = condition_of_visibility;
However, should the panel permanently evaluate the visibility
condition with periodical timing or after changing a data element
stated in the expression, it is an absolutely inappropriate waste
of time. That is why such panels are usually not timed but only
activated by another instrument which changes the visibility
condition. However, showing and hiding panels can be solved in a
very smart manner by means of OCL native procedures. For instance
it suffices to call native procedures to switch the visibility of
two panels:
panel1.Hide();
panel2.Show();
Many properties are only available through native procedures.
For example, a simple call
panel1.MoveTo( 100, 200 );
will move the instrument panel1 to coordinates
100, 200 of display pixels.
From the calling code point of view, the native procedures
behave in the same manner as any other procedures, only they are
not declared anywhere in the application source code. Each
instrument only implements a proper set of native procedures. The
list of native procedures, together with their description, can be
found within the instrument palette described in Chapter Integrated
development environment.
Therefore, there are three categories of procedures within the
Control Web, which differ in some properties:
Category of the procedure |
implemented by user |
called from procedures |
called by the system |
event procedures |
yes |
yes |
yes |
user procedures |
yes |
yes |
no |
native procedures |
no |
yes |
no |
Kinds of procedures and their calling
Summary
OCL is a powerful tool helping wherever it is necessary
to describe more complex behavior of the application. The basic
OCL features are:
OCL is a general programming language, enabling notation
of an arbitrary algorithm.
The whole OCL code is concentrated to procedures.
Procedures can never occur independently; they are always
defined within an instrument.
There is no main procedure. The Control Web is an event-driven system and procedures are
primarily called as responses to the occurrence of some
event.
The possibility to declare procedures is not restricted
only to event procedures, it is also possible to define any
quantity of user procedures in the application, however, other
procedures in the application are responsible for their
calling.
Instruments often implement native procedures, which
cannot be declared but can be called.
Local data elements, invisible from other places, can be
declared within procedures. Local data elements will make global
data elements of the same name (provided they exist)
invisible.
Procedures can have their own parameters, operating in
the same way as local data, but their values are set by the
calling code.
Procedure parameters can be passed in two — by value or by reference. The parameter passed by
the value is evaluated and copied into the variable of the
parameter, its changes will not be reflected outside the
procedure. All changes in the parameter passed by reference will
be reflected directly in the respective data element. It is not
possible to pass constants and expressions by reference, only
simple data elements.
It is possible to pass the whole array into the procedure
by reference. The built-in functions loindex and
hiindex enable finding of the lower and upper
indexes of the passed array.
The procedure may return the value. Then, it is possible
to call it as the function within the expression either in the
body of another procedure or another arbitrary
expression.
It is possible to declare local data elements within the
instrument. These elements then can be used in notation of
expressions in the instrument as well as in the procedures of
the given instrument. It depends on the sequence order of
declarations — local data elements must be
declared before their use anywhere in the instrument. Local data
elements are invisible from other instruments.
When deciding which data element of the same name is to
be used, the most local always has priority: a variable in the
procedure has priority against a variable in the instrument and
a variable in the instrument has priority against a global
variable.
It is not possible to declare other kinds of data
elements than constants and variables at the local
level.
The procedure is identified by its signature — name, types and sequence order of parameters and
the type of return value. All numerical types are compatible
(exchangeable) and coincide from the signature point of
view.
Two procedures with identical signatures cannot be
declared within one instrument. To avoid problems connected with
distinguishing signatures, it is not possible to declare
procedures with the same name as basic event procedures and
differing only in parameters, even if their signatures are
different.
The event procedure can be called after the incidence of
the event only if its signature corresponds to a defined
signature for the given event procedure.
|