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
    {attribute 'pytmc' := '
        pv: bEPS_OK
        io: i
        field: DESC check if nFlags are all true
    '}
    bEPS_OK : BOOL := TRUE;
END_STRUCT
END_TYPE
Related:

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:

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:

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:

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:

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_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:

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

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:

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*: DINT;
    // 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;
    // Value to scale the end result to
    {attribute 'pytmc' := '
        pv: RES
        io: io
    '}
    fResolution : LREAL := 1;
    {attribute 'pytmc' := '
        pv: OFF
        io: io
    '}
    fOffset : LREAL;
END_VAR
VAR_OUTPUT
    // The real value read from the output
    {attribute 'pytmc' := '
        pv: VAL
        io: i
    '}
    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;
    fReal := fReal * fResolution + fOffset;
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;
    // RMS of all values in the array
    {attribute 'pytmc' := '
        pv: STATS:RMS
        io: i
    '}
    fRMS: 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;
    fRMSSum: LREAL;
    fVarianceSum: LREAL;
    fVarianceMean: LREAL;
END_VAR
rTrig(CLK:=bExecute);
IF bReset THEN
    fMean := 0;
    fStDev := 0;
    fMax := 0;
    fMin := 0;
    fRange := 0;
    fRMS := 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, rms
    nElemsSeen := 0;
    fSum := 0;
    fRMSSum := 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];
        fRMSSum := fRMSSum + EXPT(aSignal[nIndex], 2);
        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;
        fRMS := SQRT(fRMSSum / nElemsSeen);

        // 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_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_ECATAutoRestart

FUNCTION_BLOCK FB_ECATAutoRestart
VAR_INPUT
    sNetId          : T_AmsNetId;
    nSlaveAddr      : UINT;
    bEnable             : BOOL;
    nMaxErrorCount  : INT := 5; // Stop Trying to Change state after this many tries
END_VAR
VAR_IN_OUT
END_VAR
VAR_OUTPUT
    bError          : BOOL;
    nErrorId        : UDINT;
END_VAR
VAR
    nStateMachineState      : INT := 1;
    nErrorCount             : INT := 0;
    bStopTrying             : BOOL;

    fbgetSlaveState         : FB_EcGetSlaveState;
    fbsetSlaveState         : FB_EcSetSlaveState;
    fbLogger                        : FB_LogMessage := (eSubsystem:= E_Subsystem.FIELDBUS);
    bLog : BOOL;
    sLogMessage : T_MaxString;
END_VAR
StateMachine(bEnable:=bEnable,
    bfbGetBusy := fbgetSlaveState.bBusy,
    bfbGetUpdatedState => fbgetSlaveState.bExecute,
    bfbGetError := fbgetSlaveState.bError,
    nCurretState := fbgetSlaveState.state.deviceState,
    nNextState => fbsetSlaveState.reqState,
    sLogMessage=> sLogMessage,
    bLog=> bLog,
    bSetNextState=> fbsetSlaveState.bExecute,
    bfbSetBusy := fbsetSlaveState.bBusy,
    bfbSetError := fbsetSlaveState.bError,
    bError => bError);

fbgetSlaveState(sNetId:= sNetId, nSlaveAddr:= nSlaveAddr);
fbsetSlaveState(sNetId:=sNetId, nSlaveAddr:=nSlaveAddr, tTimeout:=T#5s);

IF bLog THEN
    fbLogger(sMsg:=sLogMessage, eSevr:=TcEventSeverity.Info);
END_IF

IF bError OR bStopTrying THEN
    bError := TRUE;
    IF fbgetSlaveState.bError OR bStopTrying THEN
           nErrorId := fbgetSlaveState.nErrId;
    ELSIF fbsetSlaveState.bError OR bStopTrying THEN
        nErrorId := fbsetSlaveState.nErrId;
    END_IF
ELSE
    bError := FALSE;
    nErrorId := 0 ;
END_IF

END_FUNCTION_BLOCK

METHOD StateMachine
VAR_INPUT
    bEnable                 : BOOL;
    nCurretState            : WORD;

    bfbGetBusy              : BOOL;
    bfbGetError                             : BOOL;

    bfbSetBusy              : BOOL;
    bfbSetError             : BOOL;

END_VAR
VAR_OUTPUT
    bfbGetUpdatedState      : BOOL;
    bSetNextState           : BOOL;
    nNextState              : WORD;

    sLogMessage             : T_MaxString;
    bLog                    : BOOL;
    bError                                  : BOOL;
END_VAR
CASE nStateMachineState OF
    0 :
        nErrorCount := 0;
        bStopTrying := FALSE;

        IF bEnable THEN
            nStateMachineState := 1;
        END_IF

    1 : // Monitor State

        IF NOT bEnable THEN
            nStateMachineState := 0;
        END_IF

        // Get Updated terminal State
        IF NOT bfbGetBusy THEN
            bfbGetUpdatedState := TRUE;
        ELSE
            bfbGetUpdatedState := FALSE;
        END_IF

        IF bfbGetError THEN
            bError := TRUE;
            nStateMachineState := 3;
        ELSE
            // Only Try to Update when We have good state reading.
            // If not in OP try to Move into Another State
            IF nCurretState <> EC_DEVICE_STATE_OP AND bEnable AND NOT bStopTrying AND NOT bfbGetError THEN
                nStateMachineState := 2;
            END_IF

            IF nCurretState = EC_DEVICE_STATE_OP AND NOT bfbGetError THEN
                // WE ARE IN OP
                nNextState := nCurretState;
                nErrorCount := 0;
                bStopTrying := FALSE;
                bError := FALSE;
            END_IF
        END_IF


     2 : // FIND NEXT STATE AND SIGNAL TO MOVE
        CASE nCurretState OF
            EC_DEVICE_STATE_BOOTSTRAP:
                nNextState := EC_DEVICE_STATE_INIT;
                sLogMessage := 'TRYING TO GO FROM BOOTSTRAP TO INIT';
                bLog := TRUE;
            EC_DEVICE_STATE_INIT:
                nNextState := EC_DEVICE_STATE_PREOP;
                sLogMessage := 'TRYING TO GO FROM INIT TO PREOP';
                bLog := TRUE;
            EC_DEVICE_STATE_PREOP:
                nNextState := EC_DEVICE_STATE_SAFEOP;
                sLogMessage := 'TRYING TO GO FROM PREOP TO SAFEOP';
                bLog := TRUE;
            EC_DEVICE_STATE_SAFEOP:
                nNextState := EC_DEVICE_STATE_OP;
                sLogMessage := 'TRYING TO GO FROM SAFEOP TO OP';
                bLog := TRUE;
        END_CASE

        nStateMachineState := 3;
        // Signal to try to move to next State
        bSetNextState := TRUE;

      3 : // Wait to Finish Trying to Move to Next State

         IF bfbSetError OR bfbGetError THEN
            bError := TRUE;
            nErrorCount := nErrorCount + 1;
            IF nErrorCount = nMaxErrorCount THEN
                bStopTrying := TRUE;
            END_IF
         END_IF

         IF NOT bfbSetBusy THEN
            bSetNextState := FALSE;
            nStateMachineState := 1;
         END_IF


END_CASE
END_METHOD
Related:

FB_ECATAutoRestart_Test

FUNCTION_BLOCK FB_ECATAutoRestart_Test EXTENDS TcUnit.FB_TestSuite
VAR
    fbECATAutoRestart : FB_ECATAutoRestart;

    fbTON_TestTimer         : TON;
    bStartTest                      : BOOL := FALSE;


END_VAR
TestSingleLogPerTransitionExpect4Logs();
TestAnyStateExpectChangeToOpStateInSeq();
TestNotEnabledExpectNoStateChange();
Test5RetriesExpectErrorStopTrying();
TestBadAddressExpectError();

END_FUNCTION_BLOCK

METHOD Test5RetriesExpectErrorStopTrying
VAR_INPUT
END_VAR
VAR
    nIndex                  : INT;
    nStateMachineState      : INT;

    // State Machine Control Inputs
    bEnable                 : BOOL;
    bfbGetBusy              : BOOL;
    bfbGetError              : BOOL;
    nCurretState            : WORD;
    bfbSetBusy              : BOOL;
    bfbSetError             : BOOL;

    // State Machine Control Outputs
    bfbGetUpdatedState      : BOOL;
    bSetNextState           : BOOL;
    nNextState              : WORD;
    sLogMessage             : T_MaxString;
    bLog                    : BOOL;

END_VAR
TEST('Test5RetriesExpectErrorStopTrying');

nStateMachineState := 1; // Start Test on Start State
bEnable := TRUE;
bfbGetBusy := FALSE;
bfbGetError := FALSE;
nCurretState := EC_DEVICE_STATE_PREOP;
bfbSetBusy := FALSE;
bfbSetError := TRUE;

FOR nIndex := 1 TO 20 DO
    fbECATAutoRestart.StateMachine(bEnable:=bEnable,
        bfbGetBusy := bfbGetBusy,
        bfbGetError := bfbGetError,
        bfbGetUpdatedState => bfbGetUpdatedState,
        nCurretState := nCurretState,
        nNextState => nNextState,
        sLogMessage=> sLogMessage,
        bLog=> bLog,
        bSetNextState=> bSetNextState,
        bfbSetBusy := bfbSetBusy,
        bfbSetError := bfbSetError);
END_FOR


AssertEquals_BOOL(Expected:=TRUE,
                    Actual:= fbECATAutoRestart.bStopTrying,
                    Message:='bStopTrying was not set after trying to get new position after 20 Cycles');

// CLEAR ANY ERRORS AFTER TESTS
FOR nIndex := 1 TO 3 DO
    fbECATAutoRestart.StateMachine(bEnable:=FALSE,
        bfbGetBusy := bfbGetBusy,
        bfbGetError := bfbGetError,
        bfbGetUpdatedState => bfbGetUpdatedState,
        nCurretState := nCurretState,
        nNextState => nNextState,
        sLogMessage=> sLogMessage,
        bLog=> bLog,
        bSetNextState=> bSetNextState,
        bfbSetBusy := bfbSetBusy,
        bfbSetError := bfbSetError);
END_FOR

TEST_FINISHED();
END_METHOD

METHOD TestAnyStateExpectChangeToOpStateInSeq
VAR_INPUT
END_VAR
VAR

    // State Machine Control Inputs
    bEnable                 : BOOL;
    bfbGetBusy              : BOOL;
    bfbGetError                             : BOOL;
    nCurretState            : WORD;
    bfbSetBusy              : BOOL;
    bfbSetError             : BOOL;

    // State Machine Control Outputs
    bfbGetUpdatedState      : BOOL;
    bSetNextState           : BOOL;
    nNextState              : WORD;
    sLogMessage             : T_MaxString;
    bLog                    : BOOL;
    // TestAnyStateChangeOpInSeq Vars

    nIndex          : INT := 1;
    aPossibleStart : ARRAY [1 .. 4] OF BYTE := [EC_DEVICE_STATE_BOOTSTRAP,
                                                EC_DEVICE_STATE_INIT,
                                                EC_DEVICE_STATE_PREOP,
                                                EC_DEVICE_STATE_SAFEOP];
    nStartIndex : INT := 0;


END_VAR
TEST('TestAnyStateExpectChangeToOpStateInSeq');


bEnable := TRUE;
bfbGetBusy := FALSE;
bfbSetBusy := FALSE;
bfbSetError := FALSE;
bfbGetError := FALSE;




FOR nStartIndex := 1 TO 4 DO
    nCurretState := aPossibleStart[nStartIndex];
    // Takes 3 cycles for statemachine to update state
    // 4 - nStartIndex is number of steps needed to return to OP
    FOR nIndex := 1 TO 3 * 4 - nStartIndex DO
    fbECATAutoRestart.StateMachine
            (bEnable:=bEnable,
            bfbGetBusy := bfbGetBusy,
            bfbGetError := bfbGetError,
            bfbGetUpdatedState => bfbGetUpdatedState,
            nCurretState := nCurretState,
            nNextState => nNextState,
            sLogMessage=> sLogMessage,
            bLog=> bLog,
            bSetNextState=> bSetNextState,
            bfbSetBusy := bfbSetBusy,
            bfbSetError := bfbSetError);

    IF bSetNextState THEN
        nCurretState := nNextState;
    END_IF
    END_FOR

    AssertEquals_BYTE(Expected:=EC_DEVICE_STATE_OP,
        Actual:= TO_BYTE(nCurretState),
        Message:='Test Did not Return to OP');
END_FOR

 TEST_FINISHED();
END_METHOD

METHOD TestBadAddressExpectError
VAR_INPUT
END_VAR
VAR
    bError                          : BOOL;
    bErrorId                        : UDINT;
    nIndex                          : INT;
END_VAR
TEST('TestBadAddressExpectError');

FOR nIndex := 1 TO 6 DO

    fbECATAutoRestart(sNetId:='1.0.0.0.1',
                    nSlaveAddr:=11101,
                    bEnable:= TRUE,
                    bError=>bError,
                    nErrorId=>bErrorId);


    IF bError THEN
    AssertEquals_BOOL(Expected:=TRUE,
        Actual:=bError,
        Message:= 'Expected bError to be TRUE');

    AssertEquals_UDINT(Expected:= 7,
        Actual:= bErrorId,
        Message:= 'Expected ERR_TARGETMACHINENOTFOUND (0x7)');

    END_IF
END_FOR

   TEST_FINISHED();
END_METHOD

METHOD TestNotEnabledExpectNoStateChange
VAR_INPUT
END_VAR
VAR
    nStateMachineStartState      : INT;
    // State Machine Control Inputs
    bEnable                 : BOOL;
    bfbGetBusy              : BOOL;
    bfbGetError                             : BOOL;
    nCurretState            : WORD;
    bfbSetBusy              : BOOL;
    bfbSetError             : BOOL;

    // State Machine Control Outputs
    bfbGetUpdatedState      : BOOL;
    bSetNextState           : BOOL;
    nNextState              : WORD;
    sLogMessage             : T_MaxString;
    bLog                    : BOOL;

END_VAR
TEST('TestNotEnabledExpectNoStateChange');

nStateMachineStartState := 1;
bEnable := FALSE;
bfbGetBusy := FALSE;
nCurretState := EC_DEVICE_STATE_PREOP;
bfbSetBusy := FALSE;
bfbSetError := TRUE;
bfbGetError := FALSE;

fbECATAutoRestart.StateMachine(bEnable:=bEnable,
    bfbGetBusy := bfbGetBusy,
    bfbGetError     := bfbGetError,
    bfbGetUpdatedState => bfbGetUpdatedState,
    nCurretState := nCurretState,
    nNextState => nNextState,
    sLogMessage=> sLogMessage,
    bLog=> bLog,
    bSetNextState=> bSetNextState,
    bfbSetBusy := bfbSetBusy,
    bfbSetError := bfbSetError);


AssertEquals_WORD(Expected:= 0,
                    Actual:= nNextState,
                    Message:= 'When bEnable is False, nNextState should not return anything');

TEST_FINISHED();
END_METHOD

METHOD TestSingleLogPerTransitionExpect4Logs
VAR_INPUT
END_VAR

VAR
        // State Machine Control Inputs
    bEnable                 : BOOL;
    bfbGetBusy              : BOOL;
    nCurretState            : WORD;
    bfbSetBusy              : BOOL;
    bfbSetError             : BOOL;
    bfbGetError                             : BOOL;

    // State Machine Control Outputs
    bfbGetUpdatedState      : BOOL;
    bSetNextState           : BOOL;
    nNextState              : WORD;
    sLogMessage             : T_MaxString;
    bLog                    : BOOL;

    nIndex          : INT := 1;
    nLogCount       : INT := 0;
END_VAR
TEST('TestSingleLogPerTransitionExpect4Logs');

fbTON_TestTimer(IN:=bStartTest,PT:=T#1s);
IF  NOT bStartTest THEN bStartTest := TRUE; END_IF

bEnable := TRUE;
bfbGetBusy := FALSE;
bfbSetBusy := FALSE;
bfbSetError := FALSE;
bfbGetError := FALSE;

nCurretState := EC_DEVICE_STATE_INIT;
// Using More cyles then needed
 FOR nIndex := 1 TO 3 * 4 DO
    fbECATAutoRestart.StateMachine
            (bEnable:=bEnable,
            bfbGetBusy := bfbGetBusy,
            bfbGetError := bfbGetError,
            bfbGetUpdatedState => bfbGetUpdatedState,
            nCurretState := nCurretState,
            nNextState => nNextState,
            sLogMessage=> sLogMessage,
            bLog=> bLog,
            bSetNextState=> bSetNextState,
            bfbSetBusy := bfbSetBusy,
            bfbSetError := bfbSetError);

    IF bLog THEN
        nLogCount := nLogCount + 1;
    END_IF
END_FOR

AssertEquals_INT(Expected:= 4,
    Actual:=nLogCount,
    Message:='More than 4 Logs where Produced');

TEST_FINISHED();
END_METHOD
Related:

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:

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_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_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_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:

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:

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_FlutterDetection

FUNCTION_BLOCK FB_FlutterDetection
VAR_INPUT
    bVarToMonitor : BOOL;
    fPastTime : TIME := T#5000ms;
    nMaxFlipsAllowed : UDINT := 10;
    // If set to TRUE, reset flutter detection.
    {attribute 'pytmc' := 'pv: FLUTTER:RESET'}
    bReset : BOOL;
END_VAR
VAR_OUTPUT
    bExceededFlipMax : BOOL;
END_VAR
VAR
    bInit : BOOL := FALSE;
    bLastVarValue : BOOL;
    nNumFlipped : UINT;
    tTimer : TON;
    rtReset : r_trig;
END_VAR
IF NOT bInit THEN
    bLastVarValue := bVarToMonitor;
    nNumFlipped := 0;
    bExceededFlipMax := FALSE;
    tTimer(IN:=FALSE);
    tTimer(IN:=TRUE, PT:=fPastTime);
    bInit := TRUE;
END_IF

rtReset(CLK:=bReset);

IF rtReset.Q THEN
    // re-initialize
    bInit := FALSE;
END_IF

bReset R= rtReset.Q;
tTimer();

IF tTimer.Q THEN
    IF nNumFlipped > nMaxFlipsAllowed THEN
        bExceededFlipMax := TRUE;
        // timer off, no need to count.
    ELSE
        nNumFlipped := 0;
        // reset timer
        tTimer(IN:=FALSE);
        tTimer(IN:=TRUE);
    END_IF
END_IF
// stop counting after exceed.
IF NOT bExceededFlipMax THEN
    IF bLastVarValue <> bVarToMonitor THEN
        nNumFlipped := nNumFlipped +1;
        bLastVarValue := bVarToMonitor;
    END_IF
END_IF

END_FUNCTION_BLOCK

FB_FlutterDetection_Test

FUNCTION_BLOCK FB_FlutterDetection_Test EXTENDS TcUnit.FB_TestSuite
VAR_INPUT
END_VAR
VAR_OUTPUT
END_VAR
VAR
    fbFlutterDet : FB_FlutterDetection;

    nFirstCycleCount : UDINT;
    nCurrentSystemCycle : UDINT;
    nCurrentLocalCycle : UDINT;
END_VAR
TestFlutterResolution();
TestFlutterReset();

END_FUNCTION_BLOCK

METHOD TestFlutterReset
VAR_INST
    fbTestTimer : TON := (PT := T#300ms);
    bInit : BOOL := FALSE;
    bVarToMonitor : BOOL := TRUE;
END_VAR
(* after trip, function should latch exceeded flag remains tripped. Finally,
check that after reset, the coount resets and the exceeded flag
goes low, then trips again. *)
TEST('TestFlutterReset');
IF NOT bInit THEN
    nFirstCycleCount := _TaskInfo[GETCURTASKINDEXEX()].CycleCount;
    bInit := TRUE;
END_IF
nCurrentSystemCycle := _TaskInfo[GETCURTASKINDEXEX()].CycleCount;
nCurrentLocalCycle := nCurrentSystemCycle - nFirstCycleCount;
fbTestTimer(IN := TRUE);
bVarToMonitor := NOT bVarToMonitor;

fbFlutterDet(bVarToMonitor:=bVarToMonitor ,fPastTime:=T#50ms ,nMaxFlipsAllowed:=4);

CASE nCurrentLocalCycle OF
10:
    AssertTrue(fbFlutterDet.bExceededFlipMax, 'exceeded flag not present');
11:
    // latched
    AssertTrue(fbFlutterDet.bExceededFlipMax, 'exceeded flag not present');
12:
    // latched
    AssertTrue(fbFlutterDet.bExceededFlipMax, 'exceeded flag not present');
    fbFlutterDet(bReset:=TRUE);
13:
    // reset
    AssertFalse(fbFlutterDet.bExceededFlipMax, 'false exceeded');
25:
    // lose a few cycles on reset.
    AssertTrue(fbFlutterDet.bExceededFlipMax, 'exceeded flag not present');
END_CASE

IF fbTestTimer.Q THEN
    TEST_FINISHED();
END_IF
END_METHOD

METHOD TestFlutterResolution
VAR_INST
    fbTestTimer : TON := (PT := T#200ms);

    fbFlutterDetExceedResolution : FB_FlutterDetection;
    fbFlutterDetMaxResolution : FB_FlutterDetection;
    nCycleTime : UDINT;
    nCalcCycleTime : TIME;
    nNumCycles : UDINT := 4;
    bVarToMonitor : BOOL := TRUE;
END_VAR
(*Test the max flips that can be detected, should be number of cycles - 1*)

TEST('TestFlutterResolution');
fbTestTimer(IN := TRUE);

nCycleTime := _TaskInfo[GETCURTASKINDEXEX()].CycleTime;
nCycleTime := nCycleTime / 10000; //convert to ms
nCalcCycleTime := UDINT_TO_TIME(nCycleTime * nNumCycles);

fbFlutterDetExceedResolution(bVarToMonitor:=bVarToMonitor, fPastTime:=nCalcCycleTime, nMaxFlipsAllowed:=nNumCycles);
fbFlutterDetMaxResolution(bVarToMonitor:=bVarToMonitor, fPastTime:=nCalcCycleTime, nMaxFlipsAllowed:=nNumCycles - 1);
bVarToMonitor := NOT bVarToMonitor;

IF fbTestTimer.Q THEN
    assertFalse(fbFlutterDetExceedResolution.bExceededFlipMax,'Flutter shouldnt be detected');
    assertTrue(fbFlutterDetMaxResolution.bExceededFlipMax,'Flutter not detected');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

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:

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_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:

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:

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:

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_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_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_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_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_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:

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:

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:

RUN_TESTS

PROGRAM RUN_TESTS
VAR
    // Logger
    {attribute 'analysis' := '-33'}
    fbCbTest : FB_CircuitBreaker_Test;
    {attribute 'analysis' := '-33'}
    // Data
    fbCentroidTest : FB_Test_EpicsCentroidMonitor;
    {attribute 'analysis' := '-33'}
    fbMotorTest : FB_Test_EpicsMotorMonitor;
    // EPS
    {attribute 'analysis' := '-33'}
    fbFlutterTest : FB_FlutterDetection_Test;
    // Hardware
    {attribute 'analysis' := '-33'}
    fbECATAutoRestartTest : FB_ECATAutoRestart_Test;
END_VAR
TcUnit.RUN();

END_PROGRAM
Related:

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