DUTs
----


DUT_EPS
^^^^^^^

::

    TYPE DUT_EPS :
    STRUCT
        // Contains EPS flags
        {attribute 'pytmc' := '
            pv: nFlags
            io: i
            field: DESC Contains EPS flags
        '}
        nFlags: UDINT := 16#FFFFFFFF;

        // Desciption of values nFlags contains
        {attribute 'pytmc' := '
            pv: sFlagDesc
            io: i
            field: DESC semicolon-delimited nFlag variable
        '}
        sFlagDesc: STRING;

        // Name to use for log messages.
        {attribute 'pytmc' := '
            pv: sMessage
            io: i
            field: DESC Message from EPS to usr
        '}
        sMessage: STRING;

        // Keep Track if nFlags are all true
        {atribute 'pytmc' := '
            pv: bEPS_OK
            io: i
            field: DESC check if nFlags are all true;
        '}
        bEPS_OK : BOOL := TRUE;
    END_STRUCT
    END_TYPE


Related:
    * `DUT_EPS`_


E_EcatDiagState
^^^^^^^^^^^^^^^

::

    TYPE E_EcatDiagState :
    (
        Idle := 0,
        GetSlaveAddresses := 1,
        GetSlaveStates := 2,
        GetTopoDataLen := 3,
        GetTopoData := 4,
        ScanSlaves := 5,
        GetSlaveIdentity := 6,
        GetSlaveName := 7,
        GetScannedSlaveName := 8,
        LogDiagnostics := 9,
        Done := 8000
    );
    END_TYPE




E_EcCommState
^^^^^^^^^^^^^

::

    TYPE E_EcCommState : (
        eEcState_UNDEFINED 	:= 0,
        eEcState_INIT 		:= 1,
        eEcState_PREOP 		:= 2,
        eEcState_BOOT 		:= 3,
        eEcState_SAFEOP 	:= 4,
        eEcState_OP 		:= 8
    ) UDINT;
    END_TYPE




E_LogEventType_WC
^^^^^^^^^^^^^^^^^

::

    {attribute 'qualified_only'}
    {attribute 'strict'}
    TYPE E_LogEventType_WC :
    (
        AlarmCleared 	:= 0,
        AlarmConfirmed 	:= 1,
        AlarmRaised 	:= 2,
        MessageSent 	:= 3
    );
    END_TYPE

    (* Note: this is the working copy version that is automatically made a
       global type when pinning ST_LoggingEventInfo *)




E_Subsystem
^^^^^^^^^^^

::

    //LCLS Defined subsystems, make sure these correspond with casSubsystems in FB_LogMessage
    TYPE E_Subsystem :
    (
        NILVALUE := 0, //Undefined system
        VACUUM := 1, //Vacuum control system
        MPS := 2, //Machine protection system
        MOTION := 3, //Motion control systems
        FIELDBUS := 4, //EtherCAT networks
        SDS := 5, //Sample delivery system
        OPTICS := 6 //Optics control system
    )WORD;
    END_TYPE


Related:
    * `FB_LogMessage`_


ST_EcDevice
^^^^^^^^^^^

::

    //EtherCAT Device struct for EtherCAT diagnostics
    TYPE ST_EcDevice :
    STRUCT
        nDeviceState: BYTE; //EtherCAT state machine state number, 8 is OP is good
        sDeviceState :STRING; //EtherCAT state machine state, OP is good
        nLinkState: BYTE; //EtherCAT link state, 8 is good
        nAddrr: WORD; //EtherCAT slave address
        sType: STRING; //EtherCAT slave type
        sName:STRING; //EtherCAT slave name
    END_STRUCT
    END_TYPE




ST_EcMasterDevState
^^^^^^^^^^^^^^^^^^^

::

    TYPE ST_EcMasterDevState :
    STRUCT
        eEcState	 		: E_EcCommState;
        nReserved			: ARRAY [0..2] OF UINT;

        bLinkError			: BOOL;
        bResetRequired		: BOOL;
        bMissFrmRedMode		: BOOL;
        bWatchdogTriggerd	: BOOL;
        bDriverNotFound		: BOOL;
        bResetActive		: BOOL;
        bAtLeastOneNotInOp	: BOOL;
        bDcNotInSync		: BOOL;
    END_STRUCT
    END_TYPE


Related:
    * `E_EcCommState`_


ST_EpicsMotorMSTA
^^^^^^^^^^^^^^^^^

::

    TYPE ST_EpicsMotorMSTA :
    STRUCT

        (* DIRECTION: last raw direction; (0:Negative, 1:Positive) *)
        bPositiveDirection : BIT;
        (* DONE: motion is complete. *)
        bDone : BIT;
        (* PLUS_LS: plus limit switch has been hit. *)
        bPlusLimitSwitch : BIT;
        (* HOMELS: state of the home limit switch. *)
        bHomeLimitSwitch : BIT;
        (* Unused *)
        bUnused0 : BIT;
        (* POSITION: closed-loop position control is enabled. *)
        bClosedLoop : BIT;
        (* SLIP_STALL: Slip/Stall detected (eg. fatal following error) *)
        bSlipStall : BIT;
        (* HOME: if at home position. *)
        bHome : BIT;
        (* PRESENT: encoder is present. *)
        bEncoderPresent : BIT;
        (* PROBLEM: driver stopped polling, or hardware problem *)
        bHardwareProblem : BIT;
        (* MOVING: non-zero velocity present. *)
        bMoving : BIT;
        (* GAIN_SUPPORT: motor supports closed-loop position control. *)
        bGainSupport : BIT;
        (* COMM_ERR: Controller communication error. *)
        bCommError : BIT;
        (* MINUS_LS: minus limit switch has been hit. *)
        bMinusLimitSwitch : BIT;
        (* HOMED: the motor has been homed. *)
        bHomed : BIT;

    END_STRUCT
    END_TYPE




ST_FbDiagnostics
^^^^^^^^^^^^^^^^

::

    //Stuff to log messages within function blocks
    TYPE ST_FbDiagnostics :
    STRUCT
        asResults	:	ARRAY [1..20] OF T_MaxString; //Diagnostic messages, use to record state changes or other important events.
        {attribute 'naming' := 'omit'}
        //Incrementer, included here to facilitate using asResults
        resultIdx	:	FB_Index := (
            LowerLimit := 1,
            UpperLimit := 20
        );
        {attribute 'naming' := 'omit'}
        fString	:	FB_FormatString; //Use to create good log messages, similar to C++ fstring
    END_STRUCT
    END_TYPE


Related:
    * `FB_Index`_


ST_LoggingEventInfo_WC
^^^^^^^^^^^^^^^^^^^^^^

::

    TYPE ST_LoggingEventInfo_WC :
    STRUCT
        (*
            Message or Alarm{Cleared,Confirmed,Raised} event information

            ** Working copy - to be used for the pinned ST_LoggingEventInfo **
            * The process for updating this type is as follows:
                1. Copy this structure and rename to ST_LoggingEventInfo
                2. Remove the working copy notes section
                3. Pin the global data type
            ** End of working copy information **

            Note that elements here do not follow the usual Hungarian notation /
            variable-type-prefixing naming convention due to the member names being
            used directly in the generation of the JSON document.
        *)

        {attribute 'pytmc' := '
            pv: Schema
            io: i
            field: DESC Schema string
        '}
        schema		:	STRING := 'twincat-event-0';

        {attribute 'pytmc' := '
            pv: Timestamp
            io: i
            field: DESC Unix timestamp
        '}
          ts			:	LREAL;

          {attribute 'pytmc' := '
            pv: Hostname
            io: i
            field: DESC PLC Hostname
        '}
          plc			:	STRING;

        {attribute 'pytmc' := '
            pv: Severity
            io: i
            field: DESC TcEventSeverity
            field: ZRST Verbose
            field: ONST Info
            field: TWST Warning
            field: THST Error
        '}
        severity	:	TcEventSeverity;

        {attribute 'pytmc' := '
            pv: MessageID
            io: i
            field: DESC TwinCAT Message ID
        '}
        id			:	UDINT;

        {attribute 'pytmc' := '
            pv: EventClass
            io: i
            field: DESC TwinCAT Event class
        '}
        event_class		:	STRING;

        {attribute 'pytmc' := '
            pv: Message
            io: i
        '}
        msg				: STRING(255);
        // This is actually: T_MaxString
        // which has been expanded due to requirements for pinning global data types.

        {attribute 'pytmc' := '
            pv: Source
            io: i
        '}
        source			: STRING(255);
        // This is actually: STRING(Tc3_EventLogger.ParameterList.cSourceNameSize - 1)
        // which has been expanded due to requirements for pinning global data types.

        {attribute 'pytmc' := '
            pv: EventType
            io: i
            field: DESC The event type
        '}
        event_type	:	 E_LogEventType;

        {attribute 'pytmc' := '
            pv: MessageJSON
            io: i
            field: DESC Metadata with the message
        '}
        json 		:	STRING(10000);
        (*
        NOTE: this JSON gets inserted as an escaped string in the "json" key.
        TODO: it may be possible to use `fbJson.AddRawObject`, but this would
              require us to switch back to creating the JSON in a more manual
              way with AddKey/AddInt (and such).
        *)

    END_STRUCT
    END_TYPE




ST_PendingEvent
^^^^^^^^^^^^^^^

::

    TYPE ST_PendingEvent :
    STRUCT

        {attribute 'pytmc' := '
            pv:
        '}
        stEventInfo			:	ST_LoggingEventInfo;
        bInUse				:	BOOL;
        fbRequestEventText	:	FB_RequestEventText;

    END_STRUCT
    END_TYPE




ST_SlaveState
^^^^^^^^^^^^^

::

    TYPE ST_SlaveState :
    STRUCT
        (* slave state *)
        eEcState 		: E_EcCommState;
        nReserved		: UINT;
        bError			: BOOL;
        bInvalidVPRS	: BOOL;
        nReserved2		: UINT;

        (* link state *)
        bNoCommToSlave	: BOOL;
        bLinkError		: BOOL;
        bMissingLink	: BOOL;
        bUnexpectedLink	: BOOL;
        bPortA			: BOOL;
        bPortB			: BOOL;
        bPortC			: BOOL;
        bPortD			: BOOL;
    END_STRUCT
    END_TYPE


Related:
    * `E_EcCommState`_


ST_SlaveStateInfo
^^^^^^^^^^^^^^^^^

::

    TYPE ST_SlaveStateInfo :
    STRUCT
        nIndex 			: DINT;
        sName 			: STRING; 		(* name of slave given in System Manager *)
        sType 			: STRING;		(* type of slave, e.g. EK1100*)
        nECAddr 		: UINT;			(* EtherCAT Slave Addr *)
        bDiagData		: BOOL;			(* DiagData in Slave State *)
        stPortCRCErrors	: ST_EcCrcErrorEx;(* Slave CRC-Errors, separate for each Port *)
        nSumCRCErrors	: UDINT;		(* Slave CRC-Errors, sum of all Ports *)
        stState 		: ST_SlaveState;(* EtherCAT State and Link state*)
    END_STRUCT
    END_TYPE


Related:
    * `ST_SlaveState`_


ST_SlaveStateInfoScanned
^^^^^^^^^^^^^^^^^^^^^^^^

::

    TYPE ST_SlaveStateInfoScanned :
    STRUCT
        nIndex 			: DINT;
        sName 			: STRING; 		(* name of slave given in System Manager *)
        sType 			: STRING;		(* type of slave, e.g. EK1100*)
        nECAddr 		: UINT;			(* EtherCAT Slave Addr *)
        bDifferentName	: BOOL;			(* Name of Scanned configuration differs from Configured configuration *)
        bDifferentType	: BOOL;			(* Type Of Scanned configuration differs from Configured configuration *)
        bDifferentAddr	: BOOL;			(* EcAddress of Scanned configuration differs from Configured configuration *)
    END_STRUCT
    END_TYPE




ST_System
^^^^^^^^^

::

    //Defacto system structure, must be included in all projects
    {attribute 'analysis' := '-33'}
    TYPE ST_System :
    STRUCT

        xSwAlmRst 		: BOOL;(* Global Alarm Reset - EPICS Command *)
        xAtVacuum 	    : BOOL;(* System At Vacuum *)
        xFirstScan	    : BOOL; (* This boolean is true for the first scan, and is false thereafter, use for initialization of stuff *)
        xOverrideMode	: BOOL; //This bit is set when using the override features of the system
        xIOState        : BOOL; (* ECat Bus Health *)

    END_STRUCT
    END_TYPE




ST_TopologyData
^^^^^^^^^^^^^^^

::

    TYPE ST_TopologyData :
    STRUCT
        iOwnPhysicalAddr 	: UINT;
        iOwnAutoIncAddr		: UINT;
        stPhysicalAddr		: ST_PortAddr;
        stAutoIncAddr		: ST_PortAddr;
        iPortDelay			: ARRAY [0..2] OF UDINT; (* EC_AD, EC_DB, EC_BC *)
        iReserved			: ARRAY [0..7] OF UDINT;
    END_STRUCT
    END_TYPE




GVLs
----


DefaultGlobals
^^^^^^^^^^^^^^

::

    //These are variables every PLC project should have
    {attribute 'analysis' := '-33'}
    VAR_GLOBAL

        stSys	:	ST_System; //Included for you
        fTimeStamp: LREAL;
    END_VAR


Related:
    * `ST_System`_


GeneralConstants
^^^^^^^^^^^^^^^^

::

    {attribute 'qualified_only'}
    {attribute 'analysis' := '-33'}
    VAR_GLOBAL CONSTANT
        // 16 including "Unknown" is the max for an EPICS MBBI/MBBO
        // This is the max number of user-defined states (OUT, TARGET1, YAG...)
        // You can make this smaller if you want to use less memory in your program in exchange for limiting your max state count
        // You can make this larger if you want to use states-based FBs sized beyond the EPICS enum limit
        MAX_STATES: UINT := 15;
    END_VAR




Global_Variables_EtherCAT
^^^^^^^^^^^^^^^^^^^^^^^^^

::

    {attribute 'analysis' := '-33'}
    VAR_GLOBAL CONSTANT
        iSLAVEADDR_ARR_SIZE	: UINT := 256;
        ESC_MAX_PORTS : UINT := 3; // Maximum number of ports (4) on ESC
    END_VAR




Global_Version
^^^^^^^^^^^^^^

::

    {attribute 'TcGenerated'}
    // This function has been automatically generated from the project information.
    VAR_GLOBAL CONSTANT
        {attribute 'const_non_replaced'}
        {attribute 'linkalways'}
        stLibVersion_LCLS_General : ST_LibVersion := (iMajor := 2, iMinor := 8, iBuild := 2, iRevision := 0, sVersion := '2.8.2');
    END_VAR




GVL_Logger
^^^^^^^^^^

::

    {attribute 'qualified only'}
    //Global variables for logging to syslog
    VAR_GLOBAL CONSTANT
        (*
        Using the IP address directly avoids DNS configuration issues.
        While we may want to address this in the future, for now the static IP
        will suffice:

        $ nslookup ctl-logsrv01
        Name:   ctl-logsrv01.pcdsn
        Address: 172.21.32.36
        *)
        {attribute 'pytmc' := '
            pv: @(PREFIX)LCLSGeneral:LogHost
            io: io
            field: DESC The log host IP address
        '}
        cLogHost		:	STRING(15)	:= '172.21.32.36';

        {attribute 'pytmc' := '
            pv: @(PREFIX)LCLSGeneral:LogPort
            io: io
            field: DESC The log host UDP port
        '}
        iLogPort		:	UINT		:= 54321;

        sIpTidbit : STRING(6) := '172.21';

        // Log message circuit breaker configuration

        // Initialization constants for circuit breakers
        nLocalTripThreshold : TIME := T#1ms; // Minimum time between log messages
        nMinTimeViolationAcceptable : INT := 5; // Trip if `nLocalTripThreshold` exceeded `nMinTimeViolationAcceptable` times
        nLocalTrickleTripThreshold : TIME := T#100ms; // Default trickle trip, activated by global threshold
        nTrickleTripTime : TIME := T#10s; // Default time for log-handler to recognize a trickle overload condition, many log-message FB occasionally creating a message
        // such that every PLC cycle is emitting a message (this is considered to be too much).
        nTripResetPeriod : TIME := T#10m; // Default time for CB auto-reset
    END_VAR

    VAR_GLOBAL
        sPlcHostname	:	STRING		:= 'unknown';

        (* Ref: https://infosys.beckhoff.com/english.php?content=../content/1033/tcpipserver/html/TcPlcLibTcpIp_FB_SocketUdpSendTo.htm
            TODO: Activate the "Replace constants" option in the
            TwinCAT PLC Control->"Project->Options...->Build" dialog window.
        *)
        {attribute 'analysis' := '-33'}
        TCPADS_MAXUDP_BUFFSIZE : UDINT :=10000;

        {analysis -33}
        fbRootLogger : FB_LogMessage; //Instantiated here to be used everywhere
        {analysis +33}

        {attribute 'pytmc' := '
            pv: @(PREFIX)LCLSGeneral:GlobalLogTrickleTrip
            io: i
            field: DESC Tripped by overall log count
        '}
        bTrickleTripped :   BOOL; // Global trickle trip flag

        {attribute 'pytmc' := '
            pv: @(PREFIX)LCLSGeneral:LogMessageCount
            io: i
            field: DESC Total log messages on the last cycle
        '}
        nGlobAccEvents : UDINT; // Global log message count
    END_VAR


Related:
    * `FB_LogMessage`_


POUs
----


F_ConvertTicksToUnixTimestamp
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    FUNCTION F_ConvertTicksToUnixTimestamp : LREAL
    VAR_INPUT
        nTimestamp : ULINT;
    END_VAR
    VAR CONSTANT
        // Timer ticks in Windows are 100ns (1e-7 sec)
        nTicksToSeconds : LREAL := 10_000_000;
        // Epoch offset 1601 to 1970
        nEpochOffset	: LREAL := 11_644_473_600;
    END_VAR
    F_ConvertTicksToUnixTimestamp := ULINT_TO_LREAL(nTimestamp) / nTicksToSeconds - nEpochOffset;

    END_FUNCTION




F_SendUDPMessage
^^^^^^^^^^^^^^^^

::

    FUNCTION F_SendUDPMessage : HRESULT
    VAR_INPUT
        sMessage			:	POINTER TO STRING;
        fbSocket			:	REFERENCE TO FB_ConnectionlessSocket;
        sHost				:	STRING;
        iPort				:	UINT;
    END_VAR

    VAR
        fbSend				:	FB_SocketUdpSendTo;
    END_VAR
    IF sMessage <> 0 AND __ISVALIDREF(fbSocket) THEN
        fbSend.hSocket		:= fbSocket.hSocket;
        fbSend.sRemoteHost	:= sHost;
        fbSend.nRemotePort	:= iPort;
        fbSend.pSrc 		:= sMessage;
        fbSend.cbLen		:= LEN2(sMessage);
        fbSend.bExecute	 	:= TRUE;
        fbSend();

        fbSend.bExecute 	R= fbSend.bBusy;
    END_IF

    END_FUNCTION




FB_AnalogInput
^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_AnalogInput
    (*
        Converts the integer from an analog input terminal to a real unit value (e.g., volts)
        2019-10-09 Zachary Lentz
    *)
    VAR_INPUT
        // Connect this input to the terminal
        iRaw AT %I*: INT;
        // The number of bits correlated with the terminal's max value. This is not necessarily the resolution parameter.
        iTermBits: UINT;
        // The fReal value correlated with the terminal's max value
        fTermMax: LREAL;
        // The fReal value correlated with the terminal's min value
        fTermMin: LREAL;
    END_VAR
    VAR_OUTPUT
        // The real value read from the output
        fReal: LREAL;
    END_VAR
    VAR
        fScale: LREAL;
    END_VAR
    IF fScale = 0 AND fTermMax > fTermMin THEN
        fScale := (EXPT(2, iTermBits) - 1) / (fTermMax - fTermMin);
    END_IF
    IF fScale <> 0 THEN
        fReal := iRaw / fScale + fTermMin;
    END_IF

    END_FUNCTION_BLOCK




FB_AnalogOutput
^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_AnalogOutput
    (*
        Converts a real unit value (e.g., volts) to the integer needed for an analog output terminal.
        2019-10-09 Zachary Lentz
    *)
    VAR_INPUT
        // The real value to send to the output
        fReal: LREAL;
        // The maximum allowed real value for the connected hardware
        fSafeMax: LREAL;
        // The minimum allowed real value for the connected hardware
        fSafeMin: LREAL;
        // The number of bits correlated with the terminal's max output. This is not necessarily the resolution parameter.
        iTermBits: UINT;
        // The fReal value correlated with the terminal's max output
        fTermMax: LREAL;
        // The fReal value correlated with the terminal's min output
        fTermMin: LREAL;
    END_VAR
    VAR_OUTPUT
        // Connect this output to the terminal
        iRaw AT %Q*: INT;
    END_VAR
    VAR
        fScale: LREAL;
    END_VAR
    // Set the scaling from real to raw
    IF fScale = 0 AND fTermMax > fTermMin THEN
        fScale := (EXPT(2, iTermBits) - 1) / (fTermMax - fTermMin);
    END_IF

    // Adjust real value to be within the limits
    fReal := MIN(fReal, fSafeMax, fTermMax);
    fReal := MAX(fReal, fSafeMin, fTermMin);

    // Scale the output accordingly
    iRaw := LREAL_TO_INT((fReal - fTermMin) * fScale);

    END_FUNCTION_BLOCK




FB_BasicStats
^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_BasicStats
    (*
        Minimalist Array Stats for LREALs
        2019-10-10 Zachary Lentz

        Calculates the most basic stats for an array and provides pytmc control points.
        This is an alternative to the TC3 Condition Monitoring library which requires an
        additional license and had a more complicated interface.
    *)
    VAR_IN_OUT
        // Input array of floats
        {attribute 'pytmc' := '
            pv: STATS:DATA
            io: i
        '}
        aSignal: ARRAY[*] OF LREAL;
    END_VAR
    VAR_INPUT
        // If TRUE, we will update the results every cycle
        {attribute 'pytmc' := 'pv: STATS:ALWAYS_CALC'}
        bAlwaysCalc: BOOL;
        // On rising edge, do one calculation
        {attribute 'pytmc' := 'pv: STATS:EXECUTE'}
        bExecute: BOOL;
        // If set to TRUE, reset outputs
        {attribute 'pytmc' := 'pv: STATS:RESET'}
        bReset: BOOL;
        // If nonzero, we will only pay attention to the first nElems items in aSignal
        {attribute 'pytmc' := '
            pv: STATS:NELM
            io: i
        '}
        nElems: UDINT;
    END_VAR
    VAR_OUTPUT
        // Average of all values in the array
        {attribute 'pytmc' := '
            pv: STATS:MEAN
            io: i
        '}
        fMean: LREAL;
        // Standard deviation of all values in the array
        {attribute 'pytmc' := '
            pv: STATS:STDEV
            io: i
        '}
        fStDev: LREAL;
        // Largest value in the array
        {attribute 'pytmc' := '
            pv: STATS:MAX
            io: i
        '}
        fMax: LREAL;
        // Smallest value in the array
        {attribute 'pytmc' := '
            pv: STATS:MIN
            io: i
        '}
        fMin: LREAL;
        // Largest array element subtracted by the smallest
        {attribute 'pytmc' := '
            pv: STATS:RANGE
            io: i
        '}
        fRange: LREAL;
        // True if the other outputs are valid
        {attribute 'pytmc' := '
            pv: STATS:VALID
            io: i
        '}
        bValid: BOOL;
    END_VAR
    VAR
        rTrig: R_TRIG;
        nIndex: DINT;
        nElemsSeen: UDINT;
        fSum: LREAL;
        fVarianceSum: LREAL;
        fVarianceMean: LREAL;
    END_VAR
    rTrig(CLK:=bExecute);
    IF bReset THEN
        fMean := 0;
        fStDev := 0;
        fMax := 0;
        fMin := 0;
        fRange := 0;
        bValid := FALSE;
        bReset := FALSE;
    ELSIF NOT (bExecute OR bAlwaysCalc) THEN
        bValid := FALSE;
    ELSIF bAlwaysCalc OR rTrig.Q THEN
        // First pass through aSignal: get sum, mean, max, min
        nElemsSeen := 0;
        fSum := 0;
        fMax := aSignal[LOWER_BOUND(aSignal, 1)];
        fMin := fMax;
        FOR nIndex := LOWER_BOUND(aSignal, 1) TO UPPER_BOUND(aSignal, 1) DO
            nElemsSeen := nElemsSeen + 1;
            fSum := fSum + aSignal[nIndex];
            IF aSignal[nIndex] > fMax THEN
                fMax := aSignal[nIndex];
            ELSIF aSignal[nIndex] < fMin tHEN
                fMin := aSignal[nIndex];
            END_IF
            IF nElems > 0 AND nElemsSeen >= nElems THEN
                EXIT;
            END_IF
        END_FOR
        IF nElemsSeen > 0 THEN
            fMean := fSum / nElemsSeen;
            fRange := fMax - fMin;

            // Second pass through aSignal: get the sum of the variances and then the stdev
            nElemsSeen := 0;
            fVarianceSum := 0;
            FOR nIndex := LOWER_BOUND(aSignal, 1) TO UPPER_BOUND(aSignal, 1) DO
                nElemsSeen := nElemsSeen + 1;
                fVarianceSum := fVarianceSum + (aSignal[nIndex] - fMean) * (aSignal[nIndex] - fMean);
                IF nElems > 0 AND nElemsSeen >= nElems THEN
                    EXIT;
                END_IF
            END_FOR
            IF nElemsSeen > 1 THEN
                fVarianceMean := fVarianceSum / (nElemsSeen - 1);
                fStDev := SQRT(fVarianceMean);
                bValid := TRUE;
            END_IF
        END_IF
    END_IF

    END_FUNCTION_BLOCK




FB_CircuitBreaker_Test
^^^^^^^^^^^^^^^^^^^^^^

::

    {attribute 'call_after_init'}
    FUNCTION_BLOCK FB_CircuitBreaker_Test EXTENDS TcUnit.FB_TestSuite
    VAR_INPUT
    END_VAR
    VAR_OUTPUT
    END_VAR
    VAR
    END_VAR
    AutoReset();
    SingleBadLogger();
    DeathByManySmall();

    END_FUNCTION_BLOCK

    METHOD AutoReset
    VAR_INPUT
    END_VAR
    VAR_INST
        fbLog : FB_LogMessage := (
            bEnableAutoReset:=TRUE,
            nTripResetPeriod := T#5s);

        //Auto reset test
        tonAutoResetTest : TON;

        {attribute 'analysis' := '-27'}
        Init : BOOL := TRUE;
    END_VAR
    (* Test that the CB resets itself after a cooldown period *)
    TEST('AutoReset');
    IF Init THEN
        fbLog.CircuitBreaker();
        WRITE_PROTECTED_BOOL(ADR(fbLog.bTripped), TRUE);
        WRITE_PROTECTED_BOOL(ADR(fbLog.bLocalTripped), TRUE);
        fbLog.CircuitBreaker();
        fbLog.CircuitBreaker();
        Init := FALSE;
    END_IF

    tonAutoResetTest(IN:=NOT Init, PT:=T#6s);

    IF tonAutoResetTest.Q THEN

        fbLog.CircuitBreaker();

        AssertFalse(fbLog.bTripped,
            'Circuit breaker should be reset automatically');

        TEST_FINISHED_NAMED('AutoReset');
    END_IF
    END_METHOD

    METHOD DeathByManySmall
    VAR_INPUT
    END_VAR
    VAR

    END_VAR
    VAR_INST
        fbLogNoisy : FB_LogMessage;
        tNoisy : TON := (PT:=T#50ms); //50ms is > the local trip threshold default of 1ms
        fbLogNice : FB_LogMessage := (nTrickleTripThreshold:=T#2s);
        tNice : TON := (PT:=T#5s);
        tTrickle : TON := (PT:= GVL_Logger.nTrickleTripTime + T#1s);
        fbLogHandler : FB_LogHandler;
    END_VAR
    TEST('ManySmall');
    // Create a condition where
    // a few loggers did their thing, while keeping under the
    // local logging rate limit until the global trickle trip
    // was triggered. Then verify only those loggers that
    // were just a little too noisy, would trip off, while others stayed up.

    fbLogHandler.CircuitBreaker();

    // Call this guy every 50ms or so.
    tNoisy(IN := NOT tNoisy.Q);
    IF tNoisy.Q THEN
        fbLogNoisy.CircuitBreaker();
    END_IF

    // Call this guy every 5s or so
    tNice(IN := NOT tNoisy.Q);
    IF tNice.Q THEN
        fbLogNice.CircuitBreaker();
    END_IF

    tTrickle(IN:=TRUE);
    IF tTrickle.Q THEN
        AssertTrue(fbLogNoisy.bLocalTrickleTripped AND NOT fbLogNice.bLocalTrickleTripped,
                'Only Noisy should be tripped.');
        TEST_FINISHED_NAMED('ManySmall');
    END_IF
    END_METHOD

    METHOD SingleBadLogger
    VAR_INPUT
    END_VAR
    VAR
        idx : UINT;
    END_VAR
    VAR_INST
        fbLog : FB_LogMessage := (
            bEnableAutoReset:=TRUE,
            nTripResetPeriod := T#5s);
    END_VAR
    ResetCircuitBreakerGlobals();

    (* In this scenario, a logger trips off because it has been called too many times
    in one cycle, leading to a large excess of messages.
    *)
    TEST('LocalFastTrip');
        FOR idx := 0 TO GVL_LOGGER.nMinTimeViolationAcceptable + 1 DO
            fbLog.CircuitBreaker();
        END_FOR

        AssertTrue(fbLog.bLocalTripped AND NOT fbLog.bLocalTrickleTripped,
            'Only local trip should occur in these conditions');
    TEST_FINISHED();
    END_METHOD


Related:
    * `FB_LogHandler`_
    * `FB_LogMessage`_
    * `GVL_Logger`_
    * `ResetCircuitBreakerGlobals`_


FB_CoE_FastRead
^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_CoE_FastRead
    (*
        Utility to repeatedly read a CoE parameter
        2019-10-09 Zachary Lentz

        In practice, it's impossible to read most CoE parameters every cycle,
        but this is a best effort and will work if the data is available
    *)
    VAR_INPUT
        // If TRUE we'll attempt a CoE read this cycle.
        bExecute: BOOL;
        // Link this to your terminal's drive reference variables under InfoData.
        stPlcDriveRef AT %I*: ST_PlcDriveRef;
        // Hexadecimal index of CoE, e.g. the 8010 in 8010:12
        nIndex: UINT;
        // Hexadecimal subindex of CoE, e.g. the 12 in 8010:12
        nSubIndex: BYTE;
        // Pointer to a value to fill with the result of the read, e.g. ADR(MyValue)
        pDstBuf: PVOID;
        // Data size of pDstBuf, e.g. SIZEOF(MyValue)
        cbBufLen: UINT;
    END_VAR
    VAR_OUTPUT
        // TRUE if the value was updated on this cycle.
        bNewValue: BOOL;
    END_VAR
    VAR
        fbRead: FB_CoERead_ByDriveRef;
        stDriveRef: ST_DriveRef;
        iLoop: INT;
        bInnerExec: BOOL;
    END_VAR
    stDriveRef.sNetId := F_CreateAmsNetId(stPlcDriveRef.aNetId);
    stDriveRef.nSlaveAddr := stPlcDriveRef.nSlaveAddr;
    stDriveRef.nDriveNo := stPlcDriveRef.nDriveNo;
    stDriveRef.nDriveType := stPlcDriveRef.nDriveType;

    bNewValue := FALSE;
    IF bExecute THEN
        // You need to do this block 3 times per cycle to have a chance at always getting a read
        FOR iLoop:= 1 TO 3 DO
            fbRead(
                stDriveRef := stDriveRef,
                nIndex := nIndex,
                nSubIndex := nSubIndex,
                pDstBuf := pDstBuf,
                cbBufLen := cbBufLen,
                bExecute := bInnerExec,
                tTimeout := T#1s);

            IF bInnerExec AND NOT fbRead.bBusy AND NOT fbRead.bError THEN
                bInnerExec := FALSE;
                bNewValue := TRUE;
            ELSE
                bInnerExec := TRUE;
            END_IF
        END_FOR
    END_IF

    END_FUNCTION_BLOCK




FB_DataBuffer
^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_DataBuffer
    (*
        Function Block to accumulate data into an array.
        2019-10-09 Zachary Lentz

        Requires the user to supply pointers to the value and to 2 arrays:
        1. A partial buffer that we will slowly fill one value at a time
        2. An output buffer that will only update when the partial buffer is full

        Take great care of the following, or else your program will likely crash,
        or at least have corrupt data:
        1. The input type and array types must match
        2. The provided element count must be accurate and match both arrays
        3. The provided element size is correct

        As this function block as no way of checking that you did this correctly.
    *)
    VAR_INPUT
        // Whether or not to accumulate on this cycle
        bExecute: BOOL;
        // Address of the value to accumulate
        pInputAdr: PVOID;
        // Size of the accumulated value
        iInputSize: UDINT;
        // Number of values in the output array
        iElemCount: UDINT;
        // Address of the rolling buffer to be filled every cycle
        pPartialAdr: PVOID;
        // Address of the output buffer to be filled when the rolling buffer is full
        pOutputAdr: PVOID;
    END_VAR
    VAR_OUTPUT
        // Set to TRUE on the cycle that we copy the output array
        bNewArray: BOOL;
    END_VAR
    VAR
        iArrayIndex: UDINT := 0;
    END_VAR
    bNewArray := FALSE;
    IF bExecute THEN
        MEMCPY(
            destAddr := pPartialAdr + iArrayIndex*iInputSize,
            srcAddr := pInputAdr,
            n := iInputSize);
        iArrayIndex := iArrayIndex + 1;
        IF iArrayIndex >= iElemCount THEN
            MEMCPY(
                destAddr := pOutputAdr,
                srcAddr := pPartialAdr,
                n := iElemCount*iInputSize);
            iArrayIndex := 0;
            bNewArray := TRUE;
        END_IF
    END_IF

    END_FUNCTION_BLOCK




FB_EcatDiag
^^^^^^^^^^^

::

    (*
    Ecat bus diagnostic tool
    2015-11-4 Alex Wallace
    This function block checks the states of all slaves on the ecat bus network,
    it could be modified to export the states of the slaves on an individual basis,
    but for now it sets the output boolean true if all slaves are OP and false otherwise.
    To start the block provide a falling edge on the first pass boolean input.

    2018-05-05 Margaret Ghaly
    Function block has been modified to retrieve the Device State of the Ethercat Master.
    It also exports the states and information of each individual configured Slave.
    And saves them in the array q_aEcConfSlaveInfo.
    *)
    FUNCTION_BLOCK FB_EcatDiag
    VAR_INPUT
        {attribute 'naming' := 'omit'}
        I_AMSNetId AT %I* : AMSNETID; //Link to the AMSNETID name in the ethercat master info.
        i_xFirstPass: BOOL; //Hook to system first pass boolean for proper intialization (must be true for the first cycle of the PLC)
    END_VAR
    VAR_OUTPUT
        q_xAllSlaveStatesGood: BOOL; // Set to True if all Slaves are in OP State
        q_anTermStates: ARRAY[1..256] OF BYTE; //ECAT State of terminals in the bus
        q_xMasterStateGood:BOOL; // Set to True if the Master Device State is OP
        q_nMasterState: WORD; // The Device State of the Master
        q_sMasterState:STRING; //State of the ECAT master
        q_astEcConfSlaveInfo :  ARRAY[1..256] OF ST_EcDevice; //State of all ECAT slaves in the bus
        q_nSlaves: UINT; // the Number of the connected Slaves
    END_VAR
    VAR
        sNetId: T_AmsNetId; //NetId string
        astTermStates: ARRAY[1..256] OF ST_EcSlaveState; //ECAT Slave States Buffer
        astEcConfSlaveInfo: ARRAY[1..256] OF ST_EcSlaveConfigData; //ECAT Slave Configs Buffer
        fbGetAllSlaveStates: FB_EcGetAllSlaveStates; //Acquires the ECAT Slave States puts them into astTermStates

        fbGetMasterState: FB_EcGetMasterState; //Acquires ECAT Master State
        fbGetConfSlaves: FB_EcGetConfSlaves; //Acquires the ECAT slave configuration of the bus (how many, what kind, etc)
        {attribute 'naming' := 'omit'}
        ftReset: F_TRIG; //Reset trigger sensor
        {attribute 'naming' := 'omit'}
        ftMasterReset: F_TRIG; //Retrigger sensor for GetMasterState
        nIterator: INT; //Generic iterator placeholder
    END_VAR
    //Create the net ID string
    sNetId := F_CreateAmsNetId(I_AMSNetId);

    //Query the state of all terminals, collect in astTermStates
    ftReset(CLK:=fbGetAllSlaveStates.bBusy OR i_xFirstPass);
    fbGetAllSlaveStates.bExecute := ftReset.Q;
    fbGetAllSlaveStates(sNetId:=sNetId, pStateBuf := ADR(astTermStates), cbBufLen:=SIZEOF(astTermStates));
    //Keep checking...



    //Cycle through each entry in the array and check if we have anyone not in OP and that the link state is good.
    // If so, then set our global IO bad boolean.
    IF fbGetAllSlaveStates.nSlaves > 0 THEN
        q_xAllSlaveStatesGood := TRUE;
    FOR nIterator := 1 TO (UINT_TO_INT(fbGetAllSlaveStates.nSlaves) ) BY 1
        DO
        IF NOT( (astTermStates[nIterator].deviceState = EC_DEVICE_STATE_OP) AND (astTermStates[nIterator].linkState = EC_LINK_STATE_OK)) THEN
            q_xAllSlaveStatesGood := FALSE;
        END_IF
        q_anTermStates[nIterator] := astTermStates[nIterator].deviceState;
        q_astEcConfSlaveInfo[nIterator].nDeviceState :=astTermStates[nIterator].deviceState;//
        q_astEcConfSlaveInfo[nIterator].nLinkState :=astTermStates[nIterator].linkState;//
        q_astEcConfSlaveInfo[nIterator].sDeviceState:= F_ConvSlaveStateToString(state:=astTermStates[nIterator]);//

    END_FOR
    END_IF

    // Read the EtherCAT state of the master. If the call is successful,
    //the State output variable of type WORD contains the requested status information.
    ftMasterReset(CLK:=fbGetMasterState.bBusy OR i_xFirstPass);
    fbGetMasterState(sNetId:= sNetId, bExecute:=ftMasterReset.Q,
                    state => q_nMasterState,bError=>,
                    nErrId=>);
    q_xMasterStateGood:= (fbGetMasterState.state = BYTE_TO_UINT(EC_DEVICE_STATE_OP));
    q_sMasterState := F_ConvMasterDevStateToString(fbGetMasterState.state);

    //This function is used to read a list of all configured slaves from the EtherCat master object Directory
    //needs to run only once
    fbGetConfSlaves(bExecute := i_xFirstPass, sNetId :=sNetId, pArrEcConfSlaveInfo := ADR(astEcConfSlaveInfo),cbBufLen := SIZEOF(astEcConfSlaveInfo));
    q_nSlaves:=fbGetConfSlaves.nSlaves;

    IF  NOT (fbGetConfSlaves.bBusy) THEN
        FOR nIterator := 1 TO (UINT_TO_INT(fbGetConfSlaves.nSlaves) ) BY 1
        DO
        q_astEcConfSlaveInfo[nIterator].nAddrr :=astEcConfSlaveInfo[nIterator].nAddr;
        q_astEcConfSlaveInfo[nIterator].sName :=astEcConfSlaveInfo[nIterator].sName;
        q_astEcConfSlaveInfo[nIterator].sType :=astEcConfSlaveInfo[nIterator].sType;
    END_FOR
        fbGetConfSlaves.bExecute := FALSE;
    END_IF

    END_FUNCTION_BLOCK


Related:
    * `ST_EcDevice`_


FB_EcatDiagWrapper
^^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_EcatDiagWrapper
    VAR_INPUT
    END_VAR
    VAR_OUTPUT
        bAllFrameWcStatesOK		: BOOL;				// all frames are OK
        bEtherCATOK				: BOOL;				// no problem on EtherCAT
        bFrameWcStateError		: BOOL;				// at least one fram with error
        bSlaveCountError		: BOOL;				// EtherCAT slave count mismatch (# of cfg slaves <> # of found slaves)
        bMasterDevStateError	: BOOL;				// EtherCAT master device state signals error
        stMasterDevState		: ST_EcMasterDevState; // device state split to a structure
        bBusy					: BOOL;				// diagnostic FB is busy
        bError					: BOOL;				// diagnostic FB has an error
        iErrorID				: UDINT;			// error ID of diagnostic FB
    END_VAR
    VAR

        (* ******************* EtherCAT Frame ***************************** *)
        fbEtherCATFrameDiag		: FB_EtherCATFrameDiag; // frame diagnostic
        {attribute 'TcLinkTo' := 'TIID^Device 1 (EtherCAT)^Inputs^Frm1WcState'}
        wFrmXWcState AT %I*		: WORD; 			// link to task related ethercat frame state (Frm1WcState)
        wReqZeroMask			: WORD := 16#FFFF; 	// clear bit to ignore datagram error of Frm1WcState
        bFrameWcStateOK			: BOOL;				// this frame is OK

        (* ******************* EtherCAT Diag ***************************** *)
        fbEtherCATDiag			: FB_EtherCATDiag;	// deep EtherCAT diagnostic

        (* cyclic variables from EtherCAT Master *)
        {attribute 'TcLinkTo' := 'TIID^Device 1 (EtherCAT)^Inputs^SlaveCount'}
        nEcMasterSlaveCount AT %I* 	: UINT;			// link to SlaveCount of EtherCAT Master (Inputs)
        {attribute 'TcLinkTo' := 'TIID^Device 1 (EtherCAT)^Inputs^DevState'}
        nEcMasterDevState AT %I* 	: UINT;			// link to DevState of EtherCAT Master (Inputs)
        {attribute 'TcLinkTo' := 'TIID^Device 1 (EtherCAT)^InfoData^DevId'}
        nEcMasterDeviceId AT %I*	: UINT;			// link to DevID of EtherCAT Master (InfoData)
        {attribute 'TcLinkTo' := 'TIID^Device 1 (EtherCAT)^InfoData^AmsNetId'}
        arrEcMasterNetId AT %I* 	: T_AmsNetIdArr;// link to NetID of EtherCAT Master (InfoData)
        sEcMasterNetId  			: T_AmsNetId := '';
        {attribute 'TcLinkTo' := 'TIID^Device 1 (EtherCAT)^InfoData^CfgSlaveCount'}
        nEcMasterSlaveCountCfg AT %I* 	: UINT;		// link to CfgSlaveCount of EtherCAT Master (InfoData)

        (* general variables *)
        arrDiagSlaveInfo 			: ARRAY [0..ESC_MAX_PORTS] OF ST_SlaveStateInfo;		// read in info of configured EtherCAT slaves
        arrDiagSlaveInfoScanned		: ARRAY [0..ESC_MAX_PORTS] OF ST_SlaveStateInfoScanned; // read in info of scanned EtherCAT slaves
    END_VAR
    (*************************************** Frame Diag *********************************************)
    fbEtherCATFrameDiag(
        wFrmXWcState 	:= wFrmXWcState,
        wReqZeroMask 	:= wReqZeroMask,
        bFrameWcStateOK	=> bFrameWcStateOK
    );
    bAllFrameWcStatesOK := bFrameWcStateOK;

    (*************************************** EtherCAT Diag *********************************************)
    (* generate Net Id *)
    sEcMasterNetId := F_CreateAmsNetId(nIds := arrEcMasterNetId);
    fbEtherCATDiag(
        sIPCNetID			:= '',
        sMasterNetID		:= sEcMasterNetId,
        nMasterDevID		:= nEcMasterDeviceId,
        nSlaveCount			:= nEcMasterSlaveCount,
        nSlaveCountCfg		:= nEcMasterSlaveCountCfg,
        nMasterDevState		:= nEcMasterDevState,
        bAllFrameWcStatesOK	:= bAllFrameWcStatesOK,
        tTimeout			:= T#5s,
        arrDiagSlaveInfo	:= arrDiagSlaveInfo,
        arrDiagSlaveInfoScanned	:= arrDiagSlaveInfoScanned,
        bEtherCATOK			=> bEtherCATOK,
        bFrameWcStateError	=> bFrameWcStateError,
        bSlaveCountError	=> bSlaveCountError,
        bMasterDevStateError=> bMasterDevStateError,
        stMasterDevState	=> stMasterDevState,
        bBusy				=> bBusy,
        bError				=> bError,
        iErrorID			=> iErrorID
    );

    END_FUNCTION_BLOCK


Related:
    * `FB_EtherCATDiag`_
    * `FB_EtherCATFrameDiag`_
    * `ST_EcMasterDevState`_
    * `ST_SlaveStateInfo`_
    * `ST_SlaveStateInfoScanned`_


FB_EL6_Com
^^^^^^^^^^

::

    FUNCTION_BLOCK FB_EL6_Com
    (*
        Communicate with a serial device connected to an EL6XXX
        2019-10-09 Zachary Lentz and Jackson Sheppard

        May contain assumptions about the device we wrote it for, potentially will need to be adjusted
    *)
    VAR_INPUT
        // Command to send to the serial device
        {attribute 'pytmc' := '
            pv: CMD
            io: io
        '}
        sCmd: STRING;

        // Pulse this to TRUE and back to FALSE when it's time to send
        {attribute 'pytmc' := '
            pv: SEND
            io: io
        '}
        bSend: BOOL;

        // Any static prefix to add before every sent message
        sSendPrefix: STRING;
        // Any static suffix to add after every sent message
        sSendSuffix: STRING;
        // Any static prefix to strip off of every recieved message
        sRecvPrefix: STRING;
        // Any static suffic to strip off of every recieved message
        sRecvSuffix: STRING;
        tTimeout: TIME := T#1S;
    END_VAR
    VAR_IN_OUT
        stIn_EL6: EL6inData22B;
        stOut_EL6: EL6outData22B;
    END_VAR
    VAR_OUTPUT
        // The response recieved from the serial device
        {attribute 'pytmc' := '
            pv: RESP
            io: input
        '}
        sResponse: STRING;

        // This is set to TRUE after recieving a response
        {attribute 'pytmc' := '
            pv: DONE
            io: input
        '}
        bDone: BOOL;

        {attribute 'pytmc' := '
            pv: ERR:SER
            io: input
        '}
        eSerialLineErrorID: ComError_t;

        {attribute 'pytmc' := '
            pv: ERR:SEND
            io: input
        '}
        eSendErrorID: ComError_t;

        {attribute 'pytmc' := '
            pv: ERR:RECV
            io: input
        '}
        eRecvErrorID: ComError_t;
    END_VAR
    VAR
        // Communication Buffers
        TxBuffer: ComBuffer;
        RxBuffer: ComBuffer;
        fbClearComBuffer: ClearComBuffer;

        // Parameters for PLC -> EL6
        fbEL6Ctrl: SerialLineControl;
        bEL6CtrlError: BOOL;
        eEL6CtrlErrorID: ComError_t;

        // Parameters for EL6 -> Serial Device
        fbSend: SendString;
        bSendBusy: BOOL;
        eLastSendErrorID: ComError_t;
        fbReceive: ReceiveString;
        sReceivedString: STRING;
        sLastReceivedString: STRING;
        bStringReceived: BOOL;
        bReceiveBusy: BOOL;
        bReceiveError: BOOL;
        eReceiveErrorID: ComError_t;
        bReceiveTimeout: BOOL;
        nReceiveCounter: UDINT;
        nSendCounter: UDINT;
        sStringToSend: STRING;
        fbFormatString: FB_FormatString;

        // Parameters for state-machine implementation
        nStep: INT := 0;
    END_VAR
    fbEL6Ctrl(
        Mode:= SERIALLINEMODE_EL6_22B,
        pComIn:= ADR(stIn_EL6),
        pComOut:= ADR(stOut_EL6),
        SizeComIn:= UINT_TO_INT(SIZEOF(stIn_EL6)),
        Error=> ,
        ErrorID=> eSerialLineErrorID,
        TxBuffer:= TxBuffer,
        RxBuffer:= RxBuffer );
    IF fbEL6Ctrl.Error THEN
        bEL6CtrlError := TRUE;
        eEL6CtrlErrorID := fbEL6Ctrl.ErrorID;
    END_IF
    IF bSend THEN
        nStep := 10;
        bSend := FALSE;
        bDone := FALSE;
    END_IF
    // Attempt at solution that sends one command at a time, not on constant loop
    CASE nStep OF
        0:
            ; // idle
        10:
            // Clear buffers in case any lingering data
            fbClearComBuffer(Buffer:=TxBuffer);
            fbClearComBuffer(Buffer:=RxBuffer);
            // Prepare string to send
            sStringToSend := CONCAT(sSendPrefix, CONCAT(sCmd, sSendSuffix));
            // Send string
            fbSend(	SendString:= sStringToSend,
                    TXbuffer:= TxBuffer,
                    Busy=> bSendBusy,
                    Error=> eSendErrorID);
            IF fbSend.Error <> COMERROR_NOERROR THEN
                eLastSendErrorID := fbSend.Error;
            ELSE
                nSendCounter := nSendCounter + 1;
            END_IF
            nStep := nStep + 10;
        20:
            // Finish sending String
            IF fbSend.Busy THEN
                fbSend(	SendString:= sStringToSend,
                        TXbuffer:= TxBuffer,
                        Busy=> bSendBusy,
                        Error=> eSendErrorID);
                IF fbSend.Error <> COMERROR_NOERROR THEN
                    eLastSendErrorID := fbSend.Error;
                ELSE
                    nSendCounter := nSendCounter + 1;
                END_IF
            ELSE
                nStep := nStep + 10;
            END_IF
        30:
            // Get Reply
            fbReceive(
                Prefix:= sRecvPrefix,
                Suffix:= sRecvSuffix,
                Timeout:= tTimeout,
                ReceivedString:= sReceivedString,
                RXbuffer:= RxBuffer,
                StringReceived=> bStringReceived,
                Busy=> bReceiveBusy,
                Error=> eRecvErrorID,
                RxTimeout=> bReceiveTimeout );
            IF fbReceive.Error <> COMERROR_NOERROR THEN
                eReceiveErrorID := fbReceive.Error;
            END_IF
            IF bStringReceived THEN
                nReceiveCounter := nReceiveCounter + 1;
                // Check for response
                IF FIND(sReceivedString, sStringToSend)=0 THEN
                    sResponse := sReceivedString;
                    bDone := TRUE;
                    nStep := 0;
                END_IF
            END_IF
    END_CASE

    END_FUNCTION_BLOCK




FB_EpicsCentroidMonitor
^^^^^^^^^^^^^^^^^^^^^^^

::

    (*
        EPICS AreaDetector Stats Plugin Centroid Monitor

        Requirements:

        * Requires an existing IOC with an EPICS AreaDetector.
        * Requires an AreaDetector plugin chain that includes a stats plugin.
        * Requires a pytmc "link" pragma to specify the plugin PV prefix.

        What information is included?

        * Centroid X position
        * Centroid Y position
        * Array count index
        * The validity of the data, as determined by:
            - The ArrayCounter_RBV PV changing
            - Alarm severity of the PVs
            - The centroid value changing, even minimally

        Example:

            {attribute 'pytmc' := '
                pv: @(PREFIX):Chk:Centroid1
                link: AREA:DETECTOR:CAM:01:Stats2:
            '}
            fbCentroid : FB_EpicsCentroidMonitor;

            fbCentroid(bEnable:=TRUE, fMinimumValidChange:=1E-6, fNewFrameMinimumChange:=1E-6);
            IF fbCentroid.bValid AND fbCentroid.bIsUpdating THEN
                fbCentroid.fCentroidX;
                fbCentroid.fCentroidY;
                fbCentroid.nArrayCount;
                fbCentroid.fFrameTime;
            END_IF

        Above, ``AREA:DETECTOR:CAM:01:Stats2`` should refer to the PV prefix of a
        stats plugin.
        That is, ``caget AREA:DETECTOR:CAM:01:Stats2:ArrayCounter_RBV`` should not
        fail.

        How does this work?

        pytmc offers the ability to push EPICS process variable data into PLCs by
        way of link pragmas.  These link pragmas create additional supporting EPICS
        records that monitor PVs from any IOC on the controls network. In this case,
        the PLC IOC will monitor vital AreaDetector plugin record data and let your
        PLC project know of its status.

        Why are there multiple thresholds I have to specify?

        ``fNewFrameMinimumChange`` is the minimum change in pixels to say that
        when the array counter changes, these are the new values for X and Y.
        While this seems a bit confusing (or perhaps unnecessary), we do not
        know when the array counters or centroid values will be updated.  These
        do not get written in a single, atomic write to the PLC. We may see an
        updated array count and then a centroid X change and then a Y change.
        This threshold is a way of working around that complexity and saying
        the new data is here only when it's changed at least a bit and we have
        a new frame number.

        ``fMinimumValidChange``, which by default is set to the same as the above
        threshold, says that if the centroid value doesn't change by at least this
        on a frame-to-frame basis, consider this a result of a faulty AreaDetector
        implementation.  The monitor would report values below this threshold as
        "not updating" - ``bIsUpdating`` as ``FALSE``.

        What needs to happen to get new centroid values?

        - An updated array count
        - An updated X centroid value (above fNewFrameMinimumChange)
        - An updated Y centroid value (above fNewFrameMinimumChange)
        - NO_ALARM severities for all values

        This will result in an updated:

        - fFrameTime (seconds between valid frames)

        What needs to happen for ``bIsUpdating`` to be TRUE?

        - All items from the above "new centroid values"
        - Frame time below fMaximumFrameTime
        - Centroid X and Y values above fMinimumValidChange

    *)
    FUNCTION_BLOCK FB_EpicsCentroidMonitor
    VAR_INPUT
        fMaximumFrameTime : LREAL := 0.2;
        (* Minimum change to be considered updating correctly - buggy detectors may need this. *)
        fMinimumValidChange : LREAL := 1E-6;
        (* Minimum change in pixels to be considered a new value for X, Y *)
        fNewFrameMinimumChange : LREAL := 1E-6;
        bEnable : BOOL := TRUE;
    END_VAR
    VAR_OUTPUT
        {attribute 'pytmc' := '
            pv: IsUpdating
            io: input
        '}
        bIsUpdating : BOOL;
        {attribute 'pytmc' := '
            pv: CentroidX
            io: input
        '}
        fCentroidX : LREAL;
        {attribute 'pytmc' := '
            pv: CentroidY
            io: input
        '}
        fCentroidY : LREAL;
        {attribute 'pytmc' := '
            pv: ArrayCount
            io: input
        '}
        nArrayCount : UDINT;
        bValid : BOOL;
        {attribute 'pytmc' := '
            pv: FrameTime
            io: input
            field: DESC Time between frame updates
            field: EGU sec
        '}
        fFrameTime : LREAL;
    END_VAR
    VAR
        {attribute 'pytmc' := '
            pv: CX_
            link: CentroidX_RBV
        '}
        fbCentroidX : FB_LREALFromEPICS;

        {attribute 'pytmc' := '
            pv: CY_
            link: CentroidY_RBV
        '}
        fbCentroidY : FB_LREALFromEPICS;

        fLastCentroidX : LREAL;
        fLastCentroidY : LREAL;

        {attribute 'pytmc' := '
            pv: Cnt_
            link: ArrayCounter_RBV
        '}
        fbArrayCounter : FB_LREALFromEPICS;

        // Last array count value
        nLastArrayCount : UDINT;
        // Time of the last frame update
        tLastUpdate: TIME;

        bInit : BOOL;
        // Did we see a frame update yet? (FALSE at startup; TRUE after first frame.)
        bSawFrame : BOOL;
        // Do we have a new frame? Has the array count updated, and position change above fNewFrameMinimumChange?
        bHaveNewFrame : BOOL;
        // For this new frame, is it above the threshold fMinimumValidChange?
        bAboveThreshold : BOOL;

    END_VAR
    IF NOT bEnable THEN
        bValid := FALSE;
        RETURN;
    END_IF

    IF NOT bInit THEN
        tLastUpdate := TIME();
        fFrameTime := 1000.0;
        bSawFrame := FALSE;
        bInit := TRUE;
    END_IF

    fbCentroidX();
    fbCentroidY();
    fbArrayCounter();

    bValid := (
        fbCentroidX.bValid AND
        fbCentroidY.bValid AND
        fbArrayCounter.bValid
    );

    fCentroidX := fbCentroidX.fValue;
    fCentroidY := fbCentroidY.fValue;
    nArrayCount := LREAL_TO_UDINT(fbArrayCounter.fValue);

    (*
        Only consider that we have a new frame if:
        1. The array count has been updated
        2. X and Y values have changed even minimally
            a. With background noise, this should not be a problem.
            b. With an invalid black/white/stale image, this will indicate
               'no new frame' despite an increasing array count.
    *)
    bHaveNewFrame := (
        nArrayCount <> nLastArrayCount AND
        ABS(fLastCentroidX - fCentroidX) >= fNewFrameMinimumChange AND
        ABS(fLastCentroidY - fCentroidY) >= fNewFrameMinimumChange AND
        TRUE
    );

    IF bHaveNewFrame THEN
        bAboveThreshold := (
            ABS(fLastCentroidX - fCentroidX) >= fMinimumValidChange AND
            ABS(fLastCentroidY - fCentroidY) >= fMinimumValidChange
        );

        fFrameTime := TIME_TO_LREAL(TIME() - tLastUpdate) * 0.001; // Milliseconds -> seconds
        tLastUpdate := TIME();
        nLastArrayCount := nArrayCount;
        bSawFrame := TRUE;

        fLastCentroidX := fCentroidX;
        fLastCentroidY := fCentroidY;
    END_IF

    bIsUpdating := bSawFrame AND (fFrameTime <= fMaximumFrameTime) AND bAboveThreshold;

    END_FUNCTION_BLOCK


Related:
    * `FB_LREALFromEPICS`_


FB_EpicsMotorMonitor
^^^^^^^^^^^^^^^^^^^^

::

    (*
        EPICS Motor Record Monitoring tool

        Requirements:

        * Requires an existing IOC with an EPICS motor record.
        * Requires a pytmc "link" pragma to specify the motor record prefix.

        What information is included?

        * The validity of the source data (as determined by alarm severity)
        * Readback value (scaled encoder position in ``fPosition``)
        * Motion status (moving or not, ``bIsMoving``)
        * Homed status, limit status, and others (via ``stMSTA``, see
          ``ST_EpicsMotorMSTA``).

        Example:

            {attribute 'pytmc' := '
                pv: @(PREFIX):Internal:Mon
                link: MOTOR:PV:NAME
            '}
            fbMotorMonitor : FB_EpicsMotorMonitor;

            fbMotorMonitor(bEnable:=TRUE);
            IF fbMotorMonitor.bValid THEN
                fbMotorMonitor.fPosition;
                fbMotorMonitor.bIsMoving;
                fbMotorMonitor.stMSTA.bHomed;
            END_IF

        Above, ``MOTOR:PV:NAME`` should refer to an EPICS motor record.
        That is, ``caget MOTOR:PV:NAME.RTYP`` should return "motor".

        How does this work?

        pytmc offers the ability to push EPICS process variable data into PLCs by
        way of link pragmas.  These link pragmas create additional supporting EPICS
        records that monitor PVs from any IOC on the controls network. In this case,
        the PLC IOC will monitor vital motor record information and let your PLC
        project know of its status.

        Note that not all motor records may be created equally.  Some of the status
        fields may not be consistent.  Be sure to check how your motor works and
        what fields are worth paying attention to when using this.

    *)
    FUNCTION_BLOCK FB_EpicsMotorMonitor
    VAR_INPUT
        bEnable : BOOL := TRUE;
    END_VAR
    VAR_OUTPUT
        bIsMoving : BOOL;
        fPosition : LREAL;
        nMSTA_Raw : UINT;
        stMSTA : ST_EpicsMotorMSTA;
        bValid : BOOL;
    END_VAR
    VAR
        {attribute 'pytmc' := '
            pv: RBV_
            link: .RBV
        '}
        fbRBVCheck : FB_LREALFromEPICS;

        {attribute 'pytmc' := '
            pv: Dmov_
            link: .DMOV
        '}
        fbMovingCheck : FB_LREALFromEPICS;

        {attribute 'pytmc' := '
            pv: Msta_
            link: .MSTA
        '}
        fbMotorStatusCheck : FB_LREALFromEPICS;
    END_VAR
    IF NOT bEnable THEN
        bValid := FALSE;
        RETURN;
    END_IF

    fbRBVCheck();
    fbMovingCheck();
    fbMotorStatusCheck();
    bValid := (
        fbRBVCheck.bValid AND
        fbMovingCheck.bValid AND
        fbMotorStatusCheck.bValid
    );

    (* Moving status is DMOV; this comes in as a floating point value
         DMOV = 0 -> moving
         DMOV = 1 -> done moving, or not moving
    *)
    bIsMoving := ABS(fbMovingCheck.fValue) < 1e-5;
    fPosition := fbRBVCheck.fValue;

    nMSTA_Raw := LREAL_TO_UINT(fbMotorStatusCheck.fValue);
    stMSTA.bPositiveDirection := nMSTA_Raw.0;
    stMSTA.bDone := nMSTA_Raw.1;
    stMSTA.bPlusLimitSwitch := nMSTA_Raw.2;
    stMSTA.bHomeLimitSwitch := nMSTA_Raw.3;
    stMSTA.bUnused0 := nMSTA_Raw.4;
    stMSTA.bClosedLoop := nMSTA_Raw.5;
    stMSTA.bSlipStall := nMSTA_Raw.6;
    stMSTA.bHome := nMSTA_Raw.7;
    stMSTA.bEncoderPresent := nMSTA_Raw.8;
    stMSTA.bHardwareProblem := nMSTA_Raw.9;
    stMSTA.bMoving := nMSTA_Raw.10;
    stMSTA.bGainSupport := nMSTA_Raw.11;
    stMSTA.bCommError := nMSTA_Raw.12;
    stMSTA.bMinusLimitSwitch := nMSTA_Raw.13;
    stMSTA.bHomed := nMSTA_Raw.14;

    END_FUNCTION_BLOCK


Related:
    * `FB_LREALFromEPICS`_
    * `ST_EpicsMotorMSTA`_


FB_EPS
^^^^^^

::

    FUNCTION_BLOCK FB_EPS
    VAR_IN_OUT
        eps : DUT_EPS;
    END_VAR
    VAR_OUTPUT
    END_VAR
    VAR
    END_VAR


    END_FUNCTION_BLOCK

    METHOD setBit : BOOL
    VAR_INPUT
        nBits : BYTE;
        bValue : BOOL;
    END_VAR
    VAR
        nMask : UDINT := 1;
        nBitValue : UDINT := 0;
    END_VAR
    nMask := SHL(nMask, nBits);
    nBitValue := SHR((nMask AND eps.nFlags), nBits);

    IF (TO_BOOL(nBitValue)) <> bValue THEN
        eps.nFlags := eps.nFlags XOR nMask;
    END_IF

    // Check if all values are true
    IF eps.nFlags = 16#FFFFFFFF THEN
        eps.bEPS_OK := TRUE;
    ELSE
        eps.bEPS_OK := FALSE;
    END_IF
    END_METHOD

    METHOD setDescription : BOOL
    VAR_INPUT
        desciption : STRING;
    END_VAR
    eps.sFlagDesc := desciption;
    END_METHOD

    METHOD setMessage : BOOL
    VAR_INPUT
        message : STRING;
    END_VAR
    eps.sMessage := message;
    END_METHOD


Related:
    * `DUT_EPS`_


FB_EtherCATDiag
^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_EtherCATDiag
    VAR_INPUT
        sIPCNetID					: T_AmsNetId; 	// AmsNetId of the IPC
        sMasterNetID				: T_AmsNetId; 	// AmsNetId of the EtherCAT master device
        nMasterDevID				: UINT;			// Device ID of EtherCAT master
        nSlaveCount 				: UINT;			// current slave count
        nSlaveCountCfg 				: UINT;			// configured slave count
        nMasterDevState				: WORD;			// device state of EtherCAT Master
        bAllFrameWcStatesOK			: BOOL;			// all FrameWcState OK?
        tTimeout					: TIME := T#5S; // ads timeout
        eSubSystem : E_Subsystem := E_Subsystem.FIELDBUS; // Subsystem, change to (MPS, VACUUM, MOTION, etc)
    END_VAR
    VAR_OUTPUT
        bEtherCATOK					: BOOL;			// no problem on EtherCAT
        bFrameWcStateError			: BOOL;			// error in at least one frame
        bSlaveCountError			: BOOL;			// EtherCAT slave count mismatch (# of cfg slaves <> # of found slaves)
        bMasterDevStateError		: BOOL;			// error in master device state
        stMasterDevState			: ST_EcMasterDevState; // splitted master device state
        bBusy						: BOOL;			// FB busy
        bError						: BOOL;			// FB with error
        iErrorID					: UDINT;		// FB error ID
    END_VAR
    VAR_IN_OUT
        arrDiagSlaveInfo 			: ARRAY [0..ESC_MAX_PORTS] OF ST_SlaveStateInfo;		// read in info from configured slaves
        arrDiagSlaveInfoScanned		: ARRAY [0..ESC_MAX_PORTS] OF ST_SlaveStateInfoScanned;	// read in info from scanned slaves
    END_VAR
    VAR
        iState						: E_EcatDiagState;
        nMasterDevStatePrev			: WORD;
        bSlaveCountErrorPrev		: BOOL;
        bAllFrameWcStatesOKPrev		: BOOL;
        bDiagReq					: BOOL := TRUE;
        I							: UDINT;
        P							: UDINT;

        arrSlaveInfo 				: ARRAY [0..iSLAVEADDR_ARR_SIZE] OF ST_SlaveStateInfo;
        rSlaveInfo  :   REFERENCE TO ST_SlaveStateInfo;

        (* -- Get Slave Addresses *)
        fbGetSlaveAddresses 		: FB_EcGetAllSlaveAddr;
        arrSlaveAddresses 			: ARRAY[0..iSLAVEADDR_ARR_SIZE] OF UINT;
        iNumOfSlavesRead			: UINT;

        (* -- Get Slave States *)
        fbGetAllSlaveStates			: FB_EcGetAllSlaveStates;
        arrSlaveStates 				: ARRAY[0..iSLAVEADDR_ARR_SIZE] OF ST_EcSlaveState;

        (* -- Get Topology Data *)
        iTopologyData				: UDINT;
        fbGetTopologyData			: ADSREAD;
        arrTopologyData				: ARRAY[0..iSLAVEADDR_ARR_SIZE] OF ST_TopologyData;

        (* -- Check Topology *)
        aiDiagIndex					: ARRAY [0..ESC_MAX_PORTS] OF UINT;
        iDiagIndex : UINT;
        aiDiagPort					: ARRAY [0..ESC_MAX_PORTS] OF UINT;
        iDiagPort : UINT;
        iIdx						: DINT;

        (* -- Scan Slaves *)
        fbEcGetScannedSlaves		: FB_EcGetScannedSlaves;
        arrScannedSlaveInfo			: ARRAY [0..iSLAVEADDR_ARR_SIZE] OF ST_EcSlaveScannedData; // what...
        rScannedSlaveInfo   :   REFERENCE TO ST_EcSlaveScannedData;
        nScannedSlaves				: UINT;

        (* -- Get Slave Identities *)
        fbGetSlaveIdentity			: FB_EcGetSlaveIdentity;
        stIdentity 					: ST_EcSlaveIdentity;

        (* -- Get Slave Names *)
        fbGetSlaveName 				: IOF_GetBoxNameByAddr;
        arrSlaveInfoScanned			: ARRAY [0..iSLAVEADDR_ARR_SIZE] OF ST_SlaveStateInfoScanned; // the F
        rSlaveInfoScanned   :   REFERENCE TO ST_SlaveStateInfoScanned;
        strName						: STRING;



        // Logging components
        fbLogger : FB_LogMessage := (eSubsystem := eSubsystem);

        fbJson : FB_JsonSaxWriter;
        fbJsonDataType : FB_JsonReadWriteDataType;
        rDiagSlaveInfo : REFERENCE TO ST_SlaveStateInfo;
        tEtherCATOK : F_TRIG;
        tFrameWcStateError : R_TRIG;
        tMasterError : R_TRIG;
        jsonIdx : UINT;
        {attribute 'analysis' := '-27'}
        test : T_MaxString;
    END_VAR
    (* cyclic diag *)
    bFrameWcStateError := NOT bAllFrameWcStatesOK;

    bSlaveCountError := (nSlaveCount <> nSlaveCountCfg) OR (nSlaveCount = 0);
    IF (bSlaveCountError AND NOT bSlaveCountErrorPrev) OR (NOT bSlaveCountError AND bSlaveCountErrorPrev) THEN
        bSlaveCountErrorPrev := bSlaveCountError;
        bDiagReq := TRUE; // slave count error change detected --> diag required
    END_IF

    IF (bAllFrameWcStatesOK AND NOT bAllFrameWcStatesOKPrev) OR (NOT bAllFrameWcStatesOK AND bAllFrameWcStatesOKPrev) THEN
        bAllFrameWcStatesOKPrev := bAllFrameWcStatesOK;
        bDiagReq := TRUE; // frame error change detected --> diag required
    END_IF

    IF (nMasterDevState <> nMasterDevStatePrev) THEN
        M_CheckMasterDevState();
        bDiagReq := TRUE; // devstate change detected --> diag required
    END_IF

    (* acyclic diag *)
    CASE iState OF
    E_EcatDiagState.Idle: (* IDLE *)
        IF bDiagReq THEN				// diag requested
            bDiagReq := FALSE;

            IF sMasterNetID <> '' AND sMasterNetID <> '0.0.0.0.0.0' THEN
                iState := E_EcatDiagState.GetSlaveAddresses;	// execute diag
                bBusy := TRUE;
            ELSE
                bError := TRUE;
                iErrorID := 7;
            END_IF

            bEtherCATOK := FALSE;
        ELSE
            // check for changes in idle
            IF (bSlaveCountError OR bMasterDevStateError OR NOT bAllFrameWcStatesOK) AND NOT (arrSlaveInfo[aiDiagIndex[0]].bDiagData) THEN
                bEtherCATOK := FALSE;
                bDiagReq := TRUE;//new error --> diag requested
            ELSIF (bSlaveCountError AND NOT bSlaveCountErrorPrev) OR (NOT bSlaveCountError AND bSlaveCountErrorPrev) THEN
                bSlaveCountErrorPrev := bSlaveCountError;
                bEtherCATOK := FALSE;
                bDiagReq := TRUE;// slave count error change detected --> diag required
            ELSIF (nMasterDevState <> nMasterDevStatePrev) THEN
                bEtherCATOK := FALSE;
                bDiagReq := TRUE;// devstate change detected --> diag required
            ELSIF (bAllFrameWcStatesOK AND NOT bAllFrameWcStatesOKPrev) OR (NOT bAllFrameWcStatesOK AND bAllFrameWcStatesOKPrev) THEN
                bAllFrameWcStatesOKPrev := bAllFrameWcStatesOK;
                bEtherCATOK := FALSE;
                bDiagReq := TRUE;// frame error change detected --> diag required
            ELSIF (bSlaveCountError OR bMasterDevStateError OR NOT bAllFrameWcStatesOK OR arrSlaveInfo[aiDiagIndex[0]].bDiagData) THEN
                bEtherCATOK := FALSE;
            ELSE
                bEtherCATOK := TRUE;
            END_IF
        END_IF

    E_EcatDiagState.GetSlaveAddresses: (* get adresses *)
        M_GetSlaveAdresses();

    E_EcatDiagState.GetSlaveStates: (* get states *)
        M_GetSlaveStates();

    E_EcatDiagState.GetTopoDataLen:	(* get topology data length *)
        M_GetTopoDataLen();

    E_EcatDiagState.GetTopoData:	(* get topology data *)
        M_GetTopoData();

    E_EcatDiagState.ScanSlaves:	(* scan slaves *)
        M_ScanSlaves();

    E_EcatDiagState.GetSlaveIdentity:	(* get identity *)
        M_GetSlaveIdentity();

    E_EcatDiagState.GetSlaveName:	(* get name *)
        M_GetSlaveName();

    E_EcatDiagState.GetScannedSlaveName:	(* get scanned name *)
        M_GetScannedSlaveName();

    E_EcatDiagState.LogDiagnostics: (* Log diagnostics *)
        (* I can't get the fbJsonDataType to actually convert the slave info
        structures. I just get nulls. Either I am doing this wrong, or when
        the symbol parser encounters datatypes it can't deal with it nulls
        the whole thing. I'll keep this code commented out until someone figures
        out how to deal with parsing the slave structs into the json payload *)
        IF jsonIdx < iNumOfSlavesRead THEN // the last entry is always blank
            jsonIdx := MIN(iSLAVEADDR_ARR_SIZE, jsonIdx);
            rDiagSlaveInfo REF= arrSlaveInfo[jsonIdx];
            DiagnosticJson();
            fbLogger(sMsg:=CONCAT('Diag Results: ', rDiagSlaveInfo.sName),
                    eSevr:=TcEventSeverity.Info);
            jsonIdx := jsonIdx + 1;
        ELSE
            jsonIdx := 0;
            iState := E_EcatDiagState.Done;
        END_IF

    E_EcatDiagState.Done:	(* DONE *)
        bBusy := FALSE;
        iState := 0;

    END_CASE

    // Log messages
    tEtherCATOK(CLK:=bEtherCATOK);
    IF tEtherCATOK.Q THEN
        fbLogger(sMsg:='EtherCAT failure, starting diagnostic', eSevr:=TcEventSeverity.Critical, sJson:='');
    END_IF

    tFrameWcStateError(CLK:=bFrameWcStateError);
    IF tFrameWcStateError.Q THEN
        fbLogger(sMsg:='Working Counter Frame Error: error in at least one frame', eSevr:=TcEventSeverity.Error, sJson:='');
    END_IF

    tMasterError(CLK:=bMasterDevStateError);
    IF tMasterError.Q THEN
        fbJson.StartObject();
            fbJson.AddKey('ecat_master_diag');
            fbJson.StartObject();
                fbJson.AddKey('bAtLeastOneNotInOp');
                fbJson.AddBool(stMasterDevState.bAtLeastOneNotInOp);
                fbJson.AddKey('bDcNotInSync');
                fbJson.AddBool(stMasterDevState.bDcNotInSync);
                fbJson.AddKey('bDriverNotFound');
                fbJson.AddBool(stMasterDevState.bDriverNotFound);
                fbJson.AddKey('bLinkError');
                fbJson.AddBool(stMasterDevState.bLinkError);
                fbJson.AddKey('bMissFrmRedMode');
                fbJson.AddBool(stMasterDevState.bMissFrmRedMode);
                fbJson.AddKey('bResetActive');
                fbJson.AddBool(stMasterDevState.bResetActive);
                fbJson.AddKey('bResetRequired');
                fbJson.AddBool(stMasterDevState.bResetRequired);
                fbJson.AddKey('bWatchdogTriggerd');
                fbJson.AddBool(stMasterDevState.bWatchdogTriggerd);
                fbJson.AddKey('eEcState');
                fbJson.AddUdint(stMasterDevState.eEcState);
            fbJson.EndObject();
        fbJson.EndObject();
        fbJson.CopyDocument(fbLogger.sJson, SIZEOF(fbLogger.sJson));
        fbLogger(sMsg:='Master error: error in master device state', eSevr:=TcEventSeverity.Critical);
        fbJson.ResetDocument();
    END_IF

    END_FUNCTION_BLOCK

    ACTION DiagnosticJson:
    fbJson.StartObject();
        fbJson.AddKey('ecat_diag');
        fbJson.StartObject();

            fbJson.AddKey('nECAddr');
            fbJson.AddUdint(rDiagSlaveInfo.nECAddr);

            fbJson.AddKey('nIndex');
            fbJson.AddDint(rDiagSlaveInfo.nIndex);

            fbJson.AddKey('sName');
            fbJson.AddString(rDiagSlaveInfo.sName);

            fbJson.AddKey('sType');
            fbJson.AddString(rDiagSlaveInfo.sType);

            fbJson.AddKey('bDiagData');
            fbJson.AddBool(rDiagSlaveInfo.bDiagData);

            fbJson.AddKey('stPortCRCErrors');
            fbjson.StartObject();

                fbJson.AddKey('portA');
                fbJson.AddUdint(rDiagSlaveInfo.stPortCRCErrors.portA);
                fbJson.AddKey('portB');
                fbJson.AddUdint(rDiagSlaveInfo.stPortCRCErrors.portB);
                fbJson.AddKey('portC');
                fbJson.AddUdint(rDiagSlaveInfo.stPortCRCErrors.portC);
                fbJson.AddKey('portD');
                fbJson.AddUdint(rDiagSlaveInfo.stPortCRCErrors.portD);

            fbJson.EndObject();

            fbJson.AddKey('nSumCRCErrors');
            fbjson.AddUdint(rDiagSlaveInfo.nSumCRCErrors);

            fbJson.AddKey('stState');
            fbJson.StartObject();

                fbJson.AddKey('eEcState ');
                fbJson.AddUdint(rDiagSlaveInfo.stState.eEcState);
                fbJson.AddKey('nReserved');
                fbJson.AddUdint(rDiagSlaveInfo.stState.nReserved);
                fbJson.AddKey('bError');
                fbJson.AddBool(rDiagSlaveInfo.stState.bError);
                fbJson.AddKey('bInvalidVPRS');
                fbJson.AddBool(rDiagSlaveInfo.stState.bInvalidVPRS);
                fbJson.AddKey('nReserved2');
                fbJson.AddUdint(rDiagSlaveInfo.stState.nReserved2);
                fbJson.AddKey('bNoCommToSlave');
                fbJson.AddBool(rDiagSlaveInfo.stState.bNoCommToSlave);
                fbJson.AddKey('bLinkError');
                fbJson.AddBool(rDiagSlaveInfo.stState.bLinkError);
                fbJson.AddKey('bMissingLink');
                fbJson.AddBool(rDiagSlaveInfo.stState.bMissingLink);
                fbJson.AddKey('bUnexpectedLink');
                fbJson.AddBool(rDiagSlaveInfo.stState.bUnexpectedLink);
                fbJson.AddKey('bPortA');
                fbJson.AddBool(rDiagSlaveInfo.stState.bPortA);
                fbJson.AddKey('bPortB');
                fbJson.AddBool(rDiagSlaveInfo.stState.bPortB);
                fbJson.AddKey('bPortC');
                fbJson.AddBool(rDiagSlaveInfo.stState.bPortC);
                fbJson.AddKey('bPortD');
                fbJson.AddBool(rDiagSlaveInfo.stState.bPortD);

            fbJson.EndObject();

        fbJson.EndObject();

    fbJson.EndObject();

    fbJson.CopyDocument(fbLogger.sJson, SIZEOF(fbLogger.sJson));
    fbJson.ResetDocument();
    END_ACTION

    ACTION M_CheckMasterDevState:
    (* check master errors based on devstate *)
    bMasterDevStateError 				:= nMasterDevState <> 0;
    stMasterDevState.bLinkError 		:= ((nMasterDevState AND 16#000F) = 1) OR ((nMasterDevState AND 16#000F) = 4);
    stMasterDevState.bResetRequired 	:= ((nMasterDevState AND 16#000F) = 2) OR ((nMasterDevState AND 16#FFF0) = 16#10);
    stMasterDevState.bMissFrmRedMode 	:= (nMasterDevState AND 16#000F) = 8;
    stMasterDevState.bWatchdogTriggerd 	:= (nMasterDevState AND 16#20) = 16#20;
    stMasterDevState.bDriverNotFound 	:= (nMasterDevState AND 16#40) = 16#40;
    stMasterDevState.bResetActive 		:= (nMasterDevState AND 16#80) = 16#80;
    stMasterDevState.bAtLeastOneNotInOp := ((nMasterDevState AND 16#100) = 16#100) OR ((nMasterDevState AND 16#200) = 16#200) OR
                                            ((nMasterDevState AND 16#400) = 16#400) OR ((nMasterDevState AND 16#800) = 16#800);
    stMasterDevState.bDcNotInSync 		:= (nMasterDevState AND 16#1000) = 16#1000;
    nMasterDevStatePrev 				:= nMasterDevState;
    END_ACTION

    ACTION M_GetScannedSlaveName:
    rSlaveInfoScanned REF= arrSlaveInfoScanned[aiDiagIndex[iIdx]];
    rScannedSlaveInfo REF= arrScannedSlaveInfo[aiDiagIndex[iIdx]];

    fbGetSlaveName(
        NETID		:= sIPCNetId,
        DEVICEID	:= nMasterDevID,
        BOXADDR		:= rScannedSlaveInfo.nAddr,
        START		:= TRUE,
        TMOUT		:= tTimeout,
        BOXNAME		=> strName
    );

    IF NOT fbGetSlaveName.BUSY THEN
        fbGetSlaveName(START:= FALSE);

        (* add scanned info *)
        rSlaveInfoScanned.nIndex 	:= iDiagIndex + 1;
        IF rScannedSlaveInfo.nAddr <> 0 THEN
            IF NOT fbGetSlaveName.ERR THEN
                rSlaveInfoScanned.sName := strName;
            END_IF
        ELSE
            rSlaveInfoScanned.sType		:= '';
        END_IF

        IF (iDiagIndex < nScannedSlaves) THEN
            rSlaveInfoScanned.sType		:= F_ConvProductCodeToString(rScannedSlaveInfo.stSlaveIdentity);
        ELSE
            rSlaveInfoScanned.sType		:= '';
        END_IF

        rSlaveInfoScanned.nECAddr	:= rScannedSlaveInfo.nAddr;

        IF rSlaveInfoScanned.sName <> rSlaveInfo.sName THEN
            rSlaveInfoScanned.bDifferentName := TRUE;
        ELSE
            rSlaveInfoScanned.bDifferentName := FALSE;
        END_IF

        IF rSlaveInfoScanned.nECAddr <> rSlaveInfo.nECAddr THEN
            rSlaveInfoScanned.bDifferentAddr := TRUE;
        ELSE
            rSlaveInfoScanned.bDifferentAddr := FALSE;
        END_IF

        IF rSlaveInfoScanned.sType <> rSlaveInfo.sType THEN
            rSlaveInfoScanned.bDifferentType := TRUE;
        ELSE
            rSlaveInfoScanned.bDifferentType := FALSE;
        END_IF

        IF iIdx < ESC_MAX_PORTS THEN
            iIdx := iIdx + 1;
            iState := E_EcatDiagState.GetSlaveIdentity; // loop back
        ELSE
            iIdx := 0;
            iState := E_EcatDiagState.LogDiagnostics;

            FOR I := 0 TO ESC_MAX_PORTS DO
                IF aiDiagPort[I] <> 0 THEN
                    arrDiagSlaveInfo[I] := arrSlaveInfo[aiDiagIndex[I]];
                    arrDiagSlaveInfoScanned[I] := arrSlaveInfoScanned[aiDiagIndex[I]];
                ELSE
                    MEMSET(ADR(arrDiagSlaveInfo[I]), 0, SIZEOF(arrDiagSlaveInfo[I]));
                    MEMSET(ADR(arrDiagSlaveInfoScanned[I]), 0, SIZEOF(arrDiagSlaveInfoScanned[I]));
                END_IF
            END_FOR
        END_IF
    END_IF
    END_ACTION

    ACTION M_GetSlaveAdresses:
    fbGetSlaveAddresses(
        sNetId		:= sMasterNetID,
        pAddrBuf	:= ADR(arrSlaveAddresses),
        cbBufLen	:= SIZEOF(arrSlaveAddresses),
        bExecute	:= TRUE,
        tTimeout	:= tTimeout,
        nSlaves		=> iNumOfSlavesRead
    );

    IF NOT fbGetSlaveAddresses.bBusy THEN
        fbGetSlaveAddresses(bExecute:= FALSE);
        IF NOT fbGetSlaveAddresses.bError THEN
            FOR I := 0 TO MIN(iNumOfSlavesRead, iSLAVEADDR_ARR_SIZE) DO
                arrSlaveInfo[I].nECAddr := arrSlaveAddresses[I];
            END_FOR
        END_IF
        iState := GetSlaveStates;
    END_IF
    END_ACTION

    ACTION M_GetSlaveIdentity:
    iDiagIndex := aiDiagIndex[iIdx];
    iDiagPort := aiDiagPort[iIdx];

    rSlaveInfo REF= arrSlaveInfo[iDiagIndex];

    fbGetSlaveIdentity(
        sNetId		:= sMasterNetID,
        nSlaveAddr	:= rSlaveInfo.nECAddr,
        bExecute	:= TRUE,
        tTimeout	:= tTimeout,
        identity	=> stIdentity
    );

    IF NOT fbGetSlaveIdentity.bBusy THEN
        fbGetSlaveIdentity(bExecute:= FALSE);

        IF NOT fbGetSlaveIdentity.bError THEN
            IF aiDiagPort[iIdx] <> 0 THEN
                rSlaveInfo.nIndex	:= aiDiagIndex[iIdx] + 1;
                rSlaveInfo.sType 	:= F_ConvProductCodeToString(stSlaveIdentity := stIdentity);
            END_IF
        END_IF
        iState := E_EcatDiagState.GetSlaveName;
    END_IF
    END_ACTION

    ACTION M_GetSlaveName:
    fbGetSlaveName(
        NETID		:= sIPCNetId,
        DEVICEID	:= nMasterDevID,
        BOXADDR		:= rSlaveInfo.nECAddr,
        START		:= TRUE,
        TMOUT		:= tTimeout,
        BOXNAME		=> strName
    );

    IF NOT fbGetSlaveName.BUSY THEN
        fbGetSlaveName(START:= FALSE);

        IF NOT fbGetSlaveName.ERR THEN
            IF iDiagPort <> 0 THEN
                rSlaveInfo.sName 	:= strName;
            END_IF
        END_IF

        iState := E_EcatDiagState.GetScannedSlaveName;
    END_IF
    END_ACTION

    ACTION M_GetSlaveStates:
    fbGetAllSlaveStates(
        sNetId		:= sMasterNetID,
        pStateBuf	:= ADR(arrSlaveStates),
        cbBufLen	:= SIZEOF(arrSlaveStates),
        bExecute	:= TRUE,
        tTimeout	:= tTimeout,
        nSlaves		=> iNumOfSlavesRead
    );

    IF NOT fbGetAllSlaveStates.bBusy THEN
        fbGetAllSlaveStates(bExecute:= FALSE);

        IF NOT fbGetAllSlaveStates.bError THEN
            IF iNumOfSlavesRead = nSlaveCountCfg THEN
                FOR I := 0 TO ESC_MAX_PORTS DO
                    aiDiagIndex[I] := 0;
                END_FOR

                (* split slave state and link state *)
                FOR I := 0 TO MIN(iNumOfSlavesRead, iSLAVEADDR_ARR_SIZE) DO
                    (* slave state*)
                    arrSlaveInfo[I].stState.eEcState 		:= arrSlaveStates[I].deviceState AND 16#0F;
                    arrSlaveInfo[I].stState.bError 			:= arrSlaveStates[I].deviceState.4;
                    arrSlaveInfo[I].stState.bInvalidVPRS 	:= arrSlaveStates[I].deviceState.5;
                    (* link state *)
                    arrSlaveInfo[I].stState.bNoCommToSlave 	:= arrSlaveStates[I].linkState.0;
                    arrSlaveInfo[I].stState.bLinkError 		:= arrSlaveStates[I].linkState.1;
                    arrSlaveInfo[I].stState.bMissingLink 	:= arrSlaveStates[I].linkState.2;
                    arrSlaveInfo[I].stState.bUnexpectedLink := arrSlaveStates[I].linkState.3;
                    arrSlaveInfo[I].stState.bPortA 			:= arrSlaveStates[I].linkState.4;
                    arrSlaveInfo[I].stState.bPortB 			:= arrSlaveStates[I].linkState.5;
                    arrSlaveInfo[I].stState.bPortC 			:= arrSlaveStates[I].linkState.6;
                    arrSlaveInfo[I].stState.bPortD 			:= arrSlaveStates[I].linkState.7;
                    (* DiagData *)
                    arrSlaveInfo[I].bDiagData	:= ((arrSlaveStates[I].deviceState AND 16#F0) <> 0) OR
                        (((arrSlaveStates[I].deviceState AND 16#0F) > 0) AND ((arrSlaveStates[I].deviceState AND 16#0F) < 8)) OR
                        (arrSlaveStates[I].linkState <> 0);

                    IF arrSlaveInfo[I].bDiagData THEN
                        IF (I=0) THEN
                            aiDiagIndex[0] := 0;
                        ELSE
                            IF (aiDiagIndex[0] = 0) AND NOT arrSlaveInfo[0].bDiagData THEN
                                aiDiagIndex[0] :=  UDINT_TO_UINT(I);
                            END_IF
                        END_IF
                    END_IF
                END_FOR
            END_IF
        END_IF

        IF arrSlaveInfo[aiDiagIndex[0]].bDiagData THEN
            iState := E_EcatDiagState.GetTopoDataLen;
        ELSE
            FOR I := 0 TO ESC_MAX_PORTS DO
                MEMSET(ADR(arrDiagSlaveInfo[I]), 0, SIZEOF(arrDiagSlaveInfo[I]));
                MEMSET(ADR(arrDiagSlaveInfoScanned[I]), 0, SIZEOF(arrDiagSlaveInfoScanned[I]));
            END_FOR
            iState := E_EcatDiagState.Done;
        END_IF
    END_IF
    END_ACTION

    ACTION M_GetTopoData:
    fbGetTopologyData(
        NETID	:= sMasterNetID,
        PORT	:= 16#FFFF,
        IDXGRP	:= 16#22,
        IDXOFFS	:= 0,
        LEN		:= iTopologyData*SIZEOF(arrTopologyData[0]),
        DESTADDR:= ADR(arrTopologyData),
        READ	:= TRUE,
        TMOUT	:= tTimeout,
    );

    IF NOT fbGetTopologyData.BUSY THEN
        fbGetTopologyData(READ := FALSE);

        IF NOT fbGetTopologyData.ERR THEN
            aiDiagPort[0] := arrTopologyData[aiDiagIndex[0]].iOwnPhysicalAddr;
            aiDiagPort[1] := arrTopologyData[aiDiagIndex[0]].stPhysicalAddr.portB;
            aiDiagPort[2] := arrTopologyData[aiDiagIndex[0]].stPhysicalAddr.portC;
            aiDiagPort[ESC_MAX_PORTS] := arrTopologyData[aiDiagIndex[0]].stPhysicalAddr.portD;

            (* clear diag index  *)
            aiDiagIndex[1] := 0;
            aiDiagIndex[2] := 0;
            aiDiagIndex[ESC_MAX_PORTS] := 0;

            (* find slaves on PortB-D of first slave with diag *)
            FOR P := 0 TO ESC_MAX_PORTS DO
                IF aiDiagPort[P] <> 0 THEN
                FOR I := 0 TO MIN(iTopologyData-1,iSLAVEADDR_ARR_SIZE) DO
                    IF arrTopologyData[I].iOwnPhysicalAddr = aiDiagPort[P] THEN
                        aiDiagIndex[P] := UDINT_TO_UINT(I);
                        EXIT;
                    END_IF
                END_FOR
            END_IF
            END_FOR
        END_IF

        iIdx := 0;
        iState := E_EcatDiagState.ScanSlaves;
    END_IF
    END_ACTION

    ACTION M_GetTopoDataLen:
    fbGetTopologyData(
        NETID	:= sMasterNetID,
        PORT	:= 16#FFFF,
        IDXGRP	:= EC_ADS_IGRP_MASTER_COUNT_SLAVE,
        IDXOFFS	:= EC_ADS_IOFFS_MASTER_COUNT_SLAVE,
        LEN		:= SIZEOF(iTopologyData),
        DESTADDR:= ADR(iTopologyData),
        READ	:= TRUE,
        TMOUT	:= tTimeout,
    );

    IF NOT fbGetTopologyData.BUSY THEN
        fbGetTopologyData(READ := FALSE);

        iState := E_EcatDiagState.GetTopoData;
    END_IF
    END_ACTION

    ACTION M_ScanSlaves:
    fbEcGetScannedSlaves(
        bExecute				:= TRUE,
        sNetId					:= sMasterNetID,
        pArrEcScannedSlaveInfo	:= ADR(arrScannedSlaveInfo),
        cbBufLen				:= SIZEOF(arrScannedSlaveInfo),
        tTimeout				:= tTimeout
    );

    IF NOT fbEcGetScannedSlaves.bBusy THEN
        fbEcGetScannedSlaves(bExecute := FALSE);

        IF fbEcGetScannedSlaves.bError THEN
            nScannedSlaves := 0;
        ELSE
            nScannedSlaves := fbEcGetScannedSlaves.nSlaves;
        END_IF

        iState := E_EcatDiagState.GetSlaveIdentity;

    END_IF
    END_ACTION


Related:
    * `E_EcatDiagState`_
    * `E_Subsystem`_
    * `FB_LogMessage`_
    * `ST_EcMasterDevState`_
    * `ST_SlaveStateInfo`_
    * `ST_SlaveStateInfoScanned`_
    * `ST_TopologyData`_


FB_EtherCATFrameDiag
^^^^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_EtherCATFrameDiag
    VAR_INPUT
        wFrmXWcState 		: WORD;				// FrmXWcState
        wReqZeroMask 		: WORD := 16#FFFF;	// mask, bit TRUE: require wFrmXWcState.bit = FALSE, bit FALSE: ignore wFrmXWcState.bit *)
    END_VAR
    VAR_OUTPUT
        bFrameWcStateOK		: BOOL;				// result of fram state check
    END_VAR
    VAR
    END_VAR
    (* mask out ignored error bits and compare result against 0 *)
    bFrameWcStateOK := ((wFrmXWcState AND wReqZeroMask) = 0);

    END_FUNCTION_BLOCK




FB_GetPLCHostname
^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_GetPLCHostname
    VAR_INPUT
        bEnable			: BOOL;
        tRetryDelay		: TIME := T#10s;
    END_VAR
    VAR_OUTPUT
        sHostname		: T_MaxString;
        bDone 			: BOOL;
        bError			: BOOL;
    END_VAR
    VAR
        fbGetHostName	: FB_GetHostName;

        tRetry 			: TON;
        bReset			: BOOL;
        bInitialized	: BOOL := FALSE;

    END_VAR
    IF bEnable THEN
        fbGetHostName.sNetID := '';

        IF NOT bInitialized OR (tRetry.Q AND NOT bDone) THEN
            bReset			:= TRUE;
            bInitialized	:= TRUE;
            fbGetHostName(bExecute:=FALSE);
        END_IF

        tRetry(IN:=bReset, PT:=tRetryDelay);

        bReset := FALSE;

        IF NOT bDone THEN
            fbGetHostName(bExecute:=TRUE, bError=>bError);
            IF NOT (fbGetHostName.bBusy OR bError) THEN
                sHostname	:= fbGetHostName.sHostName;
                bDone 		:= TRUE;
            END_IF
        END_IF
    END_IF

    END_FUNCTION_BLOCK




FB_GetPLCIPAddress
^^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_GetPLCIPAddress
    VAR_INPUT
        bEnable			: BOOL;
        tRetryDelay		: TIME := T#10s;
    END_VAR
    VAR_OUTPUT
        sIPAddress		: STRING(15);

        bDone 			: BOOL;
        bError			: BOOL;
    END_VAR
    VAR
        fbGetAdapterIP 	: FB_GetAdaptersInfo := (bExecute := TRUE, sNetID := ''); // Acquire IP of the correct adapter

        iIndex			: UDINT;

        tRetry 			: TON;
        bReset			: BOOL;
        bInitialized	: BOOL := FALSE;

    END_VAR
    IF bEnable THEN
        IF NOT bInitialized OR (tRetry.Q AND NOT bDone) THEN
            bReset			:= TRUE;
            bInitialized	:= TRUE;
            fbGetAdapterIP(bExecute:=FALSE);
        END_IF

        tRetry(IN:=bReset, PT:=tRetryDelay);
        bReset := FALSE;

        IF NOT bDone THEN
            fbGetAdapterIP(bExecute:=TRUE, bError=>bError);
            IF NOT (fbGetAdapterIP.bBusy or fbGetAdapterIP.bError) THEN
                FOR iIndex := 0 TO MAX_LOCAL_ADAPTERS DO
                    IF FIND(fbGetAdapterIP.arrAdapters[iIndex].sIpAddr, GVL_Logger.sIpTidbit) <> 0 THEN
                        sIPAddress := fbGetAdapterIP.arrAdapters[iIndex].sIpAddr;
                        bDone := TRUE;
                        EXIT;
                    END_IF
                END_FOR
            END_IF
        END_IF

    END_IF

    END_FUNCTION_BLOCK


Related:
    * `GVL_Logger`_


FB_Index
^^^^^^^^

::

    (* Index FB
    A. Wallace 2016-9-3

    Why doesn't beckhoff have this as a builtin type?

    Use this thing to have a simple indexer with rollover.

    *)
    FUNCTION_BLOCK FB_Index
    VAR_INPUT
        {attribute 'naming' := 'off'}
        LowerLimit : INT := 1; //Incrementer will rollver over to this value (and initialize to this value)
        ValInc : INT := 1; //Incrementer increments by this value
        UpperLimit	:	INT := 1; //Incrementer will rollover at this value to lower limit
        {attribute 'naming' := 'off'}
    END_VAR
    VAR_OUTPUT

    END_VAR
    VAR
        nVal	:	INT := LowerLimit; //Internal incrementer value, initialized to LowerLimit
    END_VAR
    {analysis -2} //Only the methods and actions are needed

    END_FUNCTION_BLOCK

    ACTION Dec:
    nVal := nVal - ValInc;
    IF nVal < LowerLimit THEN nVal := UpperLimit; END_IF
    END_ACTION

    ACTION Inc:
    // Dont use this, use ValInc
    nVal := nVal + ValInc;
    IF nVal >  UpperLimit THEN nVal := LowerLimit; END_IF
    END_ACTION

    //Decrement the counter and return new value
    METHOD DecVal : INT
    VAR_INPUT
    END_VAR
    Dec();
    DecVal := nVal;
    END_METHOD

    //Increment the counter and return new value
    METHOD PUBLIC IncVal : INT
    VAR_INPUT
    END_VAR
    Inc();
    IncVal := nVal;
    END_METHOD




FB_LED
^^^^^^

::

    FUNCTION_BLOCK FB_LED
    (*
        Reads a percentage of illumination determined by the user and converts it to a raw value
        to send to the terminal output via the FB_AnalogOutput.

        The determination of the full scale raw value (FSV) is calculated via the iTermBits,
        which is setup as an input variable to be set according to the situation. This value
        is bounded by the FSV of the terminal set by the Beckhoff architecture: (2^15 - 1) = 32767.

        Through the configurable max raw value sent to the terminal output, this LED function block
        is applicable to a wider variety of hardware: whether it be simple Analog Voltage control,
        PWM voltage output, or current controlled output.

        As an initial application, this FB will be instituted for systems of vaccum chamber LED
        illuminators on TMO endstations, which are rated for 12 V max. They will be dimmed via the
        EL2502 terminal 24 V PWM output voltage control from 0-50% duty cycle.

        2022-5-03 Maarten Thomas-Bosum
    *)
    VAR_INPUT
        {attribute 'pytmc' := '
            pv: NAME
            io: io
            field: DESC Descriptive name for the LED
            autosave_pass0: VAL DESC
        '}
        ledName : STRING;

        iTermBits : UINT;

        {attribute 'pytmc' := '
            pv: ILL:PCT
            field: EGU %
        '}
        fIlluminatorPercent: LREAL;

        {attribute 'pytmc' := '
            pv: PWR
            field: ZNAM OFF
            field: ONAM ON
        '}
        bLedPower AT %Q*: BOOL;
    END_VAR
    VAR
        iIlluminatorINT AT %Q*: INT;
        fbSetIllPercent: FB_AnalogOutput;
    END_VAR
    // Illuminator conversion to percentage
    fbSetIllPercent(
        fReal:=fIlluminatorPercent,
        fSafeMax:=100,
        fSafeMin:=0,
        iTermBits:=iTermBits,
        fTermMax:=100,
        fTermMin:=0,
        iRaw=>iIlluminatorINT);

    IF  fIlluminatorPercent > 0 THEN
        bLedPower := TRUE;
    ELSE
        bLEDPower := FALSE;
    END_IF

    END_FUNCTION_BLOCK


Related:
    * `FB_AnalogOutput`_


FB_Listener
^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_Listener EXTENDS FB_ListenerBase
    VAR_INPUT
    END_VAR

    VAR_OUTPUT
    END_VAR

    VAR
        nEventIdx			:	UINT := 0;
        nPendingEvents		:	UINT := 0;

        {attribute 'pytmc' := '
            pv: LogToVisualStudio
            io: io
        '}
        bLogToVisualStudio	:	BOOL := FALSE;

        {attribute 'pytmc' := '
            pv: MessagesSent
            io: i
        '}
        nCntMessagesSent 	: UDINT := 0;

        {attribute 'pytmc' := '
            pv: AlarmsRaised
            io: i
        '}
        nCntAlarmsRaised 	: UDINT := 0;

        {attribute 'pytmc' := '
            pv: AlarmsConfirmed
            io: i
        '}
        nCntAlarmsConfirmed : UDINT := 0;

        {attribute 'pytmc' := '
            pv: AlarmsCleared
            io: i
        '}
        nCntAlarmsCleared 	: UDINT := 0;

        {attribute 'pytmc' := '
            pv: MinSeverity
            io: io
        '}
        eMinSeverity	 	: TcEventSeverity;

        {attribute 'analysis' := '-33'}
        {attribute 'pytmc' := '
            pv: Log
        '}
        stEventInfo 		:	REFERENCE TO ST_LoggingEventInfo;

        stPendingEvents		:	ARRAY [0..nMaxEvents - 1] OF ST_PendingEvent;
        ipMessageConfig		:	ITcEventFilterConfig;
        fbSocket			:	POINTER TO FB_ConnectionlessSocket;
        bConfigured			:	BOOL	:= FALSE;


    END_VAR

    VAR_IN_OUT

    END_VAR

    VAR CONSTANT
        // The maximum number of events allowed *per-cycle*
        nMaxEvents			:	UINT := 10;
    END_VAR


    END_FUNCTION_BLOCK

    (*
        Configure an event class + severity
    *)
    METHOD Configure : HRESULT
    VAR_INPUT
        i_EventClass 	:	GUID;
        i_MinSeverity	:	TcEventSeverity := TcEventSeverity.Verbose;
        i_fbSocket		:	POINTER TO FB_ConnectionlessSocket;
    END_VAR

    VAR_INST
        bSubscribed		:	BOOL := FALSE;
    END_VAR
    IF bSubscribed THEN
        Unsubscribe();
    END_IF

    THIS^.Subscribe(ADR(ipMessageConfig), 0);
    bSubscribed 	:= TRUE;
    eMinSeverity 	:= i_MinSeverity;
    fbSocket	  	:= i_fbSocket;

    IF (ipMessageConfig = 0) THEN
        Configure 	:= 1;
        bConfigured := FALSE;
    ELSE
        ipMessageConfig.AddEventClass(i_EventClass, i_MinSeverity);
        Configure 	:= 0;
        bConfigured := TRUE;
    END_IF
    END_METHOD

    METHOD OnAlarmCleared
    VAR_INPUT
        fbEvent : REFERENCE TO FB_TcEvent;
    END_VAR
    (* Callback run from THIS^.Execute() *)
    nCntAlarmsCleared := nCntAlarmsCleared + 1;
    StoreEvent(fbEvent, eEventType:=E_LogEventType.AlarmCleared);
    END_METHOD

    METHOD OnAlarmConfirmed
    VAR_INPUT
        fbEvent : REFERENCE TO FB_TcEvent;
    END_VAR
    (* Callback run from THIS^.Execute() *)
    nCntAlarmsConfirmed := nCntAlarmsConfirmed + 1;
    StoreEvent(fbEvent, eEventType:=E_LogEventType.AlarmConfirmed);
    END_METHOD

    METHOD OnAlarmRaised
    VAR_INPUT
        fbEvent : REFERENCE TO FB_TcEvent;
    END_VAR
    (* Callback run from THIS^.Execute() *)
    nCntAlarmsRaised := nCntAlarmsRaised + 1;
    StoreEvent(fbEvent, eEventType:=E_LogEventType.AlarmRaised);
    END_METHOD

    METHOD OnMessageSent
    VAR_INPUT
        fbEvent : REFERENCE TO FB_TcEvent;
    END_VAR
    (* Callback run from THIS^.Execute() *)
    nCntMessagesSent := nCntMessagesSent + 1;
    StoreEvent(fbEvent, eEventType:=E_LogEventType.MessageSent);
    END_METHOD

    METHOD PublishEvents : HRESULT
    VAR
        nEvent				:	UINT;
        stPendingEvent		:	REFERENCE TO ST_PendingEvent;
        stEventInfo			:	REFERENCE TO ST_LoggingEventInfo;
        fbRequestEventText	:	REFERENCE TO FB_RequestEventText;
    END_VAR

    VAR_INST
        fbJson				:	FB_JsonSaxWriter;
        fbJsonDataType		:	FB_JsonReadWriteDataType;
        sJsonDoc			:	STRING(10000);

    END_VAR
    IF nPendingEvents = 0 THEN
        RETURN;
    END_IF

    FOR nEvent := 0 TO nMaxEvents - 1 DO
        stPendingEvent REF= stPendingEvents[nEvent];
        IF NOT stPendingEvent.bInUse THEN
            CONTINUE;
        END_IF

        fbRequestEventText REF= stPendingEvent.fbRequestEventText;
        stEventInfo 	   REF= stPendingEvent.stEventInfo;

        IF fbRequestEventText.bError THEN
            stEventInfo.Msg := '(Unable to retrieve message)';
        ELSIF NOT fbRequestEventText.bBusy THEN
            fbRequestEventText.GetString(stEventInfo.msg, SIZEOF(stEventInfo.msg));
        ELSE
            CONTINUE;
        END_IF

        IF bConfigured THEN
            stEventInfo.plc := GVL_Logger.sPlcHostname;

            // Generate the JSON message
            fbJson.ResetDocument();
            fbJsonDataType.AddJsonValueFromSymbol(fbJson, 'ST_LoggingEventInfo', SIZEOF(stEventInfo), ADR(stEventInfo));
            fbJson.CopyDocument(sJsonDoc, SIZEOF(sJsonDoc));

            SendMessage(sMessage:=ADR(sJsonDoc));
        END_IF

        // Mark as not in use, and fill in this event in the next StoreEvent call
        nPendingEvents			:= nPendingEvents - 1;
        stPendingEvent.bInUse 	:= FALSE;
        nEventIdx 				:= nEvent;

    END_FOR
    END_METHOD

    METHOD SendMessage : HRESULT
    VAR_INPUT
        sMessage		:	POINTER TO STRING;
    END_VAR

    VAR
        sLogStr			:	T_MaxString;
    END_VAR
    (* For subclasses to override, if necessary *)
    IF sMessage = 0 THEN
        RETURN;
    END_IF

    // Optionally log it to Visual Studio's message list
    IF bLogToVisualStudio THEN
         // Keep the message length under 255 (extended string function for LEFT/MID do not exist)
        STRNCPY(ADR(sLogStr), sMessage, MIN(220, SIZEOF(sLogStr)));
        ADSLOGSTR(
            msgCtrlMask := ADSLOG_MSGTYPE_HINT,
            msgFmtStr   := '[Logger JSON Debug] %s',
            strArg      := sLogStr
        );
    END_IF

    IF fbSocket <> 0 THEN
        // And send it along to logstash
        F_SendUDPMessage(sMessage:=sMessage, fbSocket:=fbSocket^,
                         sHost:=GVL_Logger.cLogHost, iPort:=GVL_Logger.iLogPort);
    END_IF
    END_METHOD

    METHOD PRIVATE StoreEvent : HRESULT
    VAR_INPUT
        fbEvent		:	REFERENCE TO FB_TcEvent;
        eEventType	:	E_LogEventType;
    END_VAR

    VAR
        stPendingEvent	:	REFERENCE TO ST_PendingEvent;
        stEventInfo		:	REFERENCE TO ST_LoggingEventInfo;
        nFailures		:	UINT := 0;
    END_VAR
    IF fbEvent.eSeverity < eMinSeverity THEN
        // Ignore all messages below the minimum severity
        RETURN;
    ELSIF NOT __ISVALIDREF(fbEvent) THEN
        RETURN;
    END_IF

    // Find the next slot to use in stPendingEvents
    WHILE stPendingEvents[nEventIdx].bInUse AND nFailures < nMaxEvents DO
        nFailures := nFailures + 1;
        IF ((nEventIdx := (nEventIdx + 1)) = nMaxEvents) THEN
            nEventIdx := 0;
        END_IF
    END_WHILE

    IF (nFailures = nMaxEvents) THEN
        ADSLOGSTR(
            msgCtrlMask := ADSLOG_MSGTYPE_ERROR,
            msgFmtStr   := 'Logging message buffer full (%s)',
            strArg		:= UINT_TO_STRING(nMaxEvents),
        );
        RETURN;
    END_IF

    nPendingEvents			:=	nPendingEvents + 1;
    nCntMessagesSent 		:=	nCntMessagesSent + 1;

    stPendingEvent			REF= stPendingEvents[nEventIdx];
    stEventInfo 			REF= stPendingEvent.stEventInfo;
    stPendingEvent.bInUse 	:= TRUE;

    stEventInfo.id 			:= fbEvent.nEventId;
    stEventInfo.event_class	:= GUID_TO_STRING(fbEvent.EventClass);
    stEventInfo.severity 	:= fbEvent.eSeverity;
    stEventInfo.ts			:= F_ConvertTicksToUnixTimestamp(fbEvent.nTimestamp);
    stEventInfo.source 		:= fbEvent.ipSourceInfo.sName;
    stEventInfo.event_type	:= eEventType;

    fbEvent.GetJsonAttribute(stEventInfo.json, SIZEOF(stEventInfo.json));
    stPendingEvent.fbRequestEventText.Request(eventClass:=fbEvent.EventClass, nEventId:=fbEvent.nEventId, nLangId:=1033, ipArgs:=fbEvent.ipArguments);
    END_METHOD

    {attribute 'analysis' := '-33'}
    PROPERTY PUBLIC LogToVisualStudio : BOOL
    VAR
    END_VAR
    LogToVisualStudio := bLogToVisualStudio;
    END_PROPERTY

    {attribute 'analysis' := '-33'}
    PROPERTY PUBLIC LogToVisualStudio : BOOL
    VAR
        bValue : BOOL;
    END_VAR
    THIS^.bLogToVisualStudio := bValue;
    END_PROPERTY


Related:
    * `F_ConvertTicksToUnixTimestamp`_
    * `F_SendUDPMessage`_
    * `GVL_Logger`_
    * `ST_PendingEvent`_


FB_LogHandler
^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_LogHandler
    VAR_INPUT

        {attribute 'pytmc' := '
            pv: ADS
        '}
        fbTcAdsListener : FB_Listener;

        {attribute 'pytmc' := '
            pv: Router
        '}
        fbTcRouterListener : FB_Listener;

        {attribute 'pytmc' := '
            pv: RTime
        '}
        fbTcRTimeListener : FB_Listener;

        {attribute 'pytmc' := '
            pv: System
        '}
        fbTcSystemListener : FB_Listener;

        {attribute 'pytmc' := '
            pv: Windows
        '}
        fbWindowsListener : FB_Listener;

        {attribute 'pytmc' := '
            pv: LCLS
        '}
        fbLCLSListener 	: FB_Listener;

    END_VAR
    VAR_OUTPUT
    END_VAR

    VAR

        bInitialized	:	BOOL	:= FALSE;
        bReadyToLog		: 	BOOL	:= FALSE;
        rtFirstLog		: 	R_TRIG;

        fbGetHostName 	: FB_GetPLCHostname;
        fbGetIP		 	: FB_GetPLCIPAddress;

        fbListener		:	REFERENCE TO FB_Listener;
        fbListeners		: 	ARRAY [0..nNumListeners - 1] OF POINTER TO FB_Listener;

        // Default minimum severity for subscriptions
        eMinSeverity	:	TcEventSeverity := TcEventSeverity.Verbose;

        {attribute 'naming' := 'omit'}
        rtReset				:	R_TRIG; //Reset trigger
        bReset				:	BOOL;

        fbSocket	   		:	FB_ConnectionlessSocket;

        nI					:	UINT;

        SocketEnable : BOOL;

        ctuSocketError : CTU := (PV:=3); // Circuit breaker for socket errors. 3 errors before it stops.

        tRetryConnection : TON := (PT:=T#1h); // Retry after an hour

        tofTrickleBreakerPre : TOF := (PT:=T#1s);
        tonTrickleBreaker : TON := (PT := GVL_Logger.nTrickleTripTime);
        bTripCon : BOOL;
    END_VAR

    VAR CONSTANT
        nNumListeners		:	UINT	:= 6;
    END_VAR
    IF NOT bInitialized THEN
        bInitialized := TRUE;
        fbTcAdsListener.Configure(i_EventClass:=TC_EVENT_CLASSES.TcGeneralAdsEventClass, i_MinSeverity:=eMinSeverity, i_fbSocket:=ADR(fbSocket));
        fbTcRouterListener.Configure(i_EventClass:=TC_EVENT_CLASSES.TcRouterEventClass, i_MinSeverity:=eMinSeverity, i_fbSocket:=ADR(fbSocket));
        fbTcRTimeListener.Configure(i_EventClass:=TC_EVENT_CLASSES.TcRTimeEventClass, i_MinSeverity:=eMinSeverity, i_fbSocket:=ADR(fbSocket));
        fbTcSystemListener.Configure(i_EventClass:=TC_EVENT_CLASSES.TcSystemEventClass, i_MinSeverity:=eMinSeverity, i_fbSocket:=ADR(fbSocket));
        fbWindowsListener.Configure(i_EventClass:=TC_EVENT_CLASSES.Win32EventClass, i_MinSeverity:=eMinSeverity, i_fbSocket:=ADR(fbSocket));
        fbLCLSListener.Configure(i_EventClass:=TC_EVENT_CLASSES.LCLSGeneralEventClass, i_MinSeverity:=eMinSeverity, i_fbSocket:=ADR(fbSocket));

        fbListeners[0] := ADR(fbTcAdsListener);
        fbListeners[1] := ADR(fbTcRouterListener);
        fbListeners[2] := ADR(fbTcRTimeListener);
        fbListeners[3] := ADR(fbTcSystemListener);
        fbListeners[4] := ADR(fbWindowsListener);
        fbListeners[5] := ADR(fbLCLSListener);

    END_IF

    fbGetHostName(
        bEnable := TRUE,
        sHostname => GVL_Logger.sPlcHostname,
    );

    fbGetIP(
        bEnable := TRUE,
        sIPAddress => fbSocket.sLocalHost
    );

    (* Ensure the socket is ready for when JSON documents are emitted *)
    rtReset(CLK:=bReset);

    IF (rtReset.Q AND fbSocket.bEnable) THEN
        fbSocket(bEnable:=FALSE);
    END_IF

    // Disable fbSocket if too many errors occur
    ctuSocketError(CU:=fbSocket.bError, RESET:=tRetryConnection.Q OR rtReset.Q);
    SocketEnable R= ctuSocketError.Q;
    // Retry an hour later
    tRetryConnection(IN:=ctuSocketError.Q);
    SocketEnable S= tRetryConnection.Q OR rtReset.Q;

    fbSocket(
        nLocalPort:=0,
        bEnable:=fbGetIP.bDone,
        nMode:=CONNECT_MODE_ENABLEDBG,
    );

    bReadyToLog := (
        fbGetHostName.bDone AND
        fbGetIP.bDone AND
        bInitialized AND
        fbSocket.bEnable AND
        NOT fbSocket.bError AND
        fbSocket.eState = E_SocketConnectionlessState.eSOCKET_CREATED
    );
    rtFirstLog(CLK:=bReadyToLog);

    IF rtFirstLog.Q THEN
        fbRootLogger(sMsg:='Logging system online', eSevr:=TcEventSeverity.Info,
                     eSubsystem:=E_Subsystem.NILVALUE);
    END_IF

    CircuitBreaker();

    (* Poke all of the listeners *)
    FOR nI := 0 TO nNumListeners - 1 DO
        fbListener REF= fbListeners[nI]^;
        fbListener.Execute();
        fbListener.PublishEvents();
    END_FOR

    END_FUNCTION_BLOCK

    ACTION CircuitBreaker:
    // Global log circuit breaker
    (*
    Logic explanation
    We want to trip if there is a constant stream of messages being emitted by this PLC. We also
    only want the noisy offenders to trip. To target them we set a global trickle tripped flag
    using this logic here. Then each individual FB_LogMessage evaluates itself to see if it's
    sending a message too frequently (ie. it's being called to often).

    This logic is attempting to implement the following:
    1. Trip if the total events exceeds the nTrickleThreshold for >10s
    2. Sustain the timer if the event count drops for a handful of cycles since usually a cycle amounts to 10ms, losing a few
    should not stop the trickle timer.
    *)
    bTripCon := GVL_Logger.nGlobAccEvents >0;

    tofTrickleBreakerPre(IN:=bTripCon);
    tonTrickleBreaker(IN:=tofTrickleBreakerPre.Q);

    GVL_Logger.bTrickleTripped S= tonTrickleBreaker.Q AND bTripCon;

    GVL_Logger.nGlobAccEvents := 0; // reset the count for the next cycle
    END_ACTION


Related:
    * `E_Subsystem`_
    * `FB_GetPLCHostname`_
    * `FB_GetPLCIPAddress`_
    * `FB_Listener`_
    * `FB_LogMessage`_
    * `GVL_Logger`_


FB_LogMessage
^^^^^^^^^^^^^

::

    {attribute 'reflection'}
    FUNCTION_BLOCK FB_LogMessage
    VAR_INPUT
        sMsg			: T_MaxString; 					// Message to send
        eSevr			: TcEventSeverity	:= TcEventSeverity.Verbose;
        eSubsystem		: E_Subsystem; 					// Subsystem
        sJson			: STRING(7000)	:= '{}';		// JSON to add to the message

        //Circuit breaker settings
        nMinTimeViolationAcceptable : INT := GVL_Logger.nMinTimeViolationAcceptable; // How many times the min. time can be violated before the CB trips
        nLocalTripThreshold : TIME := GVL_Logger.nLocalTripThreshold; // Minimum time between calls allowed, pairs with nMinTimeViolationAcceptable
        nTrickleTripThreshold : TIME := GVL_Logger.nLocalTrickleTripThreshold; // Trickle trip, activated by global threshold, should be >> LocalTripThreshold
        nTripResetPeriod : TIME := GVL_Logger.nTripResetPeriod; // Time for auto-reset
        bEnableAutoReset : BOOL := TRUE; //Enable circuit breaker auto-reset (true by default)
    END_VAR

    VAR_OUTPUT
    END_VAR

    VAR
        bInitialized		:	BOOL := FALSE;
        bInitFailed			:	BOOL := FALSE;
        sSubsystemSource	:	STRING;
        fbMessage			:	REFERENCE TO FB_TcMessage;
        fbMessages 			:	ARRAY [0..4] OF FB_TcMessage;
        fbSource 			:	FB_TcSourceInfo;
        ipResultMessage	 	:	I_TcMessage;
        hr 					:	HRESULT;
        hrLastInternalError :	HRESULT;
        eTraceLevel 		:	TcEventSeverity := TcEventSeverity.Verbose;
        bFirstCall : BOOL := TRUE;

        {attribute 'instance-path'}
        {attribute 'noinit'}
        sPath				:	T_MaxString;

        // Circuit breaker
        ///////////////////////////////
            nTimesViolated : INT;

            LastCallTime : ULINT;
            CurrentCallTime : ULINT;
            DeltaSinceLastCall : ULINT;

            WhenTripsCleared : ULINT;
            ftTrippedReleased : F_TRIG;

            bLocalTrickleTripped : BOOL;
            bLocalTripped : BOOL;

            {attribute 'pytmc' := '
                pv: Tripped
                io: i
                field: DESC Log message FB tripped
            '}
            bTripped    :    BOOL; // Won't emit messages if true
            {attribute 'pytmc' := '
                pv: Reset
                io: o
                field: DESC Rising-edge reset of trip
            '}
            bResetBreaker : BOOL;
            rtResetBreaker : R_TRIG;

            rtTripped : R_TRIG;
        ////////////////////////////////////////////

    END_VAR
    IF NOT bInitialized AND NOT bInitFailed THEN

        hr := fbMessages[TC_EVENTS.LCLSGeneralEventClass.Verbose.nEventId].CreateEx(TC_EVENTS.LCLSGeneralEventClass.Verbose, 0 (*fbSource*) );
        IF FAILED(hr) THEN
            bInitFailed := TRUE;
            hrLastInternalError := hr;
        END_IF

        hr := fbMessages[TC_EVENTS.LCLSGeneralEventClass.Warning.nEventId].CreateEx(TC_EVENTS.LCLSGeneralEventClass.Warning, 0 (*fbSource*) );
        IF FAILED(hr) THEN
            bInitFailed := TRUE;
            hrLastInternalError := hr;
        END_IF

        hr := fbMessages[TC_EVENTS.LCLSGeneralEventClass.Info.nEventId].CreateEx(TC_EVENTS.LCLSGeneralEventClass.Info, 0 (*fbSource*) );
        IF FAILED(hr) THEN
            bInitFailed := TRUE;
            hrLastInternalError := hr;
        END_IF

        hr := fbMessages[TC_EVENTS.LCLSGeneralEventClass.Error.nEventId].CreateEx(TC_EVENTS.LCLSGeneralEventClass.Error, 0 (*fbSource*) );
        IF FAILED(hr) THEN
            bInitFailed := TRUE;
            hrLastInternalError := hr;
        END_IF

        hr := fbMessages[TC_EVENTS.LCLSGeneralEventClass.Critical.nEventId].CreateEx(TC_EVENTS.LCLSGeneralEventClass.Critical, 0 (*fbSource*) );
        IF FAILED(hr) THEN
            bInitFailed := TRUE;
            hrLastInternalError := hr;
        END_IF

        IF bInitFailed THEN
            ADSLOGSTR(
                msgCtrlMask := ADSLOG_MSGTYPE_ERROR,
                msgFmtStr   := '[LOGGER] Initialization failed in %s',
                strArg      := sPath,
            );
        ELSE
            bInitialized := TRUE;
        END_IF
    END_IF

    IF bInitFailed THEN
        RETURN;
    END_IF


    ///////////////////////////////////////
    // Log message circuit breaker

    CircuitBreaker();
    IF bTripped AND NOT rtTripped.Q THEN RETURN; END_IF // Pass on the first one to deliver the message we're going silent

    ///////////////////////////////////////////////////////////



    // Map the message severity to the LCLSGeneralEventClass:
    CASE eSevr OF
        TcEventSeverity.Verbose: 	fbMessage REF= fbMessages[TC_EVENTS.LCLSGeneralEventClass.Verbose.nEventId];
        TcEventSeverity.Warning:	fbMessage REF= fbMessages[TC_EVENTS.LCLSGeneralEventClass.Warning.nEventId];
        TcEventSeverity.Info:		fbMessage REF= fbMessages[TC_EVENTS.LCLSGeneralEventClass.Info.nEventId];
        TcEventSeverity.Error:		fbMessage REF= fbMessages[TC_EVENTS.LCLSGeneralEventClass.Error.nEventId];
        TcEventSeverity.Critical:	fbMessage REF= fbMessages[TC_EVENTS.LCLSGeneralEventClass.Critical.nEventId];
        ELSE
            RETURN;
    END_CASE

    CASE eSubsystem OF
        E_Subsystem.FIELDBUS: 	sSubsystemSource := '/Fieldbus';
        E_Subsystem.MOTION: 	sSubsystemSource := '/Motion';
        E_Subsystem.MPS: 		sSubsystemSource := '/MPS';
        E_Subsystem.SDS: 		sSubsystemSource := '/SDS';
        E_Subsystem.VACUUM:		sSubsystemSource := '/Vacuum';
        E_Subsystem.OPTICS:     sSubsystemSource := '/Optics';
        ELSE
            sSubsystemSource := '/Unknown';
    END_CASE

    // Clearing the source here will clear the event GUID, causing the message to not be resolved.
    // However, we can change the name as desired:
    //fbSource.Clear();
    fbSource.sName := CONCAT(sPath, sSubsystemSource);

    ipResultMessage := fbMessage;
    hr := fbMessage.CreateEx(stEventEntry:=ipResultMessage.stEventEntry, ipSourceInfo:=fbSource);

    // This is where the message text gets appended:
    fbMessage.ipArguments.Clear();

    IF rtTripped.Q THEN
       fbMessage.ipArguments.AddString('Logging circuit breaker tripped, this will be the last message from this element for a while...');
    ELSIF NOT bTripped THEN
        fbMessage.ipArguments.AddString(sMsg);
    END_IF


    IF LEN(sJson) = 0 THEN
        // Ensure there's a valid JSON string here
        sJson := '{}';
    END_IF

    fbMessage.SetJsonAttribute(sJson);

    // For a final format of:
    // 'Path.to.FB_LogMessage/Subsystem': {Unknown,Error,Warning,Verbose} (message)
    // We want to send 1 more message when we trip
    IF NOT FAILED(hr) AND fbMessage.eSeverity >= eTraceLevel AND (NOT bTripped OR rtTripped.Q) THEN
        hr := fbMessage.Send(0);
    END_IF

    IF FAILED(hr) THEN
        hrLastInternalError := hr;
    END_IF

    END_FUNCTION_BLOCK

    ACTION CircuitBreaker:
    GVL_Logger.nGlobAccEvents := GVL_Logger.nGlobAccEvents + 1;

    CurrentCallTime := F_GetTaskTime();
    IF bFirstCall THEN
        DeltaSinceLastCall := 16#FFFF_FFFF;
        bFirstCall := FALSE;
    ELSE
       DeltaSinceLastCall := CurrentCallTime - LastCallTime;
    END_IF

    LastCallTime := CurrentCallTime;

    ftTrippedReleased(CLK:=bLocalTripped OR bLocalTrickleTripped);
    IF ftTrippedReleased.Q THEN
        WhenTripsCleared := CurrentCallTime;
    END_IF
    rtResetBreaker(CLK:=bResetBreaker OR
                    bEnableAutoReset AND (CurrentCallTime - WhenTripsCleared > TIME_TO_100NS(nTripResetPeriod)) );

    IF rtResetBreaker.Q THEN
       // bLocalTrickleTripped := FALSE;
        //bLocalTripped := FALSE;
        bTripped := FALSE;
        //nTimesViolated := 0;
    END_IF

    bResetBreaker := FALSE;

    IF DeltaSinceLastCall < TIME_TO_100NS(nLocalTripThreshold) THEN
        nTimesViolated := MIN(nTimesViolated + 1, nMinTimeViolationAcceptable+1);
    ELSE
        nTimesViolated := MAX(nTimesViolated - 1, 0);
    END_IF

    bLocalTripped := nTimesViolated > nMinTimeViolationAcceptable;

    bLocalTrickleTripped := DeltaSinceLastCall < TIME_TO_100NS(nTrickleTripThreshold) AND GVL_LOGGER.bTrickleTripped;

    bTripped S= bLocalTrickleTripped OR bLocalTripped;
    rtTripped(CLK:=bTripped);
    END_ACTION


Related:
    * `E_Subsystem`_
    * `GVL_Logger`_
    * `TIME_TO_100NS`_


FB_LREALBuffer
^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_LREALBuffer
    (*
        An example use of FB_DataBuffer for the likely most-common use case.
        2019-10-09 Zachary Lentz
    *)
    VAR_INPUT
        // If TRUE, we'll accumulate a value on this cycle.
        bExecute: BOOL;
        // The value to accumulate.
        fInput: LREAL;
    END_VAR
    VAR_OUTPUT
        arrOutput: ARRAY [1..1000] OF LREAL;
        bNewArray: BOOL;
    END_VAR
    VAR
        arrPartial: ARRAY [1..1000] OF LREAL;
        fbDataBuffer: FB_DataBuffer;
    END_VAR
    fbDataBuffer(
        bExecute := bExecute,
        pInputAdr := ADR(fInput),
        iInputSize := SIZEOF(fInput),
        iElemCount := 1000,
        pPartialAdr := ADR(arrPartial),
        pOutputAdr := ADR(arrOutput),
        bNewArray => bNewArray);

    END_FUNCTION_BLOCK


Related:
    * `FB_DataBuffer`_


FB_LREALFromEPICS
^^^^^^^^^^^^^^^^^

::

    (*
    Function block to link an analog value from EPICS to an LREAL on the PLC

    Usage:

        {attribute 'pytmc' := '
            pv: INTERNAL:RECORD
            link: PV:NAME:TO:LINK:TO
        '}
        fbLinkedValue1 : FB_LREALFromEPICS;

    Such that when PV:NAME:TO:LINK:TO changes in EPICS, the INTERNAL:RECORD will be used to
    push a value through to the PLC with this function block.

    As this block takes care of IOC heartbeat signals and monitors the link and value severity,
    the end-user should then only have to look at `.bValid` and `.fValue`. These are guaranteed to
    be up-to-date and valid within `tTimeout` seconds.
    *)

    FUNCTION_BLOCK FB_LREALFromEPICS

    VAR_INPUT
        iMaximumValidSeverity		: INT := 1;
    END_VAR

    VAR_OUTPUT
        bValid				: BOOL;
        fValue				: LREAL;
    END_VAR

    VAR
        iValueInvalidate	: POINTER TO ULINT;
        tonValueTimeout		: TON;
        tonSeverityTimeout	: TON;

        fLastValidValue 	: LREAL;
        iLastValidSeverity 	: INT;

        {attribute 'pytmc' := '
            pv: EPICSLink
            link:
            field: DESC Internal variable used to monitor EPICS PV in PLC
        '}
        fPLCInternalValue : LREAL;

        // Use special link syntax for now to get EPICSLink.SEVR here:
        {attribute 'pytmc' := '
            pv: EPICSLink:LinkSeverity
            link: *EPICSLink.SEVR
            field: DESC Internal variable used to monitor EPICS PV severity in PLC
        '}
        iPLCInternalSeverity : INT;

    END_VAR
    VAR CONSTANT
        // The timeout will trip after `tTimeout` if EPICS doesn't write in that time period:
        tTimeout			: TIME := T#2S;
        NAN_VALUE   		: ULINT := 16#7f_ff_ff_ff__ff_ff_ff_ff;
    END_VAR
    iValueInvalidate := ADR(fPLCInternalValue);

    IF iPLCInternalSeverity <> -1 THEN
        // New severity value
        iLastValidSeverity := iPLCInternalSeverity;
        iPLCInternalSeverity := -1;

         // Reset the timer
        tonSeverityTimeout(IN:=FALSE);
        tonSeverityTimeout(IN:=TRUE, PT:=tTimeout);
    END_IF

    IF iValueInvalidate^ <> NAN_VALUE THEN
        // New value from EPICS
        fLastValidValue 	:= fPLCInternalValue;
        iValueInvalidate^	:= NAN_VALUE;

        // Reset the timer
        tonValueTimeout(IN:=FALSE);
        tonValueTimeout(IN:=TRUE, PT:=tTimeout);
    END_IF

    tonValueTimeout();
    tonSeverityTimeout();
    bValid := (NOT tonValueTimeout.Q) AND
              (NOT tonSeverityTimeout.Q) AND
              (iLastValidSeverity <= iMaximumValidSeverity);
    fValue := fLastValidValue;

    END_FUNCTION_BLOCK




FB_STRINGFromEPICS
^^^^^^^^^^^^^^^^^^

::

    (*
    Function block to link an analog value from EPICS to a STRING on the PLC

    Usage:

        {attribute 'pytmc' := '
            pv: INTERNAL:RECORD
            link: PV:NAME:TO:LINK:TO.VAL@
        '}
        fbLinkedValue1 : FB_STRINGFromEPICS;

    Such that when PV:NAME:TO:LINK:TO.VAL$ changes in EPICS, the INTERNAL:RECORD will be used to
    push a value through to the PLC with this function block.

    The usage of "@" above has several layers of complexity, which are not strictly necessary to
    understand in order to use this function block. The reasons are as follows:

        1. TwinCAT does not allow the "$" symbol to be used in pragmas.  pytmc uses "@" as an
           alternate character for "$" as the latter is more frequently useful.
        2. The "$" is used as a suffix for EPICS string PVs where accessing strings over
           the standard 40 character length is desirable.
        3. Combining the above, using "PV.VAL@" we can direct EPICS to use long string
           handling over the Channel Access link to the link PV ("PV.VAL$").

    As this block takes care of IOC heartbeat signals and monitors the link and value severity,
    the end-user should then only have to look at `.bValid` and `.sValue`. These are guaranteed to
    be up-to-date and valid within `tTimeout` seconds.
    *)

    FUNCTION_BLOCK FB_STRINGFromEPICS

    VAR_INPUT
        iMaximumValidSeverity		: INT := 1;
    END_VAR

    VAR_OUTPUT
        bValid				: BOOL;
        sValue				: STRING;
    END_VAR

    VAR
        tonValueTimeout		: TON;
        tonSeverityTimeout	: TON;

        sLastValidValue 	: STRING;
        iLastValidSeverity 	: INT;

        {attribute 'pytmc' := '
            pv: EPICSLink
            link:
            field: DESC Internal variable used to monitor EPICS PV in PLC
        '}
        sPLCInternalValue : STRING;

        // Use special link syntax for now to get EPICSLink.SEVR here:
        {attribute 'pytmc' := '
            pv: EPICSLink:Sevr
            link: *EPICSLink.SEVR
            field: DESC Internal variable used to monitor EPICS PV severity in PLC
        '}
        iPLCInternalSeverity : INT;

    END_VAR
    VAR CONSTANT
        // The timeout will trip after `tTimeout` if EPICS doesn't write in that time period:
        tTimeout			: TIME := T#2S;
        // This is an arbitrary extended UUID.  If you want to store this value in your IOC
        // and use it with this function block - well, too bad.
        sUnsetString   		: STRING := '857be58e-5b82-4731-935f-e0e9cdfe005d-50b69a17-1499-49bc-af7b-084a8b6f63dd';
    END_VAR
    IF iPLCInternalSeverity <> -1 THEN
        // New severity value
        iLastValidSeverity := iPLCInternalSeverity;
        iPLCInternalSeverity := -1;

         // Reset the timer
        tonSeverityTimeout(IN:=FALSE);
        tonSeverityTimeout(IN:=TRUE, PT:=tTimeout);
    END_IF

    IF sPLCInternalValue <> sUnsetString THEN
        // New value from EPICS
        sLastValidValue 	:= sPLCInternalValue;
        sPLCInternalValue	:= sUnsetString;

        // Reset the timer
        tonValueTimeout(IN:=FALSE);
        tonValueTimeout(IN:=TRUE, PT:=tTimeout);
    END_IF

    tonValueTimeout();
    tonSeverityTimeout();
    bValid := (NOT tonValueTimeout.Q) AND
              (NOT tonSeverityTimeout.Q) AND
              (iLastValidSeverity <= iMaximumValidSeverity);
    sValue := sLastValidValue;

    END_FUNCTION_BLOCK




FB_TempSensor
^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_TempSensor
    (*
        Handles scaling and default diagnostics for temperature sensors,
        such as thermocouples, RTDs, and others.
        2020-03-02 Zachary Lentz
    *)
    VAR_INPUT
        // Resolution parameter from the Beckhoff docs. Default is 0.1 for 0.1 degree resolution
        fResolution: LREAL := 0.1;
    END_VAR
    VAR_OUTPUT
        {attribute 'pytmc' := '
            pv: TEMP
            io: input
            field: EGU C
            field: PREC 2
        '}
        fTemp: LREAL;

        {attribute 'pytmc' := '
            pv: CONN
            io: input
            field: ONAM Connected
            field: ZNAM Disconnected
        '}
        bConnected: BOOL;

        {attribute 'pytmc' := '
            pv: ERR
            io: input
            field: ONAM True
            field: ZNAM False
        '}
        bError AT %I*: BOOL := TRUE;

        bUnderrange AT %I*: BOOL;
        bOverrange AT %I*: BOOL;
    END_VAR
    VAR
        iRaw AT %I*: INT;
    END_VAR
    // The manual states that we are disconnected if we are both overrange and in an error state
    bConnected := NOT (bOverrange AND bError);
    fTemp := INT_TO_LREAL(iRaw) * fResolution;

    END_FUNCTION_BLOCK




FB_Test_EpicsCentroidMonitor
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    {attribute 'call_after_init'}
    FUNCTION_BLOCK FB_Test_EpicsCentroidMonitor EXTENDS TcUnit.FB_TestSuite
    VAR_INPUT
    END_VAR
    VAR_OUTPUT
    END_VAR
    VAR
    END_VAR
    TestBasics();

    END_FUNCTION_BLOCK

    METHOD TestBasics
    VAR_INPUT
    END_VAR
    VAR_INST
        fbMonitor : FB_EpicsCentroidMonitor;
        nCount : INT := 0;
    END_VAR
    TEST('Basic');

    nCount := 0;

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.0, fY:=0.0, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0S);
    fbMonitor();

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.1, fY:=0.1, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0S);
    fbMonitor(fMinimumValidChange:=0.0);

    AssertTrue(fbMonitor.bIsUpdating, Message:='Centroid not reporting updating');

    TEST_FINISHED();

    TEST('Min change');

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.0, fY:=0.0, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0S);
    fbMonitor(fMinimumValidChange:=0.1, fMaximumFrameTime:=0.2);

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.05, fY:=0.05, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0.1S);
    fbMonitor(fMinimumValidChange:=0.1, fMaximumFrameTime:=0.2);
    AssertFalse(fbMonitor.bIsUpdating, Message:='Centroid update below threshold and is not updating');

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.5, fY:=0.5, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0.1S);
    fbMonitor(fMinimumValidChange:=0.1, fMaximumFrameTime:=0.2);
    AssertTrue(fbMonitor.bIsUpdating, Message:='Centroid update above threshold and is updating');

    TEST_FINISHED();

    TEST('Severity invalidation');

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.0, fY:=0.0, nCount:=nCount, nX_Severity:=2, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0.2S);
    fbMonitor(fMinimumValidChange:=0.0);
    AssertFalse(fbMonitor.bValid, Message:='Invalid data - x severity');

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.1, fY:=0.1, nCount:=nCount, nX_Severity:=0, nY_Severity:=2, nCount_Severity:=0, tLastUpdate:=T#0.2S);
    fbMonitor(fMinimumValidChange:=0.0);
    AssertFalse(fbMonitor.bValid, Message:='Invalid data - y severity');

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.2, fY:=0.2, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=2, tLastUpdate:=T#0.2S);
    fbMonitor(fMinimumValidChange:=0.0);
    AssertFalse(fbMonitor.bValid, Message:='Invalid data - count severity');

    nCount := nCount + 1;
    WriteCentroidValue(fbMonitor:=fbMonitor, fX:=0.0, fY:=0.0, nCount:=nCount, nX_Severity:=0, nY_Severity:=0, nCount_Severity:=0, tLastUpdate:=T#0.1S);
    fbMonitor(fMinimumValidChange:=0.0);
    AssertTrue(fbMonitor.bValid, Message:='Data valid');
    AssertTrue(fbMonitor.bIsUpdating, Message:='Data valid and updating');

    TEST_FINISHED();
    END_METHOD

    METHOD WriteCentroidValue
    VAR_IN_OUT
        fbMonitor : FB_EpicsCentroidMonitor;
    END_VAR
    VAR_INPUT
        fX : LREAL;
        fY : LREAL;
        nCount : INT;
        nX_Severity : INT := 0;
        nY_Severity : INT := 0;
        nCount_Severity : INT := 0;
        tLastUpdate : TIME;
    END_VAR
    WRITE_PROTECTED_INT(ADR(fbMonitor.fbCentroidX.iPLCInternalSeverity), nX_Severity);
    WRITE_PROTECTED_INT(ADR(fbMonitor.fbCentroidY.iPLCInternalSeverity), nY_Severity);
    WRITE_PROTECTED_INT(ADR(fbMonitor.fbArrayCounter.iPLCInternalSeverity), nCount_Severity);

    WRITE_PROTECTED_LREAL(ADR(fbMonitor.fbCentroidX.fPLCInternalValue), fX);
    WRITE_PROTECTED_LREAL(ADR(fbMonitor.fbCentroidY.fPLCInternalValue), fY);
    WRITE_PROTECTED_LREAL(ADR(fbMonitor.fbArrayCounter.fPLCInternalValue), INT_TO_LREAL(nCount));

    WRITE_PROTECTED_TIME(ADR(fbMonitor.tLastUpdate), TIME() - tLastUpdate);
    END_METHOD


Related:
    * `FB_EpicsCentroidMonitor`_


FB_Test_EpicsMotorMonitor
^^^^^^^^^^^^^^^^^^^^^^^^^

::

    {attribute 'call_after_init'}
    FUNCTION_BLOCK FB_Test_EpicsMotorMonitor EXTENDS TcUnit.FB_TestSuite
    VAR_INPUT
    END_VAR
    VAR_OUTPUT
    END_VAR
    VAR
    END_VAR
    TestBasics();

    END_FUNCTION_BLOCK

    METHOD TestBasics
    VAR_INPUT
    END_VAR
    VAR_INST
        fbMonitor : FB_EpicsMotorMonitor;
    END_VAR
    TEST('Basic');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertTrue(fbMonitor.bValid, Message:='Data valid');

    TEST_FINISHED();


    TEST('Severities');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=0, nRBV_Severity:=2, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertFalse(fbMonitor.bValid, Message:='Invalid RBV');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=2, nMoving_Severity:=0);
    fbMonitor();
    AssertFalse(fbMonitor.bValid, Message:='Invalid status');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=2);
    fbMonitor();
    AssertFalse(fbMonitor.bValid, Message:='Invalid moving');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertTrue(fbMonitor.bValid, Message:='All valid');

    TEST_FINISHED();

    TEST('Moving');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0 (* DMOV *), nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertTrue(fbMonitor.bIsMoving, Message:='Moving');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=1 (* DMOV *), nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertFalse(fbMonitor.bIsMoving, Message:='Not moving');

    TEST_FINISHED();

    TEST('Position');

    WriteData(fbMonitor:=fbMonitor, fRBV:=10.0, nMoving:=0, nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertEquals_LREAL(Actual:=fbMonitor.fPosition, Expected:=10.0, Delta:=0.1, Message:='Position 1 OK');

    WriteData(fbMonitor:=fbMonitor, fRBV:=20.0, nMoving:=0, nStatus:=0, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();
    AssertEquals_LREAL(Actual:=fbMonitor.fPosition, Expected:=20.0, Delta:=0.1, Message:='Position 2 OK');

    TEST_FINISHED();

    TEST('MSTA set');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=16#0FFFF, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();

    AssertTrue(fbMonitor.stMSTA.bPositiveDirection, Message:='bPositiveDirection True');
    AssertTrue(fbMonitor.stMSTA.bDone, Message:='bDone True');
    AssertTrue(fbMonitor.stMSTA.bPlusLimitSwitch, Message:='bPlusLimitSwitch True');
    AssertTrue(fbMonitor.stMSTA.bHomeLimitSwitch, Message:='bHomeLimitSwitch True');
    AssertTrue(fbMonitor.stMSTA.bUnused0, Message:='bUnused0 True');
    AssertTrue(fbMonitor.stMSTA.bClosedLoop, Message:='bClosedLoop True');
    AssertTrue(fbMonitor.stMSTA.bSlipStall, Message:='bSlipStall True');
    AssertTrue(fbMonitor.stMSTA.bHome, Message:='bHome True');
    AssertTrue(fbMonitor.stMSTA.bEncoderPresent, Message:='bEncoderPresent True');
    AssertTrue(fbMonitor.stMSTA.bHardwareProblem, Message:='bHardwareProblem True');
    AssertTrue(fbMonitor.stMSTA.bMoving, Message:='bMoving True');
    AssertTrue(fbMonitor.stMSTA.bGainSupport, Message:='bGainSupport True');
    AssertTrue(fbMonitor.stMSTA.bCommError, Message:='bCommError True');
    AssertTrue(fbMonitor.stMSTA.bMinusLimitSwitch, Message:='bMinusLimitSwitch True');
    AssertTrue(fbMonitor.stMSTA.bHomed, Message:='bHomed True');

    TEST_FINISHED();


    TEST('MSTA zero');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=16#0000, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();

    AssertFalse(fbMonitor.stMSTA.bPositiveDirection, Message:='bPositiveDirection False');
    AssertFalse(fbMonitor.stMSTA.bDone, Message:='bDone False');
    AssertFalse(fbMonitor.stMSTA.bPlusLimitSwitch, Message:='bPlusLimitSwitch False');
    AssertFalse(fbMonitor.stMSTA.bHomeLimitSwitch, Message:='bHomeLimitSwitch False');
    AssertFalse(fbMonitor.stMSTA.bUnused0, Message:='bUnused0 False');
    AssertFalse(fbMonitor.stMSTA.bClosedLoop, Message:='bClosedLoop False');
    AssertFalse(fbMonitor.stMSTA.bSlipStall, Message:='bSlipStall False');
    AssertFalse(fbMonitor.stMSTA.bHome, Message:='bHome False');
    AssertFalse(fbMonitor.stMSTA.bEncoderPresent, Message:='bEncoderPresent False');
    AssertFalse(fbMonitor.stMSTA.bHardwareProblem, Message:='bHardwareProblem False');
    AssertFalse(fbMonitor.stMSTA.bMoving, Message:='bMoving False');
    AssertFalse(fbMonitor.stMSTA.bGainSupport, Message:='bGainSupport False');
    AssertFalse(fbMonitor.stMSTA.bCommError, Message:='bCommError False');
    AssertFalse(fbMonitor.stMSTA.bMinusLimitSwitch, Message:='bMinusLimitSwitch False');
    AssertFalse(fbMonitor.stMSTA.bHomed, Message:='bHomed False');

    TEST_FINISHED();


    TEST('MSTA something');

    WriteData(fbMonitor:=fbMonitor, fRBV:=0.0, nMoving:=0, nStatus:=2#0100_0011_0000_0110, nRBV_Severity:=0, nStatus_Severity:=0, nMoving_Severity:=0);
    fbMonitor();

    AssertFalse(fbMonitor.stMSTA.bPositiveDirection, Message:='bPositiveDirection');
    AssertTrue(fbMonitor.stMSTA.bDone, Message:='bDone');
    AssertTrue(fbMonitor.stMSTA.bPlusLimitSwitch, Message:='bPlusLimitSwitch');
    AssertFalse(fbMonitor.stMSTA.bHomeLimitSwitch, Message:='bHomeLimitSwitch');

    AssertFalse(fbMonitor.stMSTA.bUnused0, Message:='bUnused0');
    AssertFalse(fbMonitor.stMSTA.bClosedLoop, Message:='bClosedLoop');
    AssertFalse(fbMonitor.stMSTA.bSlipStall, Message:='bSlipStall');
    AssertFalse(fbMonitor.stMSTA.bHome, Message:='bHome');

    AssertTrue(fbMonitor.stMSTA.bEncoderPresent, Message:='bEncoderPresent');
    AssertTrue(fbMonitor.stMSTA.bHardwareProblem, Message:='bHardwareProblem');
    AssertFalse(fbMonitor.stMSTA.bMoving, Message:='bMoving');
    AssertFalse(fbMonitor.stMSTA.bGainSupport, Message:='bGainSupport');

    AssertFalse(fbMonitor.stMSTA.bCommError, Message:='bCommError');
    AssertFalse(fbMonitor.stMSTA.bMinusLimitSwitch, Message:='bMinusLimitSwitch');
    AssertTrue(fbMonitor.stMSTA.bHomed, Message:='bHomed');

    TEST_FINISHED();
    END_METHOD

    METHOD WriteData
    VAR_IN_OUT
        fbMonitor : FB_EpicsMotorMonitor;
    END_VAR
    VAR_INPUT
        fRBV : LREAL;
        nMoving : INT;
        nStatus : UINT;
        nRBV_Severity : INT := 0;
        nStatus_Severity : INT := 0;
        nMoving_Severity : INT := 0;
    END_VAR
    WRITE_PROTECTED_INT(ADR(fbMonitor.fbRBVCheck.iPLCInternalSeverity), nRBV_Severity);
    WRITE_PROTECTED_INT(ADR(fbMonitor.fbMotorStatusCheck.iPLCInternalSeverity), nStatus_Severity);
    WRITE_PROTECTED_INT(ADR(fbMonitor.fbMovingCheck.iPLCInternalSeverity), nMoving_Severity);

    WRITE_PROTECTED_LREAL(ADR(fbMonitor.fbRBVCheck.fPLCInternalValue), fRBV);
    WRITE_PROTECTED_LREAL(ADR(fbMonitor.fbMotorStatusCheck.fPLCInternalValue), UINT_TO_LREAL(nStatus));
    WRITE_PROTECTED_LREAL(ADR(fbMonitor.fbMovingCheck.fPLCInternalValue), INT_TO_LREAL(nMoving));
    END_METHOD


Related:
    * `FB_EpicsMotorMonitor`_


FB_ThermoCouple
^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_ThermoCouple
    (*
        Deprecated as of 2020-03-02, please use FB_TempSensor instead
        2019-10-09 Zachary Lentz
    *)
    {warning 'Function Block FB_ThermoCouple is deprecated and may be removed in a future release'}
    VAR_INPUT
        // Ratio between raw value and actual temperature. Default is 10 for 10 steps per degree (or 0.1 degree resolution)
        iScale: INT := 10;
    END_VAR
    VAR_OUTPUT
        {attribute 'pytmc' := '
            pv: STC:TEMP
            io: input
        '}
        fTemp: LREAL;

        {attribute 'pytmc' := '
            pv: STC:CONN
            io: input
            field: ONAM Connected
            field: ZNAM Disconnected
        '}
        bConnected: BOOL;

        {attribute 'pytmc' := '
            pv: STC:ERR
            io: input
        '}
        bError AT %I*: BOOL;

        bUnderrange AT %I*: BOOL;
        bOverrange AT %I*: BOOL;
    END_VAR
    VAR
        iRaw AT %I*: INT;
    END_VAR
    // The manual states that we are disconnected if we are both overrange and in an error state
    bConnected := NOT (bOverrange AND bError);
    fTemp := INT_TO_LREAL(iRaw) / iScale;

    END_FUNCTION_BLOCK


Related:
    * `FB_TempSensor`_


FB_TimeStampBuffer
^^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_TimeStampBuffer
    (*
        A Companion to FB_LREALBuffer that accumulates timestamps
        2019-10-09 Zachary Lentz
    *)
    VAR_INPUT
        // If TRUE, we'll accumulate a value on this cycle.
        bExecute: BOOL;
    END_VAR
    VAR_OUTPUT
        arrOutput: ARRAY [1..1000] OF LREAL;
        bNewArray: BOOL;
    END_VAR
    VAR
        fbUnixTime: FB_UnixTimestamp;
        fbLREALBuffer: FB_LREALBuffer;
    END_VAR
    fbUnixTime(
        bExecute := bExecute,
        fTime => fbLREALBuffer.fInput);
    fbLREALBuffer(
        bExecute := bExecute,
        arrOutput => arrOutput,
        bNewArray => bNewArray);

    END_FUNCTION_BLOCK


Related:
    * `FB_LREALBuffer`_


FB_TimeStampBufferGlobal
^^^^^^^^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_TimeStampBufferGlobal
    (*
        A Variant of FB_TimeStampBuffer that uses the global timestamp.
        2019-10-09 Zachary Lentz

        Assumes an instance of FB_UnixTimeStampGlobal is running every cycle.
    *)
    VAR_INPUT
        // If TRUE, we'll accumulate a value on this cycle.
        bExecute: BOOL;
    END_VAR
    VAR_OUTPUT
        arrOutput: ARRAY [1..1000] OF LREAL;
        bNewArray: BOOL;
    END_VAR
    VAR
        fbLREALBuffer: FB_LREALBuffer;
    END_VAR
    fbLREALBuffer(
        bExecute := bExecute,
        fInput := DefaultGlobals.fTimeStamp,
        arrOutput => arrOutput,
        bNewArray => bNewArray);

    END_FUNCTION_BLOCK


Related:
    * `DefaultGlobals`_
    * `FB_LREALBuffer`_
    * `FB_TimeStampBuffer`_
    * `FB_UnixTimeStampGlobal`_


FB_UnixTimeStamp
^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_UnixTimeStamp
    (*
        Get the unix timestamp equivalent of the PLC's time.
        2019-10-09 Zachary Lentz

        This will only sync with the Linux host when both hosts' clocks are correct.
        Largely stolen from stack overflow
    *)
    VAR_INPUT
        // If TRUE, we'll try to update the output on this cycle.
        bExecute: BOOL;
    END_VAR
    VAR_OUTPUT
        // Number of seconds in the timestamp
        iSeconds: ULINT;
        // Number of milliseconds past the seconds
        iMilliseconds: ULINT;
        // Full raw number
        iFull: ULINT;
        // Full floating point number in units of seconds
        fTime: LREAL;
        // TRUE if the output is okay to use on this cycle. Typically the output is zero when this is FALSE.
        bValid: BOOL;
    END_VAR
    VAR
        bInit: BOOL;
        fbLocalTime: FB_LocalSystemTime;
        fbGetTimeZone: FB_GetTimeZoneInformation;
        fbTimeConv: FB_TzSpecificLocalTimeToFileTime;
        fileTime: T_FILETIME;
    END_VAR
    IF NOT bInit THEN
        bInit := TRUE;
        fbGetTimeZone(bExecute:=TRUE, tzInfo => fbTimeConv.tzInfo);
    END_IF
    IF bExecute THEN
        fbLocalTime(
            bEnable := TRUE,
            dwCycle := 1,
            bValid => bValid);
        IF bValid THEN
            fbTimeConv(
                in := SYSTEMTIME_TO_FILETIME(fbLocalTime.systemTime),
                out => fileTime);
            iFull := (SHL(DWORD_TO_ULINT(fileTime.dwHighDateTime), 32) + DWORD_TO_ULINT(fileTime.dwLowDateTime)) / 10000 - 11644473600000;
            fTime := ULINT_TO_LREAL(iFull)/1000;
            iSeconds := iFull/1000;
            iMilliseconds := iFull MOD 1000;
        END_IF
    END_IF

    END_FUNCTION_BLOCK




FB_UnixTimeStampGlobal
^^^^^^^^^^^^^^^^^^^^^^

::

    FUNCTION_BLOCK FB_UnixTimeStampGlobal
    (*
        Runs FB_UnixTimeStamp and stuffs the result into this library's GVL
        2019-10-09 Zachary Lentz
    *)
    VAR_INPUT
        // If TRUE, we will update the output on this cycle.
        bExecute: BOOL;
    END_VAR
    VAR_OUTPUT
    END_VAR
    VAR
        fbTimeStamp: FB_UnixTimeStamp;
    END_VAR
    fbTimeStamp(
        bExecute := bExecute,
        fTime => DefaultGlobals.fTimeStamp);

    END_FUNCTION_BLOCK


Related:
    * `DefaultGlobals`_
    * `FB_UnixTimeStamp`_


FB_XKoyoPLCModbus
^^^^^^^^^^^^^^^^^

::

    //Facilitates communication between Beckhoff and Koyo PLC over the network.
    FUNCTION_BLOCK FB_XKoyoPLCModbus
    VAR
        fbKoyo_PLCInputCoilsRx	:	FB_MBReadCoils; //FB for reading the coils from the other PLC
        anKoyo_PLC_CnBits	:	ARRAY [0..20] OF BYTE; //Buffer for coil readbacks
        {attribute 'naming' := 'omit'}
        ftReset	: F_TRIG; //Reset edge sensor
        {attribute 'naming' := 'omit'}
        tonRetry : TON; //Retry timer
        nIndex : INT; //Index for clearing the coil array
    END_VAR

    VAR_INPUT
        i_tRetryTime : TIME := T#10S; //Retry time if modbus transaction fails
        i_sIPAddr	 : STRING[15]; //IP address of the Koyo PLC
    END_VAR

    VAR_OUTPUT
        q_xNoPLCResponse : BOOL := TRUE; //Could not reach the PLC if true
        q_anPLCResponse   : ARRAY [0..20] OF BYTE; //Buffer of coils retrieved from the other PLC
        q_xError         : BOOL := FALSE; //Transaction or other error
    END_VAR
    (* Look ma' no wires! *)
    (* A. Wallace, 2015-7-22
    XKoyoPLCModbus

    Facilitates communication between Beckhoff and Koyo PLC over the network.

    Useful if you don't have time to run a wire. Fairly reliable.

    *)

    (* Modbus Info for Koyo
    Modbus Addresses for
    Koyo DL05/06/240/250/260/430/440/450 PLCs
    PLC Memory Type		| Modbus start address Decimal (octal) | Function codes
    Inputs (X)			  2048 (04000)							2
    Special Relays (SP)	  3072 (06000)							2
    Outputs (Y)			  2048 (04000)							1, 5, 15
    Control Relays (C)	  3072 (06000)							1, 5, 15
    Timer Contacts (T)	  6144 (014000)							1, 5, 15
    Counter Contacts (CT) 6400 (014400)							1, 5, 15
    Stage Status Bits (S) 6144 (012000)							1, 5, 15
    *)

    (* Begin code *)
    // Retry after some time
    tonRetry.IN := NOT fbKoyo_PLCInputCoilsRx.bBusy;
    tonRetry.PT := i_tRetryTime;
    tonRetry();

    ftReset(CLK:=fbKoyo_PLCInputCoilsRx.bBusy);
    ftReset();

    fbKoyo_PLCInputCoilsRx.bExecute := ftReset.Q OR tonRetry.Q;

    fbKoyo_PLCInputCoilsRx(sIPAddr:='i_sIPAddr', nTCPPort:=502, nQuantity:=32, nMBAddr:=8#6000, cbLength:=USINT_TO_UDINT(SIZEOF(anKoyo_PLC_CnBits)),  pDestAddr:=ADR(anKoyo_PLC_CnBits), tTimeout:=T#10S);

    //run some error code for modbus
    IF fbKoyo_PLCInputCoilsRx.bError THEN
        //if there's a modbus error, set all incoming bits to zero
        {analysis -41} //There are one-liners for resetting an array to zero but they don't comply with 61131
        FOR nIndex := 0 TO USINT_TO_INT(SIZEOF(anKoyo_PLC_CnBits))-1 DO //starts at 0
            anKoyo_PLC_CnBits[nIndex]:=0;
        END_FOR
        {analysis +41}
        q_xError := TRUE;

    ELSIF ftReset.Q AND fbKoyo_PLCInputCoilsRx.cbRead > 0 THEN
        fbKoyo_PLCInputCoilsRx.bExecute := FALSE;
        q_xNoPLCResponse:= FALSE;
        q_xError := FALSE;

    //more error code cause we didn't manage to read anything
    ELSIF fbKoyo_PLCInputCoilsRx.cbRead = 0 THEN
        q_xError := TRUE;
        q_xNoPLCResponse:= TRUE;

    END_IF

    q_anPLCResponse := anKoyo_PLC_CnBits;

    END_FUNCTION_BLOCK




ResetCircuitBreakerGlobals
^^^^^^^^^^^^^^^^^^^^^^^^^^

::

    FUNCTION ResetCircuitBreakerGlobals : BOOL
    VAR_INPUT
    END_VAR
    VAR
    END_VAR
    GVL_Logger.bTrickleTripped := FALSE;
    GVL_Logger.nGlobAccEvents := 0;

    END_FUNCTION


Related:
    * `GVL_Logger`_


RUN_TESTS
^^^^^^^^^

::

    PROGRAM RUN_TESTS
    VAR
        {attribute 'analysis' := '-33'}
        fbCbTest : FB_CircuitBreaker_Test;
        {attribute 'analysis' := '-33'}
        fbCentroidTest : FB_Test_EpicsCentroidMonitor;
        {attribute 'analysis' := '-33'}
        fbMotorTest : FB_Test_EpicsMotorMonitor;
    END_VAR
    TcUnit.RUN();

    END_PROGRAM


Related:
    * `FB_CircuitBreaker_Test`_
    * `FB_Test_EpicsCentroidMonitor`_
    * `FB_Test_EpicsMotorMonitor`_


SYSTEM_TIME_TO_RFC3339
^^^^^^^^^^^^^^^^^^^^^^

::

    //Converts Beckhoff PLC SYSTEMTIME to RFC3339 time format as a string
    {attribute 'naming' := 'omit'}
    {attribute 'analysis' := '-23'}
    FUNCTION SYSTEM_TIME_TO_RFC3339 : STRING(255)
    VAR_INPUT
        {attribute 'naming' := 'omit'}
        tCurrentTime	:	TIMESTRUCT; //TIMESTRUCT Time to convert to RFC3339
    END_VAR
    VAR
    END_VAR
    SYSTEM_TIME_TO_RFC3339 := CONCAT(REPLACE(SYSTEMTIME_TO_STRING(tCurrentTime), 'T', 1, 11), 'Z');

    END_FUNCTION




TIME_TO_100NS
^^^^^^^^^^^^^

::

    FUNCTION TIME_TO_100NS : ULINT
    VAR_INPUT
        nTime : TIME;
    END_VAR
    VAR
    END_VAR
    TIME_TO_100NS := TIME_TO_ULINT(nTime)*10000;

    END_FUNCTION