Design
######

This chapter discusses various decisions made in the design of the SDK.

Command model standard
**********************
Each of the FBs/FCs in this SDK follow a convention called the "Command model standard". This convention is loosely based on the `PLCopen design recommendations for motion control blocks <https://plcopen.org/technical-activities/motion-control>`_.

Generally speaking, every FB/FC is designed to perform a particular command or set of commands. Their interfaces all follow the same conventions, so all blocks of a given model type are called in the same way.

There are three different command models:

* Function model
* Execute model
* Enable model

The function model is for simple stateless operations (i.e. FCs), while the execute and enable models run over time and may have internal memory (i.e. FBs). These models will be covered in more detail below.

The state output parameter
==========================
Each command can be thought of as a state machine, where each instance of the command exists in exactly one state at any point in time. These states are represented by a UDT output parameter called ``state``. Within this UDT, there is a member ``state.code`` that represents the current state as a 16-bit Word value.

The values for ``state.code`` are standardized such that the most-significant byte corresponds to a particular category. These categories are defined in the following table:

.. list-table:: 
    :widths: 20 20 60
    :header-rows: 1

    * - State category
      - State value (hex)
      - Description
    * - IDLE
      - 16#0xxx
      - The command is not running (enable model only)  
    * - BUSY
      - 16#1xxx
      - The command is running (execute model only)
    * - DONE
      - 16#2xxx
      - The command completed successfully (execute/function models only)
    * - VALID
      - 16#3xxx
      - The command is running without issues/errors (enable model only)
    * - ABORTED
      - 16#4xxx
      - The command was aborted (either locally or by a higher-priority command)
    * - ERROR
      - 16#8xxx
      - An error occurred while processing the command
    * - ERROR_BUSY
      - 16#9xxx
      - An error occurred, but the command is busy attempting to recover (enable model only)   

There can be multiple independent states within each category - for example, states 16#8000, 16#8001, and 16#8002 would represent distinct error conditions for that command.

The ``state`` parameter also expresses the command's state in the form of boolean flags, each corresponding to the bits in ``state.code``'s most significant byte. The flags are defined in the following table:

.. list-table:: 
    :widths: 15 15 70
    :header-rows: 1

    * - State flag
      - bit number
      - Description
    * - BUSY
      - 0 (2#0001)
      - The command is running  
    * - DONE
      - 1 (2#0010)
      - The command completed successfully (execute/function models only)
    * - VALID
      - 1 (2#0010)
      - The command is running without issues/error (enable model only)
    * - ABORTED
      - 2 (2#0100)
      - The command was aborted (either locally or by a higher-priority command)
    * - ERROR
      - 3 (2#1000)
      - An error occurred while processing the command 

For example, if ``state.code`` is set to 16#8000, the ERROR flag would be on and the rest would be off. In many cases, using the flags is more convenient than the state code value. More details regarding the state categories and flags for each of the command models will be covered in the next sections.

Function Model
==============
The function model represents a simple, stateless operation that processes immediately. All FCs in the SDK follow this model, since FCs, by definition, have no internal memory. An example is DecodeDateTime - this command reads the source input parameters and computes a native datetime value.

Function model commands typically use only DONE and ERROR states. If something goes wrong during execution (e.g. a parameter is out of range), an error code can be returned to notify the caller. The state codes are normally passed through the return value of the FC, but can also be passed as an output parameter. If a command has no error conditions, then the state code can be omitted altogether.

Execute model
=============

\ 

.. image:: ../assets/executeModelDiagram.drawio.png

The execute model is used for commands that run from start to finish. Once started, the command runs until it either completes the operation (DONE), experiences an error (ERROR), or is aborted (ABORTED). This model is stateful; its behavior depends on previous state and can change over time. 

Execute model commands are triggered using an ``execute`` boolean input parameter. Each rising edge on this parameter executes the command once. The command only re-triggers once the command is in a "terminal" state - i.e., DONE, ERROR, or ABORTED. Triggering the command while already running does nothing.

Here is a timing diagram for the execute model:

.. image:: ../assets/executeCmdTiming.png

.. list-table:: 
    :widths: 20 80   
    :header-rows: 0
    
    * - a.
      - A rising edge on execute starts the command, setting the busy flag to true.
    * - b.
      - Execute is automatically cleared by the command.
    * - c.
      - When the command completes, busy is set to false and done is set to true.
    * - d.   
      - When command is executed again, done is set to false and busy is set to true.
    * - e.
      - Additional execute signals while busy are ignored, rather than restarting the command.  
    * - f.
      - If a problem occurs while processing the command, the error flag is set to true.
    * - g.
      - Similarly, if something interrupts the command, the aborted flag is set to true.

Note that the ``execute`` input will automatically be cleared by the FB; there is no need to reset the trigger from outside logic. This provides additional flexibility, e.g. when controlling from browser-based HMIs that lack momentary push buttons. The input can still be written from a regular coil - either way, the command will behave the same.

Some commands (but not all) may include a ``cancel`` input for stopping a command early. ``cancel`` inputs work the same way as ``execute``, except they trigger a transition from BUSY to ABORTED.

Enable model
============

\ 

.. image:: ../assets/enableModelDiagram.png

The enable model is used for commands that run indefintely. This differs from the execute model in that the enable model does not have a DONE condition. Instead, this model refers to normal operation as VALID. The enable model is useful for things like jog operations.

Enable model commands use an ``enable`` boolean input to control their state. When ``enable`` is false, the command is IDLE and does nothing. When true, the command runs its logic sequence and writes to its outputs. 

An example of the enable model is the PumpControl FB. When enabled, the FB responds to inputs and updates the system mode accordingly. Disabling the FB allows it to be "detached" from the device's registers, which could then be controlled through some other logic.

Here is a timing diagram for the enable model:

.. image:: ../assets/enableCmdTiming.png

.. list-table:: 
    :widths: 20 80   
    :header-rows: 0
    
    * - a.
      - A rising edge on enable starts the command. The busy and valid flags are set to true.
    * - b.
      - When an error occurs, error is set to true and valid is set to false. In this case, the block logic can automatically recover from the error, so the busy flag stays at true.
    * - c.
      - The block logic handled the error - valid is set back to true and error is set back to false.
    * - d.   
      - In this case, an error occurred that requires user intervention. Error is set to true, and both valid and busy are set to false.
    * - e.
      - Setting enable to false resets the block and clears the error.  
    * - f.
      - Setting enable to false also sets both valid and busy to false.
    * - g.
      - The command may also be aborted, causing the aborted flag to be true and valid/busy to be false.

Unlike the execute model, the state flags for the enable model are not all mutually-exclusive. Under normal operation, the VALID and BUSY flags are true, meaning the command is running without issues. If an issue does occur, there are two possibilities - either it is something the FB can deal with automatically, or it is something outside of its control.

In the former scenario, the command enters an ERROR_BUSY state, which sets both the ERROR and BUSY flags to true. This means the command has identified an issue and is working to resolve it, e.g. retrying a failed message. 

In the latter scenario, the command enters an ERROR state, without the BUSY flag. The command has experienced an issue that it cannot fix, and it is up to the calling code to resolve it. When this happens, the command must be reset by setting ``enable`` to false. It can then be re-enabled once the issue has been dealt with.

The ABORTED state works the same way as the ERROR state, but this is typically used for situations where another process in the program has overwritten some value the command was using. Because enable model commands can be disabled at any point, they do not need a ``cancel`` input, like in the execute model.

Note that, while enable model commands are designed to be stopped at any time, there may be several cycles where the command performs some cleanup logic before returning to IDLE state.

Sharing access to device registers
**********************************
Many of the FBs/FCs in this SDK write to the same sets of Profinet device registers. For example, The ReadAlarmInfo and ReadRecipe FBs both call SendDCS internally, which uses the DCS registers to query data from the PD2K. Only one DCS request can be sent at a time, so care must be taken to avoid writing the registers from multiple locations in logic at the same time. For example, if ReadAlarmInfo and ReadRecipe were both executed at the same time and allowed to write to the DCS registers, they would interfere with one another and result in a race condition.

To overcome this, each of the FBs in this SDK are designed using a concept known as a semaphore. In general, semaphores prevent simultaneous writes to a shared resource (like the device registers) using a locked/unlocked mechanism. In short, the FBs will take turns without interfering with one another.

Take the example of executing both the ReadAlarmInfo and ReadRecipe FBs at the same time. Before either FB does anything, they will each read the DCS acknowledge and DCS command register values. If both values are 0 (i.e. no operation, or NOP), then that FB can assume no other FB is currently using the DCS registers and thus, continue its sequence. If either register is non-zero, however, then some other FB has accessed it first, so the FB will go into a wait state until either the registers become available again or the FB times out.

Note that this mechanism only works because all FBs follow the same rules of only accessing registers when they are considered unlocked. Nothing prevents the user's program from writing to a register directly, and doing so will cause conflicts with the SDK FBs.

It's also worth noting that this coordinating behavior works best with only 2-3 active FBs at a time. If many blocks are waiting, It's possible some of them will never be called because the earlier blocks will always take control first. In these situations, it is recommended to structure the program into a sequence where only a couple blocks are called simultaneously.

Accessing Profinet device data
*******************************
.. image:: ../assets/PD2K_device_config_addresses.png

When using the Profinet protocol, the device data is accessed via memory addresses in the process image of the PLC. The addresses can be freely assigned and do not have to be in any particular order. Unfortunately, this makes writing modular code like the SDK library difficult. Our FBs/FCs need some way to locate the registers without hardcoding in the address values.

One approach would be to pass each of the required input addresses as parameters to the FB/FC, and to write the output addresses through output parameters. However, this approach becomes tedious when many different registers are needed. For example, the SendDCS FB needs access to all DCS registers - over 16 total. It's also very easy for the programmer to mistakenly connect a register to the wrong parameter, resulting in bugs that are difficult to troubleshoot.

Another approach would be to use PLC tags assigned to the Profinet addresses. This is typical when writing a program for the end user. However, this means the code will depend on a specific set of tags being present in the PLC. Since the tags would be hardcoded into the logic, you would not be able to control multiple PD2Ks from the same PLC without first rewriting the code.

Fortunately, there is a built-in solution that provides easy, modular access to Profinet IO devices that does not involve rewriting any code!

Accessing a Profinet IO device by device number
===============================================
Within a given Profinet network, every device is assigned a unique device number. This number can be found in its device configuration under General -> Profinet Interface -> Ethernet addresses -> Profinet -> Device number:

.. image:: ../assets/profinet_device_number.png

Each Profinet device has "slots" for each of its data registers. These slot numbers are fixed for every PD2K system and defined in the GSDML file (e.g. "current system mode" will always be slot 1). Together, the device number and slot number define a "geographic location" for a given piece of data. 

Siemens provides a built-in way to look up the memory address of a slot from its geographic location. The solution is using the GEO2LOG FC (for S7-300/400, the equivalent is GEO_LOG). By passing a device number and slot number to this FC, we can look up the slot's corresponding memory location and access it like any other address. Since all Graco products have fixed slot numbers, we can access all device data from a FB/FC just by passing in the device number - no tags required!

This SDK provides two FCs called ``GetDataBySlotNum`` and ``SetDataBySlotNum``. These are wrappers around GEO2LOG/GEO_LOG, and they are used to read/write data from a Profinet device. The device number is passed to the ``pnDeviceNum`` input parameter, and the desired slot number is passed to ``slotNum``. The other FBs/FCs use these FCs internally to access the device registers by their given slot numbers.

.. _using-typetarget:

Using TypeTarget
================
The :ref:`TypeTarget` UDT is used extensively throughout the SDK code. This type has a member named "pnDeviceNum" should be assigned a specific PD2K Profinet device number. The SDK code uses this value to access device data through the method described above.

.. note::

  :ref:`TypeTarget` also supports running in "simulation" mode, where instead of operating on a real Profinet device, all data flows through a "simData" array member in the UDT. This can be useful for testing logic when a real PD2K is not available. Setting the "simulate" member to true enables this mode. Just make sure to set this to "false" when in production, otherwise the system will not operate correctly!