Tutorial ======== This chapter gives a gentle introduction to State Notation Language (SNL). Introduction ------------ SNL is a programming language specifically designed for programming *finite state machines* in such a way that it is easy for the program to interact with EPICS *process variables* (PVs), allowing to read and to write them and to react to changes in their value or status. An SNL program consists of a number of *state sets*, each one representing an independent state machine. The state set lists all the possible *states* that are allowed for this state set, along with conditions for transitions to other states of the same state set and code that is executed when the state transition take place. State Transition Diagrams ------------------------- A *state transition diagram* (STD) is a graphical notation for specifying state machines. In the STD, states are represented by oval boxes, transitions by arrows. State transitions can happen in response to both the present internal state and some external event or condition; this is represented by annotating the state transition arrow with a horizontal bar: the condition is noted above the line and the action to be taken when the transition happens below the line. A simple STD is shown in Figure 2-1. In this example the level of an input voltage is sensed; a light is turned on if the voltage is greater than 5 volts and the state is "Light is Off"; conversely, the light is turned off if the voltage becomes less than 3 volts and the state is "Light is On". Note that the output or action depends not only on the input or condition, but also on the current state. For instance, an input ``v`` of 4.2 volts does not alone determine the output (``light``), the current state matters, too. .. image:: Figure2-1.png Elements of the State Notation Language --------------------------------------- The following SNL code segment expresses the STD in Figure 2-1:: state light_off { when (v > 5.0) { light = TRUE; pvPut(light); } state light_on } state light_on { when (v < 3.0) { light = FALSE; pvPut(light); } state light_off } You will notice that the SNL appears to have a structure and syntax that is similar to the C language. In fact the SNL uses its own syntax plus a subset of C, such as expressions, assignments, and function calls. This example contains two code blocks that define two states: ``light_off`` and ``light_on``. Within each state block are :token:`when` clauses that define events or conditions (``v > 5.0`` and ``v < 3.0``) that lead to a state change. Following each condition is a block containing actions (C statements). The :c:func:`pvPut` function writes or puts the value in the variable ``light`` to the appropriate process variable. Following the action block, the next state is specified. The variables (in our example ``v`` and ``light``) must be declared and associated with process variables:: float v; short light; assign v to "Input_voltage"; assign light to "Indicator_light"; The above :token:`assign` clause associate the variables ``v`` and ``light`` with the process variables "Input_voltage" and "Indicator_light" respectively. We want the value of ``v`` to be updated automatically whenever it changes. This is accomplished with the following declaration:: monitor v; Whenever the value in the control system (the EPICS database) changes, the value of ``v`` will likewise change. Note however that this depends on the underlying system sending update messages for the value in question. When and how often such updates are communicated by the underlying system may depend on the configuration of the PV. For instance if the PV "Input_voltage" is the VAL field of an ai (analog input) record, then the value of the MDEL field of the same record specifies the amount of change that the designer considers a "relevant" change; smaller changes will not cause an event to be sent, and accordingly will not cause a state change in the above program. .. _A Complete Program: A Complete Program ------------------ Here is what the complete program for our example looks like:: program level_check float v; assign v to "Input_voltage"; monitor v; short light; assign light to "Indicator_light"; ss volt_check { state light_off { when (v > 5.0) { /* turn light on */ light = TRUE; pvPut(light); } state light_on } state light_on { when (v < 5.0) { /* turn light off */ light = FALSE; pvPut(light); } state light_off } } Each program must start with the word "program", followed by the name of the program (an identifier):: program level_check After that come declarations and then the state sets. Each state set must have name, which comes after the word "ss":: ss volt_check { ... } Adding a Second State Set ------------------------- We will now add a second state set to the previous example. This new state set generates a changing value as its output (a triangle function with amplitude 11). First, we add the following lines to the declaration:: float vout; float delta; assign vout to "Output_voltage"; Next we add the following lines after the first state set:: ss generate_voltage { state init { when () { vout = 0.0; pvPut(vout); delta = 0.2; } state ramp } state ramp { when (delay(0.1)) { if ((delta > 0.0 && vout >= 11.0) \|\| (delta < 0.0 && vout <= -11.0)) { delta = -delta; /* change direction */ } vout += delta; } state ramp; } } The above example exhibits several concepts. First, note that the :token:`when` clause in state ``init`` contains an empty event expression. This means unconditional execution of the transition. The first state in each state set is always the initial state, so we give it the name ``init``. From this first state there is an immediate unconditional transition to the state ``ramp``, initializing some variables during the transition. Note that the ``ramp`` state always returns to itself. The structure of this state set is shown in the following STD: .. image:: Figure2-2.png The final concept introduced in the last example is the :c:func:`delay` function. This function returns a ``TRUE`` value after a specified time interval from when the state was entered. The parameter to :c:func:`delay` specifies the number of seconds, and must be a floating point value (constant or expression). At this point, you may wish to try an example with the two state sets. You can jump ahead and read parts of Chapters 3-5. You probably want to pick unique names for your process variables, rather than the ones used above. You may also wish to replace the :c:func:`pvPut` statements with ``printf`` statements to display "High" and "Low" on your console. Variable Initialization and Entry Blocks ---------------------------------------- Since version 2.1 is has become simpler to initialize variables: you can use the same syntax as in C, i.e. initialize together with the declaration:: float vout = 0.0; float delta = 0.2; More complicated initialization can also be done using an :token:`entry` block instead of using a separate state:: ss generate_voltage { state ramp { option -e; entry { pvPut(vout); } when (delay(0.1)) { ... } state ramp; } } The actions in an entry block in a state declaration are executed whenever the state is enetered. The state option :: option -e; specifies that this action block is *not* executed if coming from the same state; in this case this ensures that the :: pvPut(vout); that appears inside the entry block is executed only once when the state is entered for the first time. Variable Names Using Macros --------------------------- You can specify the names of process variables or parts of them at run-time. This is done by using macro substitution. In our example we could replace the :token:`assign` statements with the following:: assign v to "{unit}:ai1"; assign vout to "{unit}:ao1"; The string within the curly brackets is a macro which has a name ("unit" in this case). At run-time you give the macro a value, which is substituted in the above string to form a complete process variable name. For example, if the macro "unit" is given a name "DTL_6:CM_2", then the run-time variable name is "DTL_6:CM_2:ai1". More than one macro may be specified within a string, and the entire string may be a macro. See :ref:`run time parameters` for more on macros. Data Types ---------- The allowable variable declaration types correspond to the C types: ``char``, ``unsigned char``, ``short``, ``unsigned short``, ``int``, ``unsigned int``, ``long``, ``unsigned long``, ``float`` , and ``double``. In addition there is the type ``string``, which is a fixed array size of type ``char`` (at the time of writing, a string can hold 40 characters). Sequencer variables having any of these types may be assigned to a process variable. The type declared does not have to be the same as the native control system value type. The conversion between types is performed at run-time. For more details see the :ref:`corresponding section in the reference `. You may specify array variables as follows:: long arc_wf[1000]; When assigned to a process variable, operations such as :c:func:`pvPut` are performed for the entire array. Arrays of Variables ------------------- Often it is necessary to have several associated process variables. The ability to assign each element of an SNL array to a separate process variable can significantly reduce the code complexity. The following illustrates this point:: float Vin[4]; assign Vin[0] to "{unit}1"; assign Vin[1] to "{unit}2"; assign Vin[2] to "{unit}3"; assign Vin[3] to "{unit}4"; We can then take advantage of the ``Vin`` array to reduce code size as in the following example:: for (i = 0; i < 4; i++) { Vin[i] = 0.0; pvPut (Vin[i]); } We also have a shorthand method for assigning channels to array elements:: assign Vin to { "{unit}1", "{unit}2", "{unit}3", "{unit}4" }; Similarly, the monitor declaration may be either by individual element:: monitor Vin[0]; monitor Vin[1]; monitor Vin[2]; monitor Vin[3]; Alternatively, we can do this for the entire array:: monitor Vin; And the same goes when `Synchronizing State Sets with Event Flags`_ and :ref:`QueuingMonitors`. Double subscripts offer additional options:: double X[2][100]; assign X to {"apple", "orange"}; The declaration creates an array with 200 elements. The first 100 elements of ``X`` are assigned to (array) "apple", and the second 100 elements are assigned to (array) "orange" . It is important to understand the distinction between the first and second array indices here. The first index defines a 2-element array of which each element is associated with a process variable. The second index defines a 100-element double array to hold the value of each of the two process variables. When used in a context where a number is expected, both indices must be specified, e.g. ``X[1][49]`` is the 50th element of the value of "orange" . When used in a context where a process variable is expected, e.g. with :c:func:`pvPut`, then only the first index should be specified, e.g. ``X[1]`` for "orange" . Dynamic Assignment ------------------ You may dynamically assign or re-assign variable to process variables during the program execution as follows:: float Xmotor; assign Xmotor to "Motor_A_2"; ... sprintf (pvName, "Motor_%s_%d", snum, mnum) pvAssign (Xmotor[i], pvName); Note that dynamic (re-)assignment fails (with a compiler error) if the variable has not been assigned statically. An empty string in the assign declaration implies no initial assignment and can be used to mark variables or array elements for later dynamic assignment:: assign Xmotor to ""; Likewise, an empty string can de-assign a variable:: pvAssign(Xmotor, ""); The current assignment status of a variable is returned by the :c:func:`pvAssigned` function as follows:: isAssigned = pvAssigned(Xmotor); The number of assigned variables is returned by the :c:func:`pvAssignCount` function as follows:: numAssigned = pvAssignCount(); The following inequality will always hold:: pvConnectCount() <= pvAssignCount() <= pvChannelCount() Having assigned a variable, you should wait for it to connect before using it (although it is OK to monitor it). See `Connection Management`_. Status of Process Variables --------------------------- Process variables have an associated status, severity and time stamp. You can obtain these with the :c:func:`pvStatus`, :c:func:`pvSeverity` and :c:func:`pvTimeStamp` functions. For example:: when (pvStatus(x_motor) != pvStatOK) { printf("X motor status=%d, severity=%d, timestamp=%d\\n", pvStatus(x_motor), pvSeverity(x_motor), pvTimeStamp(x_motor).secPastEpoch); ... These routines are described in :ref:`BuiltinFunctions`. The values for status and severity are defined in the include file *pvAlarm.h*, and the time stamp is returned as a standard EPICS ``TS_STAMP`` structure, which is defined in *tsStamp.h* . Both these files are automatically included when compiling sequences (but the SNL compiler doesn't know about them, so you will get warnings when using constants like ``pvStatOK`` or tags like ``secPastEpoch`` ). .. _EventFlags: Synchronizing State Sets with Event Flags ----------------------------------------- State sets within a program may be synchronized through the use of event flags. Typically, one state set will set an event flag, and another state set will test that event flag within a :token:`when` clause. The :token:`sync` statement may also be used to associate an event flag with a process variable that is being monitored. In that case, whenever a monitor is delivered, the corresponding event flag is set. Note that this provides an alternative to testing the value of the monitored channel and is particularly valuable when the channel being tested is an array or when it can have multiple values and an action must occur for any change. This example shows a state set that forces a low limit always to be less than or equal to a high limit. The first :token:`when` clause fires when the low limit changes and someone has attempted to set it above the high limit. The second :token:`when` clause fires when the opposite situation occurs. :: double loLimit; assign loLimit to "demo:loLimit"; monitor loLimit; evflag loFlag; sync loLimit loFlag; double hiLimit; assign hiLimit to "demo:hiLimit"; monitor hiLimit; evflag hiFlag; sync hiLimit hiFlag; ss limit { state START { when ( efTestAndClear( loFlag ) && loLimit > hiLimit ) { hiLimit = loLimit; pvPut( hiLimit ); } state START when ( efTestAndClear( hiFlag ) && hiLimit < loLimit ) { loLimit = hiLimit; pvPut( loLimit ); } state START } } The event flag is actually associated with the SNL variable, not the underlying process variable. If the SNL variable is an array then the event flag is set whenever a monitor is posted on any of the process variables that are associated with an element of that array. .. _QueuingMonitors: Queuing Monitors ---------------- Neither testing the value of a monitored channel in a :token:`when` clause nor associating the channel with an event flag and then testing the event flag can guarantee that the sequence is aware of all monitors posted on the channel. Often this doesn't matter, but sometimes it does. For example, a variable may transition to 1 and then back to 0 to indicate that a command is active and has completed. These transitions may occur in rapid succession. This problem can be avoided by using the :token:`syncQ` statement to associate a variable with both a queue and an event flag. The :c:func:`pvGetQ` function retrieves and removes the head of queue. This example illustrates a typical use of :c:func:`pvGetQ` : setting a command variable to 1 and then changing state as an active flag transitions to 1 and then back to 0. Note the use of :c:func:`pvFreeQ` to clear the queue before sending the command. Note also that, if :c:func:`pvGetQ` hadn't been used then the active flag's transitions from 0 to 1 and back to 0 might both have occurred before the :token:`when` clause in the ``sent`` state fired:: long command; assign command to "commandVar"; long active; assign active to "activeVar"; monitor active; evflag activeFlag; syncQ active activeFlag; ss queue { state start { when () { pvFreeQ( active ); command = 1; pvPut( command ); } state sent } state sent { when ( pvGetQ( active ) && active ) { } state high } state high { when ( pvGetQ( active ) && !active ) { } state done } } The ``active`` SNL variable could have been an array in the above example. It could therefore have been associated with a set of related control system ``active`` flags. In this case, the queue would have had an entry added to it whenever a monitor was posted on any of the underlying control system ``active`` flags. Asynchronous Use of pvGet ------------------------- Normally the :c:func:`pvGet` operation completes before the function returns, thus ensuring data integrity. However, it is possible to use these functions asynchronously by specifying the :option:`+a` compiler flag (see :ref:`CompilerOptions`). The operation might not be initiated until the action statements in the current transition have been completed and it could complete at any later time. To test for completion use the function :c:func:`pvGetComplete`, which is described in :ref:`BuiltinFunctions`. :c:func:`pvGet` also accepts an optional ``SYNC`` or ``ASYNC`` argument, which overrides the :option:`+a` compiler flag. For example:: pvGet( initActive[i], ASYNC ); .. _AsynchronousUseofpvPut: Asynchronous Use of pvPut ------------------------- Normally the :c:func:`pvPut` operation completes asynchronously. In the past it has been the responsibility of the programmer to ensure that the operation completed (typically by monitoring other variables). However, the function :c:func:`pvPutComplete` can now be used for this. Also, while the :option:`+a` compiler flag does not affect put operations, :c:func:`pvPut`, like :c:func:`pvGet`, accepts an optional ``SYNC`` or ``ASYNC`` argument, which forces a synchronous or asynchronous put. For example:: pvPut( init[i], SYNC ); :c:func:`pvPutComplete` supports arrays and can be used to check whether a set of puts have all completed. This example illustrates how to manage a set of parallel commands. :: #define N 3 long init[N]; long done[N]; /* used in the modified example below */ assign init to {"ss1:init", "ss2:init", "ss3:init"}; state inactive { when () { for ( i = 0; i < N; i++ ) { init[i] = 1; pvPut( init[i], ASYNC ); } } state active } state active { when ( pvPutComplete( init ) ) { } state done when ( delay( 10.0 ) ) { } state timeout } :c:func:`pvPutComplete` also supports optional arguments to wake up the state set as each put completes. The following could be inserted before the first :token:`when` clause in the ``active`` state above. The ``TRUE`` argument causes :c:func:`pvPutComplete` to return ``TRUE`` when any command completed (rather than only when all commands complete). The ``done`` argument is the address of a ``long`` array of the same size as ``init`` ; its elements are set to 0 for puts that are not yet complete and to 1 for puts that are complete. :: when ( pvPutComplete( init, TRUE, done ) ) { for ( i = 0; i < N; i++ ) printf( " %ld", done[i] ); printf( "\n" ); } state active Connection Management --------------------- All process variable connections are handled by the sequencer via the PV API. Normally the programs are not run until all process variables are connected. However, with the :option:`-c` compiler flag, execution begins while the connections are being established. The program can test for each variable's connection status with the :c:func:`pvConnected` routine, or it can test for all variables connected with the following comparison (if not using dynamic assignment, see `Dynamic Assignment`_, :c:func:`pvAssignCount` will be the same as :c:func:`pvChannelCount`):: pvConnectCount() == pvAssignCount() These routines are described in :ref:`BuiltinFunctions`. If a variable disconnects or re-connects during execution of a program, the sequencer updates the connection status appropriately; this can be tested in a :token:`when` clause, as in:: when (pvConnectCount() < pvAssignCount()) { } state disconnected When using dynamic assignment, you should wait for the newly assigned variables to connect, as in:: when (pvConnectCount() == pvAssignCount()) { } state connected when (delay(10)) { } state connect_timeout Note that the connection callback may be delivered before or after the initial monitor callback (the PV API does not specify the behavior, although the underlying message system may do so). If this matters to you, you should synchronize the value with an event flag and wait for the event flag to be set before proceeding. See `Synchronizing State Sets with Event Flags`_ for an example. Multiple Instances and Reentrant Object Code -------------------------------------------- Occasionally you will create a program that can be used in multiple instances. If these instances run in separate address spaces, there is no problem. However, if more than one instance must be executed simultaneously in a single address space, then the objects must be made reentrant using the :option:`+r` compiler flag. With this flag all variables are allocated dynamically at run time; otherwise they are declared static. With the :option:`+r` flag all variables become elements of a common data structure, and therefore access to variables is slightly less efficient. Process Variable Element Count ------------------------------ All requests for process variables that are arrays assume the array size for the element count. However, if the process variable has a smaller count than the array size, the smaller number is used for all requests. This count is available with the :c:func:`pvCount` function. The following example illustrates this:: float wf[2000]; assign wf to "{unit}:CavField.FVAL"; int LthWF; ... LthWF = pvCount(wf); for (i = 0; i < LthWF; i++) { ... } pvPut(wf); ... What's Happening at Run Time ---------------------------- At run time the sequencer blocks until something "interesting" occurs, where "interesting" means things like receiving a monitor from a PV used in a :token:`when` statement, an event flag changing state, or a delay timer expiring. The sequencer then scans the list of :token:`when` statements for the current state and evaluates each expression in turn. If a :token:`when` expression evaluates to non-zero the actions within that :token:`when` block are executed and the sequencer enters the state specified by that :token:`when` statement. The sequencer then blocks again waiting for something "interesting" to happen. A complete list of these conditions can be found in section 6.5. A common misconception among new state notation language programmers is that the sequencer somehow blocks inside the :c:func:`delay` functions within :token:`when` statements. This interpretation of the :c:func:`delay` function is incorrect but understandable given the name. A :c:func:`delay` function does not block the sequencer. Instead it tells the sequencer to compare the value specified by the :c:func:`delay` expression with a timer that is reset whenever the state is entered or whenever the state is entered from another state. If the timer matches or exceeds the value of the expression the :c:func:`delay` function returns ``TRUE`` otherwise it returns ``FALSE``. If your action statements have any sort of polling loops or calls to ``epicsThreadSleep`` you should reconsider your design. The presence of such operations is a strong indication that you're not using the sequencer as intended.