PLC Project (1): LCLSGeneral

Project root: /home/travis/build/pcdshub/lcls-twincat-general/LCLSGeneral
Project path: LCLSGeneral/LCLSGeneral.plcproj
TMC path:     LCLSGeneral/LCLSGeneral.tmc
AMS ID:       
IP Address:    (* based on AMS ID)
Port:         851

Source files:
    1.) LCLSGeneral/Data types/Misc/ST_EcDevice.TcDUT
    2.) LCLSGeneral/Data types/Misc/ST_FbDiagnostics.TcDUT
    3.) LCLSGeneral/Data types/Misc/ST_System.TcDUT
    4.) LCLSGeneral/GVLs/DefaultGlobals.TcGVL
    5.) LCLSGeneral/GVLs/Global_Variables_EtherCAT.TcGVL
    6.) LCLSGeneral/POUs/Data/FB_BasicStats.TcPOU
    7.) LCLSGeneral/POUs/Data/FB_DataBuffer.TcPOU
    8.) LCLSGeneral/POUs/Data/FB_LREALBuffer.TcPOU
    9.) LCLSGeneral/POUs/Data/FB_LREALFromEPICS.TcPOU
    10.) LCLSGeneral/POUs/Data/FB_TimeStampBuffer.TcPOU
    11.) LCLSGeneral/POUs/Data/FB_TimeStampBufferGlobal.TcPOU
    12.) LCLSGeneral/POUs/Diagnostics/DUT/E_EcatDiagState.TcDUT
    13.) LCLSGeneral/POUs/Diagnostics/DUT/E_EcCommState.TcDUT
    14.) LCLSGeneral/POUs/Diagnostics/DUT/ST_EcMasterDevState.TcDUT
    15.) LCLSGeneral/POUs/Diagnostics/DUT/ST_PortAddr.TcDUT
    16.) LCLSGeneral/POUs/Diagnostics/DUT/ST_SlaveState.TcDUT
    17.) LCLSGeneral/POUs/Diagnostics/DUT/ST_SlaveStateInfo.TcDUT
    18.) LCLSGeneral/POUs/Diagnostics/DUT/ST_SlaveStateInfoScanned.TcDUT
    19.) LCLSGeneral/POUs/Diagnostics/DUT/ST_TopologyData.TcDUT
    20.) LCLSGeneral/POUs/Diagnostics/FB_EcatDiagWrapper.TcPOU
    21.) LCLSGeneral/POUs/Diagnostics/FB_EtherCATDiag.TcPOU
    22.) LCLSGeneral/POUs/Diagnostics/FB_EtherCATFrameDiag.TcPOU
    23.) LCLSGeneral/POUs/Functions/FB_EcatDiag.TcPOU
    24.) LCLSGeneral/POUs/Functions/FB_Index.TcPOU
    25.) LCLSGeneral/POUs/Functions/FB_UnixTimeStamp.TcPOU
    26.) LCLSGeneral/POUs/Functions/FB_UnixTimeStampGlobal.TcPOU
    27.) LCLSGeneral/POUs/Functions/FB_XKoyoPLCModbus.TcPOU
    28.) LCLSGeneral/POUs/Functions/F_ConvertTicksToUnixTimestamp.TcPOU
    29.) LCLSGeneral/POUs/Hardware/FB_AnalogOutput.TcPOU
    30.) LCLSGeneral/POUs/Hardware/FB_AnalogInput.TcPOU
    31.) LCLSGeneral/POUs/Hardware/FB_CoE_FastRead.TcPOU
    32.) LCLSGeneral/POUs/Hardware/FB_EL6_Com.TcPOU
    33.) LCLSGeneral/POUs/Hardware/FB_ThermoCouple.TcPOU
    34.) LCLSGeneral/POUs/Hardware/FB_TempSensor.TcPOU
    35.) LCLSGeneral/POUs/Logger/DUTs/E_LogEventType_WC.TcDUT
    36.) LCLSGeneral/POUs/Logger/DUTs/ST_LoggingEventInfo_WC.TcDUT
    37.) LCLSGeneral/POUs/Logger/DUTs/ST_PendingEvent.TcDUT
    38.) LCLSGeneral/POUs/Logger/FB_Listener.TcPOU
    39.) LCLSGeneral/POUs/Logger/FB_LogHandler.TcPOU
    40.) LCLSGeneral/POUs/Logger/FB_LogMessage.TcPOU
    41.) LCLSGeneral/POUs/Logger/F_SendUDPMessage.TcPOU
    42.) LCLSGeneral/POUs/Logger/GVL_Logger.TcGVL
    43.) LCLSGeneral/POUs/Logger/DUTs/E_Subsystem.TcDUT
    44.) LCLSGeneral/POUs/Logger/SYSTEM_TIME_TO_RFC3339.TcPOU

POUs:
    1.) FB_AnalogInput
    2.) FB_AnalogOutput
    3.) FB_BasicStats
    4.) FB_CoE_FastRead
    5.) FB_DataBuffer
    6.) FB_EL6_Com
    7.) FB_EcatDiag
    8.) FB_EcatDiagWrapper
    9.) FB_EtherCATDiag
    10.) FB_EtherCATFrameDiag
    11.) FB_Index
    12.) FB_LREALBuffer
    13.) FB_LREALFromEPICS
    14.) FB_Listener
    15.) FB_LogHandler
    16.) FB_LogMessage
    17.) FB_TempSensor
    18.) FB_ThermoCouple
    19.) FB_TimeStampBuffer
    20.) FB_TimeStampBufferGlobal
    21.) FB_UnixTimeStamp
    22.) FB_UnixTimeStampGlobal
    23.) FB_XKoyoPLCModbus
    24.) F_ConvertTicksToUnixTimestamp
    25.) F_SendUDPMessage
    26.) SYSTEM_TIME_TO_RFC3339

GVLs:
    1.) DefaultGlobals
    2.) GVL_Logger
    3.) Global_Variables_EtherCAT

DUT: E_EcCommState

File: LCLSGeneral/POUs/Diagnostics/DUT/E_EcCommState.TcDUT

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

DUT: E_EcatDiagState

File: LCLSGeneral/POUs/Diagnostics/DUT/E_EcatDiagState.TcDUT

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

DUT: E_LogEventType_WC

File: LCLSGeneral/POUs/Logger/DUTs/E_LogEventType_WC.TcDUT

{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 *)

DUT: E_Subsystem

File: LCLSGeneral/POUs/Logger/DUTs/E_Subsystem.TcDUT

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

DUT: ST_EcDevice

File: LCLSGeneral/Data types/Misc/ST_EcDevice.TcDUT

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

DUT: ST_EcMasterDevState

File: LCLSGeneral/POUs/Diagnostics/DUT/ST_EcMasterDevState.TcDUT

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

DUT: ST_FbDiagnostics

File: LCLSGeneral/Data types/Misc/ST_fbDiagnostics.TcDUT

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

DUT: ST_LoggingEventInfo_WC

File: LCLSGeneral/POUs/Logger/DUTs/ST_LoggingEventInfo_WC.TcDUT

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

DUT: ST_PendingEvent

File: LCLSGeneral/POUs/Logger/DUTs/ST_PendingEvent.TcDUT

TYPE ST_PendingEvent :
STRUCT
	
	{attribute 'pytmc' := '
		pv:
	'}
	stEventInfo			:	ST_LoggingEventInfo;
	bInUse				:	BOOL;
	fbRequestEventText	:	FB_RequestEventText;
	
END_STRUCT
END_TYPE

DUT: ST_PortAddr

File: LCLSGeneral/POUs/Diagnostics/DUT/ST_PortAddr.TcDUT

TYPE ST_PortAddr :
STRUCT
	portA : UINT;
	portB : UINT;
	portC : UINT;
	portD : UINT;
END_STRUCT
END_TYPE

DUT: ST_SlaveState

File: LCLSGeneral/POUs/Diagnostics/DUT/ST_SlaveState.TcDUT

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

DUT: ST_SlaveStateInfo

File: LCLSGeneral/POUs/Diagnostics/DUT/ST_SlaveStateInfo.TcDUT

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

DUT: ST_SlaveStateInfoScanned

File: LCLSGeneral/POUs/Diagnostics/DUT/ST_SlaveStateInfoScanned.TcDUT

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

DUT: ST_System

File: LCLSGeneral/Data types/Misc/ST_System.TcDUT

//Defacto system structure, must be included in all projects
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 *)
	{attribute 'naming' := 'omit'}
	I_EcatMaster1 AT %I* : AMSNETID; (* AMS Net ID used for FB_EcatDiag, among others *)

END_STRUCT
END_TYPE

DUT: ST_TopologyData

File: LCLSGeneral/POUs/Diagnostics/DUT/ST_TopologyData.TcDUT

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

GVL: DefaultGlobals

File: LCLSGeneral/GVLs/DefaultGlobals.TcGVL

//These are variables every PLC project should have
VAR_GLOBAL
	
	stSys	:	ST_System; //Included for you
	fTimeStamp: LREAL;
END_VAR

GVL: GVL_Logger

File: LCLSGeneral/POUs/Logger/GVL_Logger.TcGVL

{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';
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. 
	*)
	TCPADS_MAXUDP_BUFFSIZE : UDINT :=10000;
	
	{analysis -33}
	fbRootLogger : FB_LogMessage; //Instantiated here to be used everywhere
	{analysis +33}
END_VAR

GVL: Global_Variables_EtherCAT

File: LCLSGeneral/GVLs/Global_Variables_EtherCAT.TcGVL

VAR_GLOBAL CONSTANT
	iSLAVEADDR_ARR_SIZE	: UINT := 256;
    ESC_MAX_PORTS : UINT := 3; // Maximum number of ports (4) on ESC
END_VAR

POU: FB_AnalogInput

File: LCLSGeneral/POUs/Hardware/FB_AnalogInput.TcPOU

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

END_FUNCTION_BLOCK

POU: FB_AnalogOutput

File: LCLSGeneral/POUs/Hardware/FB_AnalogOutput.TcPOU

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

POU: FB_BasicStats

File: LCLSGeneral/POUs/Data/FB_BasicStats.TcPOU

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

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

END_FUNCTION_BLOCK

POU: FB_CoE_FastRead

File: LCLSGeneral/POUs/Hardware/FB_CoE_FastRead.TcPOU

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

POU: FB_DataBuffer

File: LCLSGeneral/POUs/Data/FB_DataBuffer.TcPOU

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

POU: FB_EL6_Com

File: LCLSGeneral/POUs/Hardware/FB_EL6_Com.TcPOU

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

POU: FB_EcatDiag

File: LCLSGeneral/POUs/Functions/FB_EcatDiag.TcPOU

(* 
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

POU: FB_EcatDiagWrapper

File: LCLSGeneral/POUs/Diagnostics/FB_EcatDiagWrapper.TcPOU

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

POU: FB_EtherCATDiag

File: LCLSGeneral/POUs/Diagnostics/FB_EtherCATDiag.TcPOU

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;
    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

POU: FB_EtherCATFrameDiag

File: LCLSGeneral/POUs/Diagnostics/FB_EtherCATFrameDiag.TcPOU

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

POU: FB_Index

File: LCLSGeneral/POUs/Functions/FB_Index.TcPOU

(* 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

POU: FB_LREALBuffer

File: LCLSGeneral/POUs/Data/FB_LREALBuffer.TcPOU

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

POU: FB_LREALFromEPICS

File: LCLSGeneral/POUs/Data/FB_LREALFromEPICS.TcPOU

(*
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_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
	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 = 0);
fValue := fLastValidValue;

END_FUNCTION_BLOCK

POU: FB_Listener

File: LCLSGeneral/POUs/Logger/FB_Listener.TcPOU

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 '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

POU: FB_LogHandler

File: LCLSGeneral/POUs/Logger/FB_LogHandler.TcPOU

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_GetHostName := (bExecute := TRUE, sNetID := ''); //Acquires name of the PLC
    fbGetAdapterIP : FB_GetAdaptersInfo := (bExecute := TRUE, sNetID := ''); // Acquire IP of the correct adapter
	bHostnameSet	:	BOOL	:= FALSE;
    idxPortFind : UDINT;

	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;
    
    bAdapterSet : BOOL;
    
    ctuSocketError : CTU := (PV:=3); // Circuit breaker for socket errors. 3 errors before it stops.
    
    tRetryConnection : TON := (PT:=T#1h); // Retry after an hour
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 
	
IF NOT bHostnameSet THEN
	fbGetHostName();
	IF NOT (fbGetHostName.bBusy OR fbGetHostName.bError) THEN
		GVL_Logger.sPlcHostname := fbGetHostName.sHostName;
		bHostnameSet 			:= TRUE;
	END_IF
END_IF

IF NOT bAdapterSet THEN
    fbGetAdapterIP();
    IF NOT (fbGetAdapterIP.bBusy or fbGetAdapterIP.bError) THEN
        FOR idxPortFind := 0 TO MAX_LOCAL_ADAPTERS DO
            IF FIND(fbGetAdapterIP.arrAdapters[idxPortFind].sIpAddr,
               GVL_Logger.sIpTidbit) <> 0 THEN
                fbSocket.sLocalHost := fbGetAdapterIP.arrAdapters[idxPortFind].sIpAddr;
                SocketEnable := TRUE;
                bAdapterSet := TRUE;
                EXIT;
            END_IF
        END_FOR
    END_IF
END_IF

(* 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:=SocketEnable,
	nMode:=CONNECT_MODE_ENABLEDBG,
);

bReadyToLog := (bAdapterSet AND bHostnameSet AND bInitialized AND 
                fbSocket.bEnable AND NOT fbSocket.bError);
rtFirstLog(CLK:=bReadyToLog);

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

(* 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

POU: FB_LogMessage

File: LCLSGeneral/POUs/Logger/FB_LogMessage.TcPOU

{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
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;
	
	{attribute 'instance-path'} 
    {attribute 'noinit'} 
	sPath				:	T_MaxString;
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
	
// 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();
fbMessage.ipArguments.AddString(sMsg);

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)

IF NOT FAILED(hr) AND fbMessage.eSeverity >= eTraceLevel THEN
	hr := fbMessage.Send(0);
END_IF

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

END_FUNCTION_BLOCK

POU: FB_TempSensor

File: LCLSGeneral/POUs/Hardware/FB_TempSensor.TcPOU

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

POU: FB_ThermoCouple

File: LCLSGeneral/POUs/Hardware/FB_ThermoCouple.TcPOU

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

POU: FB_TimeStampBuffer

File: LCLSGeneral/POUs/Data/FB_TimestampBuffer.TcPOU

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

POU: FB_TimeStampBufferGlobal

File: LCLSGeneral/POUs/Data/FB_TimeStampBufferGlobal.TcPOU

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

POU: FB_UnixTimeStamp

File: LCLSGeneral/POUs/Functions/FB_UnixTimeStamp.TcPOU

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

POU: FB_UnixTimeStampGlobal

File: LCLSGeneral/POUs/Functions/FB_UnixTimeStampGlobal.TcPOU

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

POU: FB_XKoyoPLCModbus

File: LCLSGeneral/POUs/Functions/FB_XKoyoPLCModbus.TcPOU

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

POU: F_ConvertTicksToUnixTimestamp

File: LCLSGeneral/POUs/Functions/F_ConvertTicksToUnixTimestamp.TcPOU

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

POU: F_SendUDPMessage

File: LCLSGeneral/POUs/Logger/F_SendUDPMessage.TcPOU

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

POU: SYSTEM_TIME_TO_RFC3339

File: LCLSGeneral/POUs/Logger/SYSTEM_TIME_TO_RFC3339.TcPOU

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

Symbols

Boxes

NC axes

Pragma lint results

INFO:pytmc.bin.pragmalint:Total pragmas found: 79 Total linter errors: 0
PLC Project (1): LCLSGeneral
============================


Data types/Misc/ST_FbDiagnostics.TcDUT (TcPlcObject)
----------------------------------------------------

    - ST_FbDiagnostics: Declaration - 2 pragmas


Data types/Misc/ST_System.TcDUT (TcPlcObject)
---------------------------------------------

    - ST_System: Declaration - 1 pragmas


POUs/Data/FB_BasicStats.TcPOU (TcPlcObject)
-------------------------------------------

    - FB_BasicStats: Declaration - 11 pragmas


POUs/Data/FB_LREALFromEPICS.TcPOU (TcPlcObject)
-----------------------------------------------

    - FB_LREALFromEPICS: Declaration - 3 pragmas


POUs/Diagnostics/FB_EcatDiagWrapper.TcPOU (TcPlcObject)
-------------------------------------------------------

    - FB_EcatDiagWrapper: Declaration - 6 pragmas


POUs/Functions/FB_EcatDiag.TcPOU (TcPlcObject)
----------------------------------------------

    - FB_EcatDiag: Declaration - 3 pragmas


POUs/Functions/FB_Index.TcPOU (TcPlcObject)
-------------------------------------------

    - FB_Index: Declaration - 2 pragmas


POUs/Functions/FB_XKoyoPLCModbus.TcPOU (TcPlcObject)
----------------------------------------------------

    - FB_XKoyoPLCModbus: Declaration - 2 pragmas


POUs/Hardware/FB_EL6_Com.TcPOU (TcPlcObject)
--------------------------------------------

    - FB_EL6_Com: Declaration - 7 pragmas


POUs/Hardware/FB_ThermoCouple.TcPOU (TcPlcObject)
-------------------------------------------------

    - FB_ThermoCouple: Declaration - 3 pragmas


POUs/Hardware/FB_TempSensor.TcPOU (TcPlcObject)
-----------------------------------------------

    - FB_TempSensor: Declaration - 3 pragmas


POUs/Logger/DUTs/E_LogEventType_WC.TcDUT (TcPlcObject)
------------------------------------------------------

    - E_LogEventType_WC: Declaration - 2 pragmas


POUs/Logger/DUTs/ST_LoggingEventInfo_WC.TcDUT (TcPlcObject)
-----------------------------------------------------------

    - ST_LoggingEventInfo_WC: Declaration - 10 pragmas


POUs/Logger/DUTs/ST_PendingEvent.TcDUT (TcPlcObject)
----------------------------------------------------

    - ST_PendingEvent: Declaration - 1 pragmas


POUs/Logger/FB_Listener.TcPOU (TcPlcObject)
-------------------------------------------

    - FB_Listener: Declaration - 7 pragmas


POUs/Logger/FB_LogHandler.TcPOU (TcPlcObject)
---------------------------------------------

    - FB_LogHandler: Declaration - 7 pragmas


POUs/Logger/FB_LogMessage.TcPOU (TcPlcObject)
---------------------------------------------

    - FB_LogMessage: Declaration - 3 pragmas


POUs/Logger/GVL_Logger.TcGVL (TcPlcObject)
------------------------------------------

    - GVL_Logger: Declaration - 3 pragmas


POUs/Logger/SYSTEM_TIME_TO_RFC3339.TcPOU (TcPlcObject)
------------------------------------------------------

    - SYSTEM_TIME_TO_RFC3339: Declaration - 3 pragmas