DUTs

E_ATM_States

{attribute 'qualified_only'}
TYPE E_ATM_States :
(
    Unknown := 0,
    OUT := 1,
    TARGET1 := 2,
    TARGET2 := 3,
    TARGET3 := 4,
    TARGET4 := 5,
    TARGET5 := 6
) UINT;
END_TYPE
Related:

E_LIC_States

{attribute 'qualified_only'}
TYPE E_LIC_States :
(
    Unknown := 0,
    OUT := 1,
    MIRROR1 := 2,
    MIRROR2 := 3,
    TARGET := 4
) UINT;
END_TYPE

E_PPM_States

{attribute 'qualified_only'}
TYPE E_PPM_States :
(
    Unknown := 0,
    OUT := 1,
    POWERMETER := 2,
    YAG1 := 3,
    YAG2 := 4
) UINT;
END_TYPE

E_SXR_SATT_Position

{attribute 'qualified_only'}
TYPE E_SXR_SATT_Position :
(
    UNKNOWN := 0, // UNKNOWN must be in slot 0 or the FB breaks
    OUT := 1, // OUT at slot 1 is a convention
    FILTER1 := 2,
    FILTER2 := 3,
    FILTER3 := 4,
    FILTER4 := 5,
    FILTER5 := 6,
    FILTER6 := 7,
    FILTER7 := 8,
    FILTER8 := 9
) UINT;
END_TYPE

E_WFS_States

{attribute 'qualified_only'}
TYPE E_WFS_States :
(
    Unknown := 0,
    OUT := 1,
    TARGET1 := 2,
    TARGET2 := 3,
    TARGET3 := 4,
    TARGET4 := 5,
    TARGET5 := 6
) UINT;
END_TYPE

E_XPIM_Filters

{attribute 'qualified_only'}
TYPE E_XPIM_Filters :
(
    Unknown := 0,
    T50 := 1,
    T25 := 2,
    T10 := 3,
    T5 := 4,
    T1 := 5,
    T100 := 6
) UINT;
END_TYPE

E_XPIM_States

{attribute 'qualified_only'}
TYPE E_XPIM_States :
(
    Unknown := 0,
    OUT := 1,
    YAG := 2,
    DIAMOND := 3,
    RETICLE := 4
) UINT;
END_TYPE

ST_SATT_Filter

TYPE ST_SATT_Filter :
STRUCT
    {attribute 'pytmc' := '
        pv: MATERIAL
        io: input
        field: DESC Filter material name
    '}
    sFilterMaterial : STRING;

    {attribute 'pytmc' := '
        pv: THICKNESS
        io: input
        field: DESC Filter material thickness
        field: EGU um
    '}
    fFilterThickness_um : LREAL;
END_STRUCT
END_TYPE

GVLs

Global_Version

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

POUs

FB_ATM

FUNCTION_BLOCK FB_ATM
(*
    Function block for Arrival Time Monitor (ATM) controls:
    - X, Y motion
    - Y target states
    - thermocouple
*)
VAR_IN_OUT
    // Y motor (state select).
    stYStage: ST_MotionStage;
    // X motor (align target to beam).
    stXStage: ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
    // The arbiter to request beam conditions from.
    fbArbiter: FB_Arbiter;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut: ST_PositionState;
    // Settings for the TARGET1 state.
    stTarget1: ST_PositionState;
    // Settings for the TARGET2 state.
    stTarget2: ST_PositionState;
    // Settings for the TARGET3 state.
    stTarget3: ST_PositionState;
    // Settings for the TARGET4 state.
    stTarget4: ST_PositionState;
    // Settings for the TARGET5 state.
    stTarget5: ST_PositionState;
    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: MMS:STATE:SET
        io: io
    '}
    eEnumSet: E_ATM_States;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnableMotion: BOOL;
    // Set this to TRUE to enable beam parameter checks, or FALSE to disable them.
    bEnableBeamParams: BOOL;
    // Set this to TRUE to enable position limit checks, or FALSE to disable them.
    bEnablePositionLimits: BOOL;
    // The name of the device for use in the PMPS DB lookup and diagnostic screens.
    sDeviceName: STRING;
    // The name of the transition state in the PMPS database.
    sTransitionKey: STRING;
    // Set this to TRUE to re-read the loaded database immediately (useful for debug).
    bReadDBNow: BOOL;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: MMS:STATE:GET
        io: i
    '}
    eEnumGet: E_ATM_States;
    // The PMPS database lookup associated with the current position state.
    stDbStateParams: ST_DbStateParams;
END_VAR
VAR
    bInit: BOOL;

    fbYStage: FB_MotionStage;
    fbXStage: FB_MotionStage;

    fbStateDefaults: FB_PositionState_Defaults;

    {attribute 'pytmc' := '
        pv: MMS
        astPositionState.array: 1..6
    '}
    fbStates: FB_PositionStatePMPS1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;

    {attribute 'pytmc' := 'pv: STC:01'}
    fbThermoCouple1: FB_TempSensor;

    {attribute 'pytmc' :='pv: FWM'}
    fbFlowMeter: FB_AnalogInput := (iTermBits:=15, fTermMax:=60, fTermMin:=0);
END_VAR
VAR CONSTANT
    // State defaults if not provided
    fDelta: LREAL := 2;
    fAccel: LREAL := 200;
    fOutDecel: LREAL := 25;
END_VAR
IF NOT bInit THEN
    bInit := TRUE;

    stYStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;
    stXStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;

    // Partial backcompat, this used to set fVelocity too but this should be set per install
    fbStateDefaults(stPositionState:=stOut, sNameDefault:='OUT', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fOutDecel);
    fbStateDefaults(stPositionState:=stTarget1, sNameDefault:='TARGET1', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget2, sNameDefault:='TARGET2', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget3, sNameDefault:='TARGET3', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget4, sNameDefault:='TARGET4', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget5, sNameDefault:='TARGET5', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
END_IF

stYStage.bHardwareEnable := TRUE;
stYStage.bPowerSelf := FALSE;

stXStage.bLimitForwardEnable := TRUE;
stXStage.bLimitBackwardEnable := TRUE;
stXStage.bHardwareEnable := TRUE;
stXStage.bPowerSelf := TRUE;

fbYStage(stMotionStage:=stYStage);
fbXStage(stMotionStage:=stXStage);

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_ATM_States.OUT] := stOut;
    astPositionState[E_ATM_States.TARGET1] := stTarget1;
    astPositionState[E_ATM_States.TARGET2] := stTarget2;
    astPositionState[E_ATM_States.TARGET3] := stTarget3;
    astPositionState[E_ATM_States.TARGET4] := stTarget4;
    astPositionState[E_ATM_States.TARGET5] := stTarget5;
END_IF

fbStates(
    stMotionStage:=stYStage,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=bEnableMotion,
    bEnableBeamParams:=bEnableBeamParams,
    bEnablePositionLimits:=bEnablePositionLimits,
    sDeviceName:=sDeviceName,
    sTransitionKey:=sTransitionKey,
    bReadDBNow:=bReadDBNow,
    stDbStateParams=>stDbStateParams,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_ATM_States.OUT];
stTarget1 := astPositionState[E_ATM_States.TARGET1];
stTarget2 := astPositionState[E_ATM_States.TARGET2];
stTarget3 := astPositionState[E_ATM_States.TARGET3];
stTarget4 := astPositionState[E_ATM_States.TARGET4];
stTarget5 := astPositionState[E_ATM_States.TARGET5];

fbThermoCouple1();
fbFlowMeter();

END_FUNCTION_BLOCK
Related:

FB_ATMTest

FUNCTION_BLOCK FB_ATMTest EXTENDS FB_TestSuite
VAR
    fbATM: FB_ATM;
    stYStage: ST_MotionStage;
    stXStage: ST_MotionStage;

    stDefault: ST_PositionState := (
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;
    fbArbiter: FB_Arbiter(1);
    fbSubSysIO: FB_DummyArbIO;

    bInit: BOOL;
END_VAR
// Fake PMPS handling
fbSubSysIO(
    LA:=fbArbiter,
    FFO:=fbFFHWO,
);

// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbATM.stOut, sName:='OUT', fPosition:=10, sPmpsState:='T0');
fbSetup(stPositionState:=fbATM.stTarget1, sName:='T1', fPosition:=20, sPmpsState:='T1');
fbSetup(stPositionState:=fbATM.stTarget2, sName:='T2', fPosition:=30, sPmpsState:='T2');
fbSetup(stPositionState:=fbATM.stTarget3, sName:='T3', fPosition:=40, sPmpsState:='T3');
fbSetup(stPositionState:=fbATM.stTarget4, sName:='T4', fPosition:=50, sPmpsState:='T4');
fbSetup(stPositionState:=fbATM.stTarget5, sName:='T5', fPosition:=60, sPmpsState:='T5');

// Standard FB call
fbATM(
    stYStage:=stYStage,
    stXStage:=stXStage,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=TRUE,
    bEnableBeamParams:=TRUE,
    bEnablePositionLimits:=TRUE,
    sDeviceName:='DEVICE',
    sTransitionKey:='T9',
    bReadDBNow:=NOT bInit,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_ATM_States.TARGET5;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestATMStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#10s);

// Start in Unknown, then go through the state positions one by one
IF fbATM.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbATM.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in ATM move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_AttenuatorElementDensity

FUNCTION_BLOCK FB_AttenuatorElementDensity
VAR_INPUT
    sName : STRING;
END_VAR
VAR_OUTPUT
    fDensity : LREAL;
END_VAR
VAR
    fbElementDensity : FB_ElementDensity;
END_VAR
IF sName = 'C' THEN
    (* Special-case diamond here *)
    fDensity := 3.51E6;  (* C (Diamond) g/m^3 *)
ELSE
    fbElementDensity(sName:=sName);
    IF fbElementDensity.bFound THEN
        fDensity := fbElementDensity.fValue * 1.0E6; (* g/cm^3 -> g/m^3 *)
    ELSE
        fDensity := 0.0;
    END_IF
END_IF

END_FUNCTION_BLOCK

FB_CheckPositionStateWrite

FUNCTION_BLOCK FB_CheckPositionStateWrite
(*
    Save a position state during one cycle, then check it the next cycle
*)
VAR_IN_OUT
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
END_VAR
VAR_INPUT
    bCheck: BOOL;
    bSave: BOOL;
END_VAR
VAR_OUTPUT
    bHadWrite: BOOL;
END_VAR
VAR
    astCache: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    nIter: UINT;
END_VAR
bHadWrite := FALSE;
IF bCheck THEN
    FOR nIter := 1 TO GeneralConstants.MAX_STATES DO
        // Only the runtime-editable EPICS fields
        bHadWrite S= astPositionState[nIter].sName <> astCache[nIter].sName;
        bHadWrite S= astPositionState[nIter].fPosition <> astCache[nIter].fPosition;
        bHadWrite S= astPositionState[nIter].fDelta <> astCache[nIter].fDelta;
        bHadWrite S= astPositionState[nIter].fVelocity <> astCache[nIter].fVelocity;
        bHadWrite S= astPositionState[nIter].fAccel <> astCache[nIter].fAccel;
        bHadWrite S= astPositionState[nIter].fDecel <> astCache[nIter].fDecel;
    END_FOR
END_IF

IF bSave THEN
    astCache := astPositionState;
END_IF

END_FUNCTION_BLOCK

FB_CheckPositionStateWriteTest

FUNCTION_BLOCK FB_CheckPositionStateWriteTest EXTENDS FB_TestSuite
VAR
END_VAR
TestSaveCheck();

END_FUNCTION_BLOCK

METHOD TestSaveCheck
VAR_INST
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbCheckPositionStateWrite: FB_CheckPositionStateWrite;
END_VAR
TEST('TestSaveCheck');

fbCheckPositionStateWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);
fbCheckPositionStateWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
AssertFalse(fbCheckPositionStateWrite.bHadWrite, 'Saw a write when there was no write');
astPositionState[1].fPosition := astPositionState[1].fPosition + 1;
fbCheckPositionStateWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
AssertTrue(fbCheckPositionStateWrite.bHadWrite, 'Did not detect position value changed');

TEST_FINISHED();
END_METHOD
Related:

FB_LIC

FUNCTION_BLOCK FB_LIC
(*
    Function block for Laser In-Coupling (LIC) controls:
    - Y motion
    - Y mirror and target states
*)
VAR_IN_OUT
    // Y motor (state select).
    stYStage: ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
    // The arbiter to request beam conditions from.
    fbArbiter: FB_Arbiter;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut: ST_PositionState;
    // Settings for the MIRROR1 state.
    stMirror1: ST_PositionState;
    // Settings for the MIRROR2 state.
    stMirror2: ST_PositionState;
    // Settings for the TARGET1 state.
    stTarget1: ST_PositionState;
    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: MMS:STATE:SET
        io: io
    '}
    eEnumSet: E_LIC_States;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnableMotion: BOOL;
    // Set this to TRUE to enable beam parameter checks, or FALSE to disable them.
    bEnableBeamParams: BOOL;
    // Set this to TRUE to enable position limit checks, or FALSE to disable them.
    bEnablePositionLimits: BOOL;
    // The name of the device for use in the PMPS DB lookup and diagnostic screens.
    sDeviceName: STRING;
    // The name of the transition state in the PMPS database.
    sTransitionKey: STRING;
    // Set this to TRUE to re-read the loaded database immediately (useful for debug).
    bReadDBNow: BOOL;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: MMS:STATE:GET
        io: i
    '}
    eEnumGet: E_LIC_States;
    // The PMPS database lookup associated with the current position state.
    stDbStateParams: ST_DbStateParams;
END_VAR
VAR
    bInit: BOOL;

    fbYStage: FB_MotionStage;

    fbStateDefaults: FB_PositionState_Defaults;

    {attribute 'pytmc' := '
        pv: MMS
        astPositionState.array: 1..4
    '}
    fbStates: FB_PositionStatePMPS1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;
END_VAR
VAR CONSTANT
    // State defaults if not provided
    fDelta: LREAL := 2;
    fAccel: LREAL := 200;
    fOutDecel: LREAL := 25;
END_VAR
IF NOT bInit THEN
    bInit := TRUE;

    stYStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;

    // Partial backcompat, this used to set fVelocity too but this should be set per install
    fbStateDefaults(stPositionState:=stOut, sNameDefault:='OUT', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fOutDecel);
    fbStateDefaults(stPositionState:=stMirror1, sNameDefault:='MIRROR1', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stMirror2, sNameDefault:='MIRROR2', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget1, sNameDefault:='TARGET1', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
END_IF

stYStage.bHardwareEnable := TRUE;
stYStage.bPowerSelf := FALSE;

fbYStage(stMotionStage:=stYStage);

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_LIC_States.OUT] := stOut;
    astPositionState[E_LIC_States.MIRROR1] := stMirror1;
    astPositionState[E_LIC_States.MIRROR2] := stMirror2;
    astPositionState[E_LIC_States.TARGET] := stTarget1;
END_IF

fbStates(
    stMotionStage:=stYStage,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=bEnableMotion,
    bEnableBeamParams:=bEnableBeamParams,
    bEnablePositionLimits:=bEnablePositionLimits,
    sDeviceName:=sDeviceName,
    sTransitionKey:=sTransitionKey,
    bReadDBNow:=bReadDBNow,
    stDbStateParams=>stDbStateParams,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_LIC_States.OUT];
stMirror1 := astPositionState[E_LIC_States.MIRROR1];
stMirror2 := astPositionState[E_LIC_States.MIRROR2];
stTarget1 := astPositionState[E_LIC_States.TARGET];

END_FUNCTION_BLOCK
Related:

FB_LICTest

FUNCTION_BLOCK FB_LICTest EXTENDS FB_TestSuite
VAR
    fbLIC: FB_LIC;
    stYStage: ST_MotionStage;

    stDefault: ST_PositionState := (
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;
    fbArbiter: FB_Arbiter(1);
    fbSubSysIO: FB_DummyArbIO;

    bInit: BOOL;
END_VAR
// Fake PMPS handling
fbSubSysIO(
    LA:=fbArbiter,
    FFO:=fbFFHWO,
);

// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbLIC.stOut, sName:='OUT', fPosition:=10, sPmpsState:='T0');
fbSetup(stPositionState:=fbLIC.stMirror1, sName:='T1', fPosition:=20, sPmpsState:='T1');
fbSetup(stPositionState:=fbLIC.stMirror2, sName:='T2', fPosition:=30, sPmpsState:='T2');

// Standard FB call
fbLIC(
    stYStage:=stYStage,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=TRUE,
    bEnableBeamParams:=TRUE,
    bEnablePositionLimits:=TRUE,
    sDeviceName:='DEVICE',
    sTransitionKey:='T9',
    bReadDBNow:=NOT bInit,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_LIC_States.MIRROR2;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestLICStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#10s);

// Start in Unknown, then go through the state positions one by one
IF fbLIC.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbLIC.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in LIC move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_PositionState_Defaults

FUNCTION_BLOCK FB_PositionState_Defaults
VAR_IN_OUT
    stPositionState: ST_PositionState;
END_VAR
VAR_INPUT
    sNameDefault: STRING;
    fVeloDefault: LREAL;
    fDeltaDefault: LREAL;
    fAccelDefault: LREAL;
    fDecelDefault: LREAL;
END_VAR
IF stPositionState.sName = '' OR stPositionState.sName = 'Invalid' THEN
    stPositionState.sName := sNameDefault;
END_IF
IF stPositionState.fVelocity = 0 THEN
    stPositionState.fVelocity := fVeloDefault;
END_IF
IF stPositionState.fDelta = 0 THEN
    stPositionState.fDelta := fDeltaDefault;
END_IF
IF stPositionState.fAccel = 0 THEN
    stPositionState.fAccel := fAccelDefault;
END_IF
IF stPositionState.fDecel = 0 THEN
    stPositionState.fDecel := fDecelDefault;
END_IF

END_FUNCTION_BLOCK

FB_PPM

FUNCTION_BLOCK FB_PPM
(*
    Function block for Power and Profile Monitor (PPM) controls:
    - Y motion
    - Y power and profile states
    - power meter
    - camera power
    - camera illuminator
    - flow meter
    - thermocouple

    The following IO link points are included and should be used:
    - FB_PPM.fbPowerMeter.iVoltageINT should be linked to the power meter analog input voltage
    - FB_PPM.fbPowerMeter.fbThermoCouple.bError, bUnderrange, bOverrange, and iRaw should be linked to the corresponding thermocouple terminal inputs.
    - FB_PPM.fbGige.iIlluminatorINT should be linked to illuminator dimmer analog output voltage
    - FB_PPM.fbGige.bGigePower should be linked to the camera power digial output channel
    - FB_PPM.fbFlowMeter.iRaw should be linked to the flow meter current analog input channel
    - FB_PPM.fbYagThermoCouple.bError, bUnderrange, bOverrange, and iRaw should be linked to the corresponding thermocouple terminal inputs.
*)
VAR_IN_OUT
    // Y motor (state select).
    stYStage: ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
    // The arbiter to request beam conditions from.
    fbArbiter: FB_Arbiter;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut: ST_PositionState;
    // Settings for the POWERMETER state.
    stPower: ST_PositionState;
    // Settings for the YAG1 state.
    stYag1: ST_PositionState;
    // Settings for the YAG2 state.
    stYag2: ST_PositionState;
    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: MMS:STATE:SET
        io: io
    '}
    eEnumSet: E_PPM_States;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnableMotion: BOOL;
    // Set this to TRUE to enable beam parameter checks, or FALSE to disable them.
    bEnableBeamParams: BOOL;
    // Set this to TRUE to enable position limit checks, or FALSE to disable them.
    bEnablePositionLimits: BOOL;
    // The name of the device for use in the PMPS DB lookup and diagnostic screens.
    sDeviceName: STRING;
    // The name of the transition state in the PMPS database.
    sTransitionKey: STRING;
    // Set this to TRUE to re-read the loaded database immediately (useful for debug).
    bReadDBNow: BOOL;
    // Offset for the flow meter in engineering units
    fFlowOffset: LREAL := 0;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: MMS:STATE:GET
        io: i
    '}
    eEnumGet: E_PPM_States;
    // The PMPS database lookup associated with the current position state.
    stDbStateParams: ST_DbStateParams;
END_VAR
VAR
    bInit: BOOL;

    fbYStage: FB_MotionStage;

    fbStateDefaults: FB_PositionState_Defaults;

    {attribute 'pytmc' := '
        pv: MMS
        astPositionState.array: 1..4
    '}
    fbStates: FB_PositionStatePMPS1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;

    {attribute 'pytmc' := 'pv: SPM'}
    fbPowerMeter: FB_PPM_PowerMeter;

    {attribute 'pytmc' := 'pv: CAM'}
    fbGige: FB_PPM_Gige;

    {attribute 'pytmc' :='pv: FWM'}
    fbFlowMeter: FB_AnalogInput := (iTermBits:=15, fTermMax:=60, fTermMin:=0);

    {attribute 'pytmc' := 'pv: YAG'}
    fbYagThermoCouple: FB_ThermoCouple;

    {attribute 'pytmc' := 'pv: FSW'}
    fbFlowSwitch: FB_XTES_Flowswitch;
END_VAR
VAR CONSTANT
    // State defaults if not provided
    fDelta: LREAL := 2;
    fAccel: LREAL := 200;
    fOutDecel: LREAL := 25;
END_VAR
IF NOT bInit THEN
    bInit := TRUE;

    stYStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;

    // Partial backcompat, this used to set fVelocity too but this should be set per install
    fbStateDefaults(stPositionState:=stOut, sNameDefault:='OUT', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fOutDecel);
    fbStateDefaults(stPositionState:=stPower, sNameDefault:='POWERMETER', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stYag1, sNameDefault:='YAG1', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stYag2, sNameDefault:='YAG2', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
END_IF

stYStage.bHardwareEnable := TRUE;
stYStage.bPowerSelf := FALSE;

fbYStage(stMotionStage:=stYStage);

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_PPM_States.OUT] := stOut;
    astPositionState[E_PPM_States.POWERMETER] := stPower;
    astPositionState[E_PPM_States.YAG1] := stYag1;
    astPositionState[E_PPM_States.YAG2] := stYag2;
END_IF

fbStates(
    stMotionStage:=stYStage,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=bEnableMotion,
    bEnableBeamParams:=bEnableBeamParams,
    bEnablePositionLimits:=bEnablePositionLimits,
    sDeviceName:=sDeviceName,
    sTransitionKey:=sTransitionKey,
    bReadDBNow:=bReadDBNow,
    stDbStateParams=>stDbStateParams,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_PPM_States.OUT];
stPower := astPositionState[E_PPM_States.POWERMETER];
stYag1 := astPositionState[E_PPM_States.YAG1];
stYag2 := astPositionState[E_PPM_States.YAG2];

fbPowerMeter();
fbGige();
fbFlowMeter(fOffset:=fFlowOffset);
fbYagThermoCouple();
fbFlowSwitch();

END_FUNCTION_BLOCK
Related:

FB_PPM_Gige

FUNCTION_BLOCK FB_PPM_Gige
VAR
    iIlluminatorINT AT %Q*: INT;

    {attribute 'pytmc' := '
        pv: PWR
        field: ZNAM OFF
        field: ONAM ON
    '}
    bGigePower AT %Q*: BOOL;

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

    fbGetIllPercent: FB_AnalogInput;
    fbSetIllPercent: FB_AnalogOutput;

    bGigeInit: BOOL := FALSE;
END_VAR
// Turn the GigE on by default
IF NOT bGigeInit THEN
    bGigePower := TRUE;
    bGigeInit := TRUE;
END_IF

// Illuminator conversion to percentage
fbSetIllPercent(
    fReal:=fIlluminatorPercent,
    fSafeMax:=100,
    fSafeMin:=0,
    iTermBits:=15,
    fTermMax:=100,
    fTermMin:=0,
    iRaw=>iIlluminatorINT);
fbGetIllPercent(
    iRaw:=iIlluminatorINT,
    iTermBits:=15,
    fTermMax:=100,
    fTermMin:=0,
    fReal=>fIlluminatorPercent);

END_FUNCTION_BLOCK

FB_PPM_PowerMeter

FUNCTION_BLOCK FB_PPM_PowerMeter
VAR
    iVoltageINT AT %I*: INT;

    {attribute 'pytmc' := '
        pv: VOLT
        io: input
        field: EGU mV
    '}
    fVoltage: LREAL;

    {attribute 'pytmc' := '
        pv: VOLT_BUFFER
        io: input
        field: EGU mV
    '}
    fVoltageBuffer: ARRAY[1..1000] OF LREAL;

    {attribute 'pytmc' := '
        pv: CALIB
        io: input
    '}
    fCalibBase: LREAL;

    {attribute 'pytmc' := '
        pv: CALIB_BUFFER
        io: input
    '}
    fCalibBaseBuffer: ARRAY[1..1000] OF LREAL;

    {attribute 'pytmc' := '
        pv: MJ
        io: input
        field: EGU mJ
    '}
    fCalibMJ: LREAL;

    {attribute 'pytmc' := '
        pv: MJ_BUFFER
        io: input
        field: EGU mJ
    '}
    fCalibMJBuffer: ARRAY[1..1000] OF LREAL;

    {attribute 'pytmc' := '
        pv:
        io: input
    '}
    fbThermoCouple: FB_ThermoCouple;

    {attribute 'pytmc' := '
        pv: CALIB:OFFSET
        io: io
    '}
    fCalibRelOffset: LREAL;

    {attribute 'pytmc' := '
        pv: CALIB:RATIO
        io: io
    '}
    fCalibRelRatio: LREAL;

    {attribute 'pytmc' := '
        pv: CALIB:MJ_RATIO
        io: io
    '}
    fCalibMJRatio: LREAL;

    fbGetPMVoltage: FB_AnalogInput;
    fbVoltageBuffer: FB_LREALBuffer;
    fbCalibBaseBuffer: FB_LREALBuffer;
    fbCalibMJBuffer: FB_LREALBuffer;
END_VAR
fbThermoCouple();

// Convert the terminal's integer into a value in millivolts
fbGetPMVoltage(
    iRaw := iVoltageINT,
    iTermBits := 15,
    fTermMax := 10000,
    fTermMin := 0,
    fReal => fVoltage);

// Power meter calibration
fCalibBase := (fVoltage + fCalibRelOffset) * fCalibRelRatio;
fCalibMJ := fCalibBase * fCalibMJRatio;

// Buffer the full-rate Voltage and calibrated MJ values
fbVoltageBuffer(
    bExecute := TRUE,
    fInput := fVoltage,
    arrOutput => fVoltageBuffer);
fbCalibBaseBuffer(
    bExecute := TRUE,
    fInput := fCalibBase,
    arrOutput => fCalibBaseBuffer);
fbCalibMJBuffer(
    bExecute := TRUE,
    fInput := fCalibMJ,
    arrOutput => fCalibMJBuffer);

END_FUNCTION_BLOCK

FB_PPMTest

FUNCTION_BLOCK FB_PPMTest EXTENDS FB_TestSuite
VAR
    fbPPM: FB_PPM;
    stYStage: ST_MotionStage;

    stDefault: ST_PositionState := (
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;
    fbArbiter: FB_Arbiter(1);
    fbSubSysIO: FB_DummyArbIO;

    bInit: BOOL;
END_VAR
// Fake PMPS handling
fbSubSysIO(
    LA:=fbArbiter,
    FFO:=fbFFHWO,
);

// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbPPM.stOut, sName:='OUT', fPosition:=10, sPmpsState:='T0');
fbSetup(stPositionState:=fbPPM.stPower, sName:='T1', fPosition:=20, sPmpsState:='T1');
fbSetup(stPositionState:=fbPPM.stYag1, sName:='T2', fPosition:=30, sPmpsState:='T2');
fbSetup(stPositionState:=fbPPM.stYag2, sName:='T3', fPosition:=40, sPmpsState:='T3');

// Standard FB call
fbPPM(
    stYStage:=stYStage,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=TRUE,
    bEnableBeamParams:=TRUE,
    bEnablePositionLimits:=TRUE,
    sDeviceName:='DEVICE',
    sTransitionKey:='T9',
    bReadDBNow:=NOT bInit,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_PPM_States.YAG2;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestPPMStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#10s);

// Start in Unknown, then go through the state positions one by one
IF fbPPM.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbPPM.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in PPM move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_REF

FUNCTION_BLOCK FB_REF
(*
    Function block for reference laser (REF) controls:
    - Y motion
    - Y in and out states
    - laser power and dimmer

    The following IO link points are included and should be used:
    - FB_REF.fbLaser.iShutdownINT should be linked to the output for the laser dimmer's shutdown voltage
    - FB_REF.fbLaser.iLaserINT should be linked to the output for the laser dimmer's dimmer
*)
VAR_IN_OUT
    // Y motor (state select).
    stYStage: ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
    // The arbiter to request beam conditions from.
    fbArbiter: FB_Arbiter;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut: ST_PositionState;
    // Settings for the IN state.
    stIn: ST_PositionState;
    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: MMS:STATE:SET
        io: io
    '}
    eEnumSet: E_EpicsInOut;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnableMotion: BOOL;
    // Set this to TRUE to enable beam parameter checks, or FALSE to disable them.
    bEnableBeamParams: BOOL;
    // Set this to TRUE to enable position limit checks, or FALSE to disable them.
    bEnablePositionLimits: BOOL;
    // The name of the device for use in the PMPS DB lookup and diagnostic screens.
    sDeviceName: STRING;
    // The name of the transition state in the PMPS database.
    sTransitionKey: STRING;
    // Set this to TRUE to re-read the loaded database immediately (useful for debug).
    bReadDBNow: BOOL;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: MMS:STATE:GET
        io: i
    '}
    eEnumGet: E_EpicsInOut;
    // The PMPS database lookup associated with the current position state.
    stDbStateParams: ST_DbStateParams;
END_VAR
VAR
    bInit: BOOL;

    fbYStage: FB_MotionStage;

    fbStateDefaults: FB_PositionState_Defaults;

    {attribute 'pytmc' := '
        pv: MMS
        astPositionState.array: 1..2
    '}
    fbStates: FB_PositionStatePMPS1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;

    {attribute 'pytmc' := 'pv: LAS'}
    fbLaser: FB_REF_Laser;
END_VAR
VAR CONSTANT
    fDelta: LREAL := 2;
    fAccel: LREAL := 10;
END_VAR
IF NOT bInit THEN
    bInit := TRUE;

    stYStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;

    // Partial backcompat, this used to set fVelocity too but this should be set per install
    fbStateDefaults(stPositionState:=stOut, sNameDefault:='OUT', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stIn, sNameDefault:='IN', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
END_IF

stYStage.bHardwareEnable := TRUE;
stYStage.bPowerSelf := FALSE;

fbYStage(stMotionStage:=stYStage);

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_EpicsInOut.OUT] := stOut;
    astPositionState[E_EpicsInOut.IN] := stIn;
END_IF

fbStates(
    stMotionStage:=stYStage,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=bEnableMotion,
    bEnableBeamParams:=bEnableBeamParams,
    bEnablePositionLimits:=bEnablePositionLimits,
    sDeviceName:=sDeviceName,
    sTransitionKey:=sTransitionKey,
    bReadDBNow:=bReadDBNow,
    stDbStateParams=>stDbStateParams,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_EpicsInOut.OUT];
stIn := astPositionState[E_EpicsInOut.IN];

fbLaser();

END_FUNCTION_BLOCK
Related:

FB_REF_Laser

FUNCTION_BLOCK FB_REF_Laser
VAR_INPUT
    bShutdown: BOOL;

    {attribute 'pytmc' := '
        pv: PCT
        io: io
    '}
    fLaserPercent: LREAL;
END_VAR
VAR
    iShutdownINT AT %Q*: INT;
    iLaserINT AT %Q*: INT;

    fbGetLasPercent: FB_AnalogInput;
    fbSetLasPercent: FB_AnalogOutput;
END_VAR
// Send 5V to suppress laser
IF bShutdown THEN
    iShutdownINT := LREAL_TO_INT(EXPT(2, 14));
ELSE
    iShutdownINT := 0;
END_IF

// Limit to 0-5V instead of 10V
fbSetLasPercent(
    fReal:=fLaserPercent,
    fSafeMax:=100,
    fSafeMin:=0,
    iTermBits:=15,
    fTermMax:=200,
    fTermMin:=0,
    iRaw=>iLaserInt);
fbGetLasPercent(
    iRaw:=iLaserInt,
    iTermBits:=15,
    fTermMax:=200,
    fTermMin:=0,
    fReal=>fLaserPercent);

END_FUNCTION_BLOCK

FB_REFTest

FUNCTION_BLOCK FB_REFTest EXTENDS FB_TestSuite
VAR
    fbREF: FB_REF;
    stYStage: ST_MotionStage;

    stDefault: ST_PositionState := (
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;
    fbArbiter: FB_Arbiter(1);
    fbSubSysIO: FB_DummyArbIO;

    bInit: BOOL;
END_VAR
// Fake PMPS handling
fbSubSysIO(
    LA:=fbArbiter,
    FFO:=fbFFHWO,
);

// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbREF.stOut, sName:='OUT', fPosition:=10, sPmpsState:='T0');
fbSetup(stPositionState:=fbREF.stIn, sName:='T1', fPosition:=20, sPmpsState:='T1');

// Standard FB call
fbREF(
    stYStage:=stYStage,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=TRUE,
    bEnableBeamParams:=TRUE,
    bEnablePositionLimits:=TRUE,
    sDeviceName:='DEVICE',
    sTransitionKey:='T9',
    bReadDBNow:=NOT bInit,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_EpicsInOut.IN;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestREFStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#10s);

// Start in Unknown, then go through the state positions one by one
IF fbREF.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbREF.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in REF move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_SATTTest

FUNCTION_BLOCK FB_SATTTest EXTENDS FB_TestSuite
VAR
    fbSATT: FB_SXR_SATT_Stage;
    stYStage: ST_MotionStage;

    stDefault: ST_PositionState := (
        fDelta:=0.5,
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;

    bInit: BOOL;
END_VAR
// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbSATT.stOut, sName:='OUT', fPosition:=10);
fbSetup(stPositionState:=fbSATT.stFilter1, sName:='T1', fPosition:=20);
fbSetup(stPositionState:=fbSATT.stFilter2, sName:='T2', fPosition:=30);
fbSetup(stPositionState:=fbSATT.stFilter3, sName:='T3', fPosition:=40);
fbSetup(stPositionState:=fbSATT.stFilter4, sName:='T4', fPosition:=50);
fbSetup(stPositionState:=fbSATT.stFilter5, sName:='T5', fPosition:=60);
fbSetup(stPositionState:=fbSATT.stFilter6, sName:='T6', fPosition:=70);
fbSetup(stPositionState:=fbSATT.stFilter7, sName:='T7', fPosition:=80);
fbSetup(stPositionState:=fbSATT.stFilter8, sName:='T8', fPosition:=90);

// Standard FB call
fbSATT(
    stAxis:=stYStage,
    fbFFHWO:=fbFFHWO,
    bEnable:=TRUE,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_SXR_SATT_Position.FILTER8;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestSATTStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#20s);

// Start in Unknown, then go through the state positions one by one
IF fbSATT.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbSATT.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in SATT move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_SLITS

FUNCTION_BLOCK FB_SLITS


END_FUNCTION_BLOCK

ACTION ACT_BLOCK:

END_ACTION

ACTION ACT_CalculatePositions:

END_ACTION

FB_SLITS_POWER

FUNCTION_BLOCK FB_SLITS_POWER EXTENDS FB_SLITS
VAR_IN_OUT
    stTopBlade: ST_MotionStage;
    stBottomBlade: ST_MotionStage;
    stNorthBlade: ST_MotionStage;
    stSouthBlade: ST_MotionStage;
END_VAR
VAR_INPUT
    {attribute 'pytmc' := '
    pv: GO;
    io: io;
    field: ZNAM False
    field: ONAM True
    '}
    bExecuteMotion:BOOL ;
    {attribute 'pytmc' := '
    pv: PMPS_OK;
    io: i;
    field: ZNAM False
    field: ONAM True
    '}
    bMoveOk:BOOL;
END_VAR
VAR_OUTPUT
END_VAR
VAR
    fbTopBlade: FB_MotionStage;
    fbBottomBlade: FB_MotionStage;
    fbNorthBlade: FB_MotionStage;
    fbSouthBlade: FB_MotionStage;
    fPosTopBlade: LREAL;
    fPosBottomBlade: LREAL;
    fPosNorthBlade: LREAL;
    fPosSouthBlade: LREAL;

    (*Motion Parameters*)
    fSmallDelta: LREAL := 0.01;
    fBigDelta: LREAL := 10;
    fMaxVelocity: LREAL := 0.2;
    fHighAccel: LREAL := 0.8;
    fLowAccel: LREAL := 0.1;

    stTop: ST_PositionState;
    stBOTTOM: ST_PositionState;
    stNorth: ST_PositionState;
    stSouth: ST_PositionState;

    {attribute 'pytmc' := 'pv: TOP'}
    fbTop: FB_StatePTPMove;
    {attribute 'pytmc' := 'pv: BOTTOM'}
    fbBottom: FB_StatePTPMove;
    {attribute 'pytmc' := 'pv: NORTH'}
    fbNorth: FB_StatePTPMove;
    {attribute 'pytmc' := 'pv: SOUTH'}
    fbSouth: FB_StatePTPMove;

    (*EPICS pvs*)
    {attribute 'pytmc' := '
    pv: XWID_REQ;
    io: io;
    '}
    rReqApertureSizeX : REAL;
    {attribute 'pytmc' := '
    pv: YWID_REQ;
    io: io;
    '}
    rReqApertureSizeY : REAL;
    {attribute 'pytmc' := '
    pv: XCEN_REQ;
    io: io;
    '}
    rReqCenterX: REAL;
    {attribute 'pytmc' := '
    pv: YCEN_REQ;
    io: io;
    '}
    rReqCenterY: REAL;

    {attribute 'pytmc' := '
    pv: ACTUAL_XWIDTH;
    io: io;
    '}
    rActApertureSizeX : REAL;

    {attribute 'pytmc' := '
    pv: ACTUAL_YWIDTH;
    io: io;
    '}
    rActApertureSizeY : REAL;
    {attribute 'pytmc' := '
    pv: ACTUAL_XCENTER;
    io: io;
    '}
    rActCenterX: REAL;
    {attribute 'pytmc' := '
    pv: ACTUAL_YCENTER;
    io: io;
    '}
    rActCenterY: REAL;

    {attribute 'pytmc' := '
    pv: XCEN_SETZERO;
    io: io;
    '}
    rSetCenterX: REAL;
    {attribute 'pytmc' := '
    pv: YCEN_SETZERO;
    io: io;
    '}
    rSetCenterY: REAL;


    {attribute 'pytmc' := '
    pv: OPEN;
    io: io;
    field: ZNAM False
    field: ONAM True
    '}
    bOpen: BOOL;

    {attribute 'pytmc' := '
    pv: CLOSE;
    io: io;
    field: ZNAM False
    field: ONAM True
    '}
    bClose: BOOL;

    {attribute 'pytmc' := '
    pv: BLOCK;
    io: io;
    field: ZNAM False
    field: ONAM True
    '}
    bBlock: BOOL;


    {attribute 'pytmc' := '
        pv: FSW
    '}
    fbFlowSwitch: FB_XTES_Flowswitch;


    //RTDs
    {attribute 'pytmc' := '
        pv: TOP:RTD:01
    '}
    RTD_TOP_1: FB_TempSensor;
    {attribute 'pytmc' := '
        pv: TOP:RTD:02
    '}
    RTD_TOP_2: FB_TempSensor;
    {attribute 'pytmc' := '
        pv: BOTTOM:RTD:01
    '}
    RTD_Bottom_1: FB_TempSensor;
    {attribute 'pytmc' := '
        pv: BOTTOM:RTD:02
    '}
    RTD_Bottom_2: FB_TempSensor;

    {attribute 'pytmc' := '
        pv: NORTH:RTD:01
    '}
    RTD_North_1: FB_TempSensor;
    {attribute 'pytmc' := '
        pv: NORTH:RTD:02
    '}
    RTD_North_2: FB_TempSensor;
    {attribute 'pytmc' := '
        pv: SOUTH:RTD:01
    '}
    RTD_South_1: FB_TempSensor;
    {attribute 'pytmc' := '
        pv: SOUTH:RTD:02
    '}
    RTD_South_2: FB_TempSensor;

        //Local variables
    bInit: BOOL :=true;
    rTrig_Block: R_TRIG;
    rTrig_Open: R_TRIG;
    rTrig_Close: R_TRIG;

    fPosBlock: LREAL;
    fPosClose: LREAL;
    fPosOpen: LREAL;


END_VAR
//  init the motion stages parameters
IF ( bInit) THEN
    stTopBlade.bHardwareEnable := TRUE;
    stBottomBlade.bHardwareEnable := TRUE;
    stNorthBlade.bHardwareEnable := TRUE;
    stSouthBlade.bHardwareEnable := TRUE;
    stTopBlade.bPowerSelf :=TRUE;
    stBottomBlade.bPowerSelf :=TRUE;
    stNorthBlade.bPowerSelf :=TRUE;
    stSouthBlade.bPowerSelf :=TRUE;
    stTopBlade.nBrakeMode := ENUM_StageBrakeMode.NO_BRAKE;
    stBottomBlade.nBrakeMode := ENUM_StageBrakeMode.NO_BRAKE;
    stNorthBlade.nBrakeMode := ENUM_StageBrakeMode.NO_BRAKE;
    stSouthBlade.nBrakeMode := ENUM_StageBrakeMode.NO_BRAKE;
END_IF


// Instantiate Function block for all the blades
fbTopBlade(stMotionStage:=stTopBlade);
fbBottomBlade(stMotionStage:=stBottomBlade);
fbNorthBlade(stMotionStage:=stNorthBlade);
fbSouthBlade(stMotionStage:=stSouthBlade);

//SET and GET the requested and Actual values
ACT_CalculatePositions();
//ACT_BLOCK();

// PTP Motion for each blade
stTop.sName := 'Top';
stTop.fPosition := fPosTopBlade;
stTop.fDelta := fSmallDelta;
stTop.fVelocity := fMaxVelocity;
stTop.fAccel := fHighAccel;
stTop.fDecel := fHighAccel;

stBOTTOM.sName := 'Bottom';
stBOTTOM.fPosition := fPosBottomBlade;
stBOTTOM.fDelta := fSmallDelta;
stBOTTOM.fVelocity := fMaxVelocity;
stBOTTOM.fAccel := fHighAccel;
stBOTTOM.fDecel := fHighAccel;

stNorth.sName := 'North';
stNorth.fPosition := fPosNorthBlade;
stNorth.fDelta := fSmallDelta;
stNorth.fVelocity := fMaxVelocity;
stNorth.fAccel := fHighAccel;
stNorth.fDecel := fHighAccel;

stSouth.sName := 'South';
stSouth.fPosition := fPosSouthBlade;
stSouth.fDelta := fSmallDelta;
stSouth.fVelocity := fMaxVelocity;
stSouth.fAccel := fHighAccel;
stSouth.fDecel := fHighAccel;

fbTop.bExecute := fbBottom.bExecute :=fbNorth.bExecute := fbSouth.bExecute := bExecuteMotion;

fbTop(
    stPositionState:=stTop,
    bMoveOk:=bMoveOk,
    stMotionStage:=stTopBlade);

fbBottom(
    stPositionState:=stBOTTOM,
    bMoveOk:=bMoveOk,
    stMotionStage:=stBottomBlade);

fbNorth(
    stPositionState:=stNorth,
    bMoveOk:=bMoveOk,
    stMotionStage:=stNorthBlade);

fbSouth(
    stPositionState:=stSouth,
    bMoveOk:=bMoveOk,
    stMotionStage:=stSouthBlade);


////RTDs
RTD_TOP_1();
RTD_TOP_2();
RTD_Bottom_1();
RTD_Bottom_2();
RTD_North_1();
RTD_North_2();
RTD_South_1();
RTD_South_2();

//Flow Switch
fbFlowSwitch();

END_FUNCTION_BLOCK

ACTION ACT_BLOCK:
rTrig_Block (CLK:= bBlock);
rTrig_Open (CLK:= bOpen);
rTrig_Close (CLK:= bClose);

if (rTrig_Block.Q) THEN
    //BLOCK

    bBlock := false;
END_IF

if (rTrig_Open.Q) THEN


    bOpen := false;
END_IF

if (rTrig_Close.Q) THEN


    bClose := false;
END_IF
END_ACTION

ACTION ACT_CalculatePositions:
//Calculate requested Positions


fPosTopBlade := (rReqApertureSizeY/2) + rReqCenterY;
fPosBottomBlade := (-1*rReqApertureSizeY/2) + rReqCenterY;

fPosNorthBlade := (rReqApertureSizeX/2) + rReqCenterX;
fPosSouthBlade := (-1*rReqApertureSizeX/2) + rReqCenterX;


//Calculate Actual Positions


rActApertureSizeX := ABS(stNorthBlade.stAxisStatus.fActPosition - stSouthBlade.stAxisStatus.fActPosition);

rActApertureSizeY := ABS(stTopBlade.stAxisStatus.fActPosition - stBottomBlade.stAxisStatus.fActPosition);

rActCenterX := ((stNorthBlade.stAxisStatus.fActPosition + stSouthBlade.stAxisStatus.fActPosition)/2);

rActCenterY := ((stTopBlade.stAxisStatus.fActPosition + stBottomBlade.stAxisStatus.fActPosition)/2);



//ZERO BIAS

// Set Y center to zero

// Set X center to zero
END_ACTION
Related:

FB_SXR_SATT_Stage

FUNCTION_BLOCK FB_SXR_SATT_Stage
(*
    Function block for a single Solid ATTenuator (SATT) filter stack.
    This is for the L2SI compact design with 4 filter stacks that each have
    many filters.

    This function block controls:
    - Y motion
    - Y filter states
    - transmission calculation for one stack

    There will be a fast fault during motion, but no other PMPS restrictions.
*)
VAR_IN_OUT
    // Y motor (filter select)
    stAxis : ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut           : ST_PositionState;
    // Settings for the FILTER1 state.
    stFilter1       : ST_PositionState;
    // Settings for the FILTER2 state.
    stFilter2       : ST_PositionState;
    // Settings for the FILTER3 state.
    stFilter3       : ST_PositionState;
    // Settings for the FILTER4 state.
    stFilter4       : ST_PositionState;
    // Settings for the FILTER5 state.
    stFilter5       : ST_PositionState;
    // Settings for the FILTER6 state.
    stFilter6       : ST_PositionState;
    // Settings for the FILTER7 state.
    stFilter7       : ST_PositionState;
    // Settings for the FILTER8 state.
    stFilter8       : ST_PositionState;

    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: STATE:SET
        io: io
    '}
    eEnumSet: E_SXR_SATT_Position;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnable: BOOL;

    // Filter configuration information
    {attribute 'pytmc' := 'pv: FILTERS'}
    arrFilters: ARRAY[1..8] OF ST_SATT_Filter;

    // Debug helper for setting the stage's nEnableMode
    nEnableMode : E_StageEnableMode;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: STATE:GET
        io: i
    '}
    eEnumGet: E_SXR_SATT_Position;

    fTemp1 : LREAL;
    fTemp2 : LREAL;
    bIsStationary : BOOL;
    bError : BOOL;

    {attribute 'pytmc' := '
        pv: MATERIAL
        io: i
    '}
    sActiveFilterMaterial : STRING;

    {attribute 'pytmc' := '
        pv: THICKNESS
        io: i
        field: EGU um
    '}
    fActiveFilterThickness_um : LREAL;

    {attribute 'pytmc' := '
        pv: TRANSMISSION
        io: i
        field: DESC Filter transmission
    '}
    fTransmission : LREAL;
    fActiveFilterDensity : LREAL;
    fActiveFilterAtomicMass : LREAL;
    fAbsorptionConstant : LREAL;

    iFilterIndex: INT := 0;
END_VAR
VAR
    fbMotion: FB_MotionStage;

    {attribute 'pytmc' := '
        pv:
        astPositionState.array: 1..9
    '}
    fbStates: FB_PositionState1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;

    bInitialized: BOOL := FALSE;

    fbAtomicMass : FB_AtomicMass;
    fbAttenuatorElementDensity : FB_AttenuatorElementDensity;

    (* EL3202-0020: 0.01 °C per digit *)
    {attribute 'pytmc' := 'pv: RTD:1'}
    fbRTD_1: FB_TempSensor := ( fResolution:=0.01 );
    {attribute 'pytmc' := 'pv: RTD:2'}
    fbRTD_2: FB_TempSensor := ( fResolution:=0.01 );

    fbFF: FB_FastFault := (i_Desc := 'Device is moving',
                           i_TypeCode := E_MotionFFType.DEVICE_MOVE,
                           i_xAutoReset := TRUE);
END_VAR
IF NOT bInitialized THEN
    bInitialized := TRUE;

    (* Defaults for ST_MotionStage *)
    stAxis.bHardwareEnable      := TRUE;
    stAxis.bLimitBackwardEnable := TRUE;
    stAxis.bLimitForwardEnable  := TRUE;
    stAxis.bPowerSelf           := TRUE;
    stAxis.nBrakeMode           := ENUM_StageBrakeMode.NO_BRAKE;
    stAxis.nHomingMode          := ENUM_EpicsHomeCmd.NONE;

    (* Defaults for visualization *)
    // stExtra.fVisuStep                    := 0.1;

END_IF

fbRTD_1();
fTemp1 := fbRTD_1.fTemp;

fbRTD_2();
fTemp2 := fbRTD_2.fTemp;

stAxis.nEnableMode := nEnableMode;
fbMotion(stMotionStage:=stAxis);

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_SXR_SATT_Position.OUT] := stOut;
    astPositionState[E_SXR_SATT_Position.FILTER1] := stFilter1;
    astPositionState[E_SXR_SATT_Position.FILTER2] := stFilter2;
    astPositionState[E_SXR_SATT_Position.FILTER3] := stFilter3;
    astPositionState[E_SXR_SATT_Position.FILTER4] := stFilter4;
    astPositionState[E_SXR_SATT_Position.FILTER5] := stFilter5;
    astPositionState[E_SXR_SATT_Position.FILTER6] := stFilter6;
    astPositionState[E_SXR_SATT_Position.FILTER7] := stFilter7;
    astPositionState[E_SXR_SATT_Position.FILTER8] := stFilter8;
END_IF

fbStates(
    stMotionStage:=stAxis,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    bEnable:=bEnable,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_SXR_SATT_Position.OUT];
stFilter1 := astPositionState[E_SXR_SATT_Position.FILTER1];
stFilter2 := astPositionState[E_SXR_SATT_Position.FILTER2];
stFilter3 := astPositionState[E_SXR_SATT_Position.FILTER3];
stFilter4 := astPositionState[E_SXR_SATT_Position.FILTER4];
stFilter5 := astPositionState[E_SXR_SATT_Position.FILTER5];
stFilter6 := astPositionState[E_SXR_SATT_Position.FILTER6];
stFilter7 := astPositionState[E_SXR_SATT_Position.FILTER7];
stFilter8 := astPositionState[E_SXR_SATT_Position.FILTER8];

(* Filter indices are off by one due to "Out" being in position 1. *)
iFilterIndex := UINT_TO_INT(eEnumGet - 1);

IF iFilterIndex >= 1 AND iFilterIndex <= 8 THEN
    sActiveFilterMaterial := arrFilters[iFilterIndex].sFilterMaterial;
    fActiveFilterThickness_um := arrFilters[iFilterIndex].fFilterThickness_um;

    fbAtomicMass(sName:=sActiveFilterMaterial, fValue=>fActiveFilterAtomicMass);
    fbAttenuatorElementDensity(sName:=sActiveFilterMaterial, fDensity=>fActiveFilterDensity);

    fAbsorptionConstant := F_CalculateAbsorptionConstant(
        sElement:=sActiveFilterMaterial,
        fEnergyEV:=PMPS_GVL.stCurrentBeamParameters.neV,
        fDensity_gm3:=fActiveFilterDensity,
        fAtomicWeight:=fActiveFilterAtomicMass,
        bError=>bError,
    );
    fTransmission := F_CalculateTransmission(
        fAbsorptionConstant:=fAbsorptionConstant,
        fThickness_in_m:=fActiveFilterThickness_um * 1.0E-6
    );
ELSE
    sActiveFilterMaterial := '';
    fActiveFilterThickness_um := 0.0;
    fAbsorptionConstant := 0.0;
    fActiveFilterDensity := 0.0;
    fActiveFilterAtomicMass := 0.0;
    fTransmission := 1.0;
END_IF

bIsStationary := NOT stAxis.Axis.Status.Moving;
fbFF(
    i_DevName:=stAxis.sName,
    i_xOK:=bIsStationary AND eEnumGet <> E_SXR_SATT_Position.UNKNOWN,
    io_fbFFHWO := fbFFHWO
);

END_FUNCTION_BLOCK
Related:

FB_WFS

FUNCTION_BLOCK FB_WFS
(*
    Function block for WaveFront Sensor target (WFS) controls:
    - X, Z motion
    - Y target states
    - 2 thermocouples

    The following IO link points are included and should be used:
    - FB_WFS.fbThermoCouple1.fbThermoCouple.bError, bUnderrange, bOverrange, and iRaw should be linked to the corresponding thermocouple terminal inputs.
    - FB_WFS.fbThermoCouple1.fbThermoCouple.bError, bUnderrange, bOverrange, and iRaw should be linked to the corresponding thermocouple terminal inputs.
*)
VAR_IN_OUT
    // Y motor (state select).
    stYStage: ST_MotionStage;
    // Z motor (focus adjust).
    stZStage: ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
    // The arbiter to request beam conditions from.
    fbArbiter: FB_Arbiter;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut: ST_PositionState;
    // Settings for the TARGET1 state.
    stTarget1: ST_PositionState;
    // Settings for the TARGET2 state.
    stTarget2: ST_PositionState;
    // Settings for the TARGET3 state.
    stTarget3: ST_PositionState;
    // Settings for the TARGET4 state.
    stTarget4: ST_PositionState;
    // Settings for the TARGET5 state.
    stTarget5: ST_PositionState;
    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: MMS:STATE:SET
        io: io
    '}
    eEnumSet: E_WFS_States;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnableMotion: BOOL;
    // Set this to TRUE to enable beam parameter checks, or FALSE to disable them.
    bEnableBeamParams: BOOL;
    // Set this to TRUE to enable position limit checks, or FALSE to disable them.
    bEnablePositionLimits: BOOL;
    // The name of the device for use in the PMPS DB lookup and diagnostic screens.
    sDeviceName: STRING;
    // The name of the transition state in the PMPS database.
    sTransitionKey: STRING;
    // Set this to TRUE to re-read the loaded database immediately (useful for debug).
    bReadDBNow: BOOL;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: MMS:STATE:GET
        io: i
    '}
    eEnumGet: E_WFS_States;
    // The PMPS database lookup associated with the current position state.
    stDbStateParams: ST_DbStateParams;
END_VAR
VAR
    bInit: BOOL;

    fbYStage: FB_MotionStage;
    fbZStage: FB_MotionStage;

    fbStateDefaults: FB_PositionState_Defaults;

    {attribute 'pytmc' := '
        pv: MMS
        astPositionState.array: 1..6
    '}
    fbStates: FB_PositionStatePMPS1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;

    {attribute 'pytmc' := 'pv: STC:01'}
    fbThermoCouple1: FB_TempSensor;

    {attribute 'pytmc' := 'pv: STC:02'}
    fbThermoCouple2: FB_TempSensor;

    {attribute 'pytmc' := 'pv: FSW'}
    fbFlowSwitch: FB_XTES_Flowswitch;

    {attribute 'pytmc' :='pv: FWM'}
    fbFlowMeter: FB_AnalogInput := (iTermBits:=15, fTermMax:=60, fTermMin:=0);

END_VAR
VAR CONSTANT
    // State defaults if not provided
    fDelta: LREAL := 2;
    fAccel: LREAL := 200;
    fOutDecel: LREAL := 25;
END_VAR
IF NOT bInit THEN
    bInit := TRUE;

    stYStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;
    stZStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;

    // Partial backcompat, this used to set fVelocity too but this should be set per install
    fbStateDefaults(stPositionState:=stOut, sNameDefault:='OUT', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fOutDecel);
    fbStateDefaults(stPositionState:=stTarget1, sNameDefault:='TARGET1', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget2, sNameDefault:='TARGET2', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget3, sNameDefault:='TARGET3', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget4, sNameDefault:='TARGET4', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stTarget5, sNameDefault:='TARGET5', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
END_IF

stYStage.bHardwareEnable := TRUE;
stYStage.bPowerSelf := FALSE;

stZStage.bLimitForwardEnable := TRUE;
stZStage.bLimitBackwardEnable := TRUE;
stZStage.bHardwareEnable := TRUE;
stZStage.bPowerSelf := TRUE;

fbYStage(stMotionStage:=stYStage);
fbZStage(stMotionStage:=stZStage);

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_WFS_States.OUT] := stOut;
    astPositionState[E_WFS_States.TARGET1] := stTarget1;
    astPositionState[E_WFS_States.TARGET2] := stTarget2;
    astPositionState[E_WFS_States.TARGET3] := stTarget3;
    astPositionState[E_WFS_States.TARGET4] := stTarget4;
    astPositionState[E_WFS_States.TARGET5] := stTarget5;
END_IF

fbStates(
    stMotionStage:=stYStage,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=bEnableMotion,
    bEnableBeamParams:=bEnableBeamParams,
    bEnablePositionLimits:=bEnablePositionLimits,
    sDeviceName:=sDeviceName,
    sTransitionKey:=sTransitionKey,
    bReadDBNow:=bReadDBNow,
    stDbStateParams=>stDbStateParams,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_WFS_States.OUT];
stTarget1 := astPositionState[E_WFS_States.TARGET1];
stTarget2 := astPositionState[E_WFS_States.TARGET2];
stTarget3 := astPositionState[E_WFS_States.TARGET3];
stTarget4 := astPositionState[E_WFS_States.TARGET4];
stTarget5 := astPositionState[E_WFS_States.TARGET5];

fbThermoCouple1();
fbThermoCouple2();
fbFlowSwitch(); // WFS in FEE
fbFlowMeter(); // WFS in hutch

END_FUNCTION_BLOCK
Related:

FB_WFSTest

FUNCTION_BLOCK FB_WFSTest EXTENDS FB_TestSuite
VAR
    fbWFS: FB_WFS;
    stYStage: ST_MotionStage;
    stZStage: ST_MotionStage;

    stDefault: ST_PositionState := (
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;
    fbArbiter: FB_Arbiter(1);
    fbSubSysIO: FB_DummyArbIO;

    bInit: BOOL;
END_VAR
// Fake PMPS handling
fbSubSysIO(
    LA:=fbArbiter,
    FFO:=fbFFHWO,
);

// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbWFS.stOut, sName:='OUT', fPosition:=10, sPmpsState:='T0');
fbSetup(stPositionState:=fbWFS.stTarget1, sName:='T1', fPosition:=20, sPmpsState:='T1');
fbSetup(stPositionState:=fbWFS.stTarget2, sName:='T2', fPosition:=30, sPmpsState:='T2');
fbSetup(stPositionState:=fbWFS.stTarget3, sName:='T3', fPosition:=40, sPmpsState:='T3');
fbSetup(stPositionState:=fbWFS.stTarget4, sName:='T4', fPosition:=50, sPmpsState:='T4');
fbSetup(stPositionState:=fbWFS.stTarget5, sName:='T5', fPosition:=60, sPmpsState:='T5');

// Standard FB call
fbWFS(
    stYStage:=stYStage,
    stZStage:=stZStage,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=TRUE,
    bEnableBeamParams:=TRUE,
    bEnablePositionLimits:=TRUE,
    sDeviceName:='DEVICE',
    sTransitionKey:='T9',
    bReadDBNow:=NOT bInit,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_WFS_States.TARGET5;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestWFSStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#10s);

// Start in Unknown, then go through the state positions one by one
IF fbWFS.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbWFS.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in WFS move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_XPIM

FUNCTION_BLOCK FB_XPIM
(*
    Function block for XTES Imager (XPIM = XTES PIM = Profile Impager) controls:
    - Y, Zoom, Focus motion
    - Y scintillator states
    - filterwheel serial commands
    - camera power
    - LED on/off
    - flowswitch (not fully implemented)

    The following IO link points are included and should be used:
    - FB_XPIM.bZoomEndFwd should be linked to the zoom motor's forward limit switch.
    - FB_XPIM.bZoomEndBwd should be linked to the zoom motor's backward limit switch.
    - FB_XPIM.bFocusEndFwd should be linked to the focus motor's forward limit switch.
    - FB_XPIM.bFocusEndBwd should be linked to the focus motor's backward limit switch.
    - FB_XPIM.fbOpal.bOpalPower should be linked to the camera power digital output.
    - FB_XPIM.fbLED.bLEDPower should be linked to the LED power digital output.
    - FB_XPIM.fbFlowSwitch.bFlowOk should be linked to the flow switch digital input, if present.
*)
VAR_IN_OUT
    // Y motor (state select).
    stYStage: ST_MotionStage;
    // Zoom motor (camera zoom).
    stZoomStage: ST_MotionStage;
    // Focus motor (camera focus).
    stFocusStage: ST_MotionStage;
    // The fast fault output to fault to.
    fbFFHWO: FB_HardwareFFOutput;
    // The arbiter to request beam conditions from.
    fbArbiter: FB_Arbiter;
    // Serial input
    stEl6In: EL6inData22b;
    // Serial output
    stEl6Out: EL6OutData22b;
END_VAR
VAR_INPUT
    // Settings for the OUT state.
    stOut: ST_PositionState;
    // Settings for the YAG state.
    stYag: ST_PositionState;
    // Settings for the DIAMOND state.
    stDiamond: ST_PositionState;
    // Settings for the RETICLE state.
    stReticle: ST_PositionState;
    // Set this to a non-unknown value to request a new move.
    {attribute 'pytmc' := '
        pv: MMS:STATE:SET
        io: io
    '}
    eEnumSet: E_XPIM_States;
    // Set this to TRUE to enable input state moves, or FALSE to disable them.
    bEnableMotion: BOOL;
    // Set this to TRUE to enable beam parameter checks, or FALSE to disable them.
    bEnableBeamParams: BOOL;
    // Set this to TRUE to enable position limit checks, or FALSE to disable them.
    bEnablePositionLimits: BOOL;
    // The name of the device for use in the PMPS DB lookup and diagnostic screens.
    sDeviceName: STRING;
    // The name of the transition state in the PMPS database.
    sTransitionKey: STRING;
    // Set this to TRUE to re-read the loaded database immediately (useful for debug).
    bReadDBNow: BOOL;

    // While TRUE, the zoom motor cannot be moved.
    {attribute 'pytmc' := '
        pv: CLZ:LOCK
        io: io
        field: ZNAM Unlocked
        field: ONAM Locked
    '}
    bZoomLock: BOOL;

    // While TRUE, the focus motor cannot be moved.
    {attribute 'pytmc' := '
        pv: CLF:LOCK
        io: io
        field: ZNAM Unlocked
        field: ONAM Locked
    '}
    bFocusLock: BOOL;

    // Forward limit disable for zoom
    bZoomEndFwd AT %I*: BOOL;
    // Backward limit disable for zoom
    bZoomEndBwd AT %I*: BOOL;
    // Forward limit disable for focus
    bFocusEndFwd AT %I*: BOOL;
    // Backward limit disable for focus
    bFocusEndBwd AT %I*: BOOL;
END_VAR
VAR_OUTPUT
    // The current position state as an enum.
    {attribute 'pytmc' := '
        pv: MMS:STATE:GET
        io: i
    '}
    eEnumGet: E_XPIM_States;
    // The PMPS database lookup associated with the current position state.
    stDbStateParams: ST_DbStateParams;
END_VAR
VAR
    bInit: BOOL;

    fbYStage: FB_MotionStage;
    fbZoom: FB_MotionStage;
    fbFocus: FB_MotionStage;

    fbStateDefaults: FB_PositionState_Defaults;

    {attribute 'pytmc' := '
        pv: MMS
        astPositionState.array: 1..4
    '}
    fbStates: FB_PositionStatePMPS1D;
    astPositionState: ARRAY[1..GeneralConstants.MAX_STATES] OF ST_PositionState;
    fbArrCheckWrite: FB_CheckPositionStateWrite;

    {attribute 'pytmc' := 'pv: MFW'}
    fbFilterWheel: FB_XPIM_FilterWheel;

    {attribute 'pytmc' := 'pv: CAM'}
    fbOpal: FB_XPIM_Opal;

    {attribute 'pytmc' := 'pv: CIL'}
    fbLED: FB_XPIM_LED;

    {attribute 'pytmc' := 'pv: FSW'}
    fbFlowSwitch: FB_XTES_Flowswitch;
END_VAR
VAR CONSTANT
    // State defaults if not provided
    fDelta: LREAL := 2;
    fAccel: LREAL := 200;
    fOutDecel: LREAL := 25;
END_VAR
IF NOT bInit THEN
    bInit := TRUE;

    stYStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;
    stZoomStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;
    stFocusStage.nEnableMode := ENUM_StageEnableMode.DURING_MOTION;

    // Partial backcompat, this used to set fVelocity too but this should be set per install
    fbStateDefaults(stPositionState:=stOut, sNameDefault:='OUT', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fOutDecel);
    fbStateDefaults(stPositionState:=stYag, sNameDefault:='YAG', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stDiamond, sNameDefault:='DIAMOND', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
    fbStateDefaults(stPositionState:=stReticle, sNameDefault:='RETICLE', fDeltaDefault:=fDelta, fAccelDefault:=fAccel, fDecelDefault:=fAccel);
END_IF

stYStage.bHardwareEnable := TRUE;
stYStage.bPowerSelf := FALSE;
// No limit switch at the bottom
stYStage.bLimitBackwardEnable := TRUE;

// Extra lock on lens + lens limits are NO instead of NC
stZoomStage.bHardwareEnable := NOT bZoomLock;
stZoomStage.bPowerSelf := TRUE;
stZoomStage.bLimitForwardEnable := NOT bZoomEndFwd;
stZoomStage.bLimitBackwardEnable := NOT bZoomEndBwd;
stZoomStage.nHomingMode := ENUM_EpicsHomeCmd.LOW_LIMIT;
stZoomStage.fHomePosition := 0;

stFocusStage.bHardwareEnable := NOT bFocusLock;
stFocusStage.bPowerSelf := TRUE;
stFocusStage.bLimitForwardEnable := NOT bFocusEndFwd;
stFocusStage.bLimitBackwardEnable := NOT bFocusEndBwd;
stFocusStage.nHomingMode := ENUM_EpicsHomeCmd.LOW_LIMIT;
stFocusStage.fHomePosition := 0;

fbYStage(stMotionStage:=stYStage);
fbZoom(stMotionStage:=stZoomStage);
fbFocus(stMotionStage:=stFocusStage);

// Set special error message for lens lock
IF stZoomStage.bExecute AND bZoomLock THEN
    stZoomStage.bError := TRUE;
    stZoomStage.sCustomErrorMessage := 'Zoom lens is locked!';
END_IF
IF stFocusStage.bExecute AND bFocusLock THEN
    stFocusStage.bError := TRUE;
    stFocusStage.sCustomErrorMessage := 'Focus lens is locked!';
END_IF

// We need to update from PLC or from EPICS, but not both
fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=TRUE,
    bSave:=FALSE,
);
IF NOT fbArrCheckWrite.bHadWrite THEN
    astPositionState[E_XPIM_States.OUT] := stOut;
    astPositionState[E_XPIM_States.YAG] := stYag;
    astPositionState[E_XPIM_States.DIAMOND] := stDiamond;
    astPositionState[E_XPIM_States.RETICLE] := stReticle;
END_IF

fbStates(
    stMotionStage:=stYStage,
    astPositionState:=astPositionState,
    eEnumSet:=eEnumSet,
    eEnumGet:=eEnumGet,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    bEnableMotion:=bEnableMotion,
    bEnableBeamParams:=bEnableBeamParams,
    bEnablePositionLimits:=bEnablePositionLimits,
    sDeviceName:=sDeviceName,
    sTransitionKey:=sTransitionKey,
    bReadDBNow:=bReadDBNow,
    stDbStateParams=>stDbStateParams,
);

fbArrCheckWrite(
    astPositionState:=astPositionState,
    bCheck:=FALSE,
    bSave:=TRUE,
);

stOut := astPositionState[E_XPIM_States.OUT];
stYag := astPositionState[E_XPIM_States.YAG];
stDiamond := astPositionState[E_XPIM_States.DIAMOND];
stReticle := astPositionState[E_XPIM_States.RETICLE];

fbFilterWheel(
    bExecute:=TRUE,
    stIn_El6:=stEl6In,
    stOut_El6:=stEl6Out,
);

fbOpal();
fbLED(enumXPIM:=eEnumGet);
fbFlowSwitch();

END_FUNCTION_BLOCK
Related:

FB_XPIM_FilterWheel

FUNCTION_BLOCK FB_XPIM_FilterWheel
VAR_INPUT
    bExecute: BOOL;

    {attribute 'pytmc' := '
        pv: ERR:RESET
        io: output
    '}
    bResetError: BOOL;

    {attribute 'pytmc' := '
        pv: SET
        io: io
    '}
    nSetPos: E_XPIM_Filters;
END_VAR
VAR_OUTPUT
    {attribute 'pytmc' := '
        pv: GET
        io: i
    '}
    nGetPos: E_XPIM_Filters;
    bBusy: BOOL;
    bError: BOOL;
    sError: STRING;
    {attribute 'pytmc' := '
        pv: ERR:MSG
        io: input
    '}
    sLastError: STRING;
    sErrorTS: STRING;
END_VAR
VAR_IN_OUT
    stIn_EL6: EL6inData22B;
    stOut_EL6: EL6outData22B;
END_VAR
VAR
    {attribute 'pytmc' := '
        pv: RAW
    '}
    fbCom: FB_EL6_COM;

    nStep: USINT;
    bIsTest: BOOL;
    fbGetTime: NT_GetTime;
    bStopOnErr: BOOL;
END_VAR
fbCom.sSendSuffix := '$R';
fbCom.sRecvSuffix := '$R';

IF bExecute AND nStep = 0 THEN
    IF bResetError OR NOT bError THEN
        nStep := 10;
    END_IF
ELSIF NOT bExecute THEN
    nStep := 0;
END_IF
CASE nStep OF
    0:
        ; // idle
    10:
        // Get position
        bIsTest := FALSE;
        fbCom(sCmd:='pos?',
            bSend:=TRUE,
            stIn_EL6:=stIn_EL6,
            stOut_EL6:=stOut_EL6);
        nStep := nStep + 10;
    20:
        // Wait for response and set variables
        fbCom(stIn_EL6:=stIn_EL6,
            stOut_EL6:=stOut_EL6);
        IF fbCom.bDone THEN
            bError := FALSE;
            sError := '';
            nGetPos := STRING_TO_USINT(fbCom.sResponse);
            nSetPos := nGetPos;
            nStep := nStep + 10;
            IF nGetPos = 0 THEN
                sError := 'Filter wheel in invalid state';
                bStopOnErr := TRUE;
                nStep := 50;
            END_IF
        END_IF
    30:
        // Wait for a move request
        IF nSetPos <> nGetPos THEN
            fbCom(sCmd:=CONCAT('pos=', INT_TO_STRING(nSetPos)),
                bSend:=TRUE,
                stIn_EL6:=stIn_EL6,
                stOut_EL6:=stOut_EL6);
            nStep := nStep + 10;
            bBusy := TRUE;
        END_IF
    40:
        fbCom(stIn_EL6:=stIn_EL6,
            stOut_EL6:=stOut_EL6);
        // Wait for move to be done
        IF fbCom.bDone THEN
            bBusy := FALSE;
            nStep := 10;
            // Handle setpoint error
            IF fbCom.sResponse = 'Command error CMD_ARG_INVALID$N$R' THEN
                sError := 'Invalid set position';
                nStep := 50;
            END_IF
        END_IF
    50:
        // Set sError and then jump here for standard handling
        sLastError := sError;
        bError := TRUE;
        fbGetTime(NETID:='',
            START:=TRUE);
        nStep := nStep + 10;
    60:
        // Error handling continued
        fbGetTime();
        IF NOT fbGetTime.BUSY THEN
            sErrorTS := SYSTEMTIME_TO_STRING(fbGetTime.TIMESTR);
            fbGetTime.START := FALSE;
            // set bStopOnErr to TRUE if it was a major error
            IF bStopOnErr THEN
                nStep := 0;
            ELSE
                nStep := 10;
            END_IF
            bStopOnErr := FALSE;
        END_IF
END_CASE
// Check for inner comms errors, report to EPICS same way
IF NOT bError AND
    (fbCom.eRecvErrorID <> COMERROR_NOERROR
    OR fbCom.eSendErrorID <> COMERROR_NOERROR
    OR fbCom.eRecvErrorID <> COMERROR_NOERROR) THEN
    sError := 'Serial Communication Error';
    bStopOnErr := TRUE;
    nStep := 50;
END_IF

END_FUNCTION_BLOCK
Related:

FB_XPIM_LED

FUNCTION_BLOCK FB_XPIM_LED
VAR_INPUT
    {attribute 'pytmc' := '
        pv: PWR
        io: io
        field: ZNAM OFF
        field: ONAM ON
    '}
    bLEDPower AT %Q*: BOOL;

    {attribute 'pytmc' := '
        pv: AUTO
        io: io
    '}
    bLEDAuto: BOOL := TRUE;

    {attribute 'pytmc' := '
        pv: CLK:TIMEOUT
        io: io
        field: EGU min
    '}
    fLEDTimeOut: LREAL := 10;

    enumXPIM: E_XPIM_States;
END_VAR
VAR_OUTPUT
    {attribute 'pytmc' := '
        pv: CLK:REMAINING
        io: io
        field: EGU min
    '}
    fLEDRemaining: LREAL;
END_VAR
VAR
    tonLED: TON;
    enumLastCycle: E_XPIM_States := E_XPIM_States.Unknown;
END_VAR
// If configured, change the LED level automatically
// LED is always (and only) useful at Reticle state
IF bLEDAuto AND enumXPIM <> enumLastCycle THEN
    // Turn on the LED when we get to the Reticle
    IF enumXPIM = E_XPIM_States.Reticle THEN
        bLEDPower := TRUE;
    // Turn off the LED when we stop at any other state
    ELSIF enumXPIM <> E_XPIM_States.Unknown THEN
        bLEDPower := FALSE;
    END_IF
END_IF
enumLastCycle := enumXPIM;

// If configured, start a shutdown timer when LED goes high
IF fLEDTimeOut <> 0 THEN;
    tonLED(IN:=bLEDPower,
           PT:=LREAL_TO_TIME(fLEDTimeOut * 60 * 1000));
    fLEDRemaining := fLEDTimeOut - TIME_TO_LREAL(tonLED.ET) / 60 / 1000;

    IF tonLED.Q THEN
        bLEDPower := FALSE;
    END_IF
END_IF

END_FUNCTION_BLOCK
Related:

FB_XPIM_Opal

FUNCTION_BLOCK FB_XPIM_Opal
VAR_INPUT
    {attribute 'pytmc' := '
        pv: PWR
        io: io
        field: ZNAM OFF
        field: ONAM ON
    '}
    bOpalPower AT %Q*: BOOL;
END_VAR
VAR
    bOpalInit: BOOL := FALSE;
END_VAR
// Turn the Opal on by default
IF NOT bOpalInit THEN
    bOpalPower := TRUE;
    bOpalInit := TRUE;
END_IF

END_FUNCTION_BLOCK

FB_XPIMTest

FUNCTION_BLOCK FB_XPIMTest EXTENDS FB_TestSuite
VAR
    fbXPIM: FB_XPIM;
    stYStage: ST_MotionStage;
    stZoomStage: ST_MotionStage;
    stFocusStage: ST_MotionStage;

    stEl6In: EL6inData22b;
    stEl6Out: EL6OutData22b;

    stDefault: ST_PositionState := (
        fVelocity:=10,
        bMoveOk:=TRUE,
        bValid:=TRUE
    );
    fbSetup: FB_StateSetupHelper;

    fbFFHWO: FB_HardwareFFOutput;
    fbArbiter: FB_Arbiter(1);
    fbSubSysIO: FB_DummyArbIO;

    bInit: BOOL;
END_VAR
// Fake PMPS handling
fbSubSysIO(
    LA:=fbArbiter,
    FFO:=fbFFHWO,
);

// Fake limit handling
stYStage.bLimitBackwardEnable := TRUE;
stYStage.bLimitForwardEnable := TRUE;

// Standard state setup
fbSetup(stPositionState:=stDefault, bSetDefault:=TRUE);
fbSetup(stPositionState:=fbXPIM.stOut, sName:='OUT', fPosition:=10, sPmpsState:='T0');
fbSetup(stPositionState:=fbXPIM.stReticle, sName:='T1', fPosition:=20, sPmpsState:='T1');
fbSetup(stPositionState:=fbXPIM.stYag, sName:='T2', fPosition:=30, sPmpsState:='T2');
fbSetup(stPositionState:=fbXPIM.stDiamond, sName:='T3', fPosition:=40, sPmpsState:='T3');

// Standard FB call
fbXPIM(
    stYStage:=stYStage,
    stZoomStage:=stZoomStage,
    stFocusStage:=stFocusStage,
    fbFFHWO:=fbFFHWO,
    fbArbiter:=fbArbiter,
    stEl6In:=stEl6In,
    stEl6Out:=stEl6Out,
    bEnableMotion:=TRUE,
    bEnableBeamParams:=TRUE,
    bEnablePositionLimits:=TRUE,
    sDeviceName:='DEVICE',
    sTransitionKey:='T9',
    bReadDBNow:=NOT bInit,
);

TestStateMove();

bInit := TRUE;

END_FUNCTION_BLOCK

METHOD TestStateMove
VAR_INST
    tonTimer: TON;
    nIterState: UINT := 0;
END_VAR
VAR CONSTANT
    nLastState: UINT := E_XPIM_States.RETICLE;
END_VAR
// Sanity check: can we at least move to every named state?
TEST('TestXPIMStateMove');

// Prepare a timeout
tonTimer(IN:=TRUE, PT:=T#10s);

// Start in Unknown, then go through the state positions one by one
IF fbXPIM.eEnumGet = nIterState THEN
    nIterState := nIterState + 1;
    fbXPIM.eEnumSet := nIterState;
END_IF

IF tonTimer.Q OR nIterState > nLastState THEN
    AssertFalse(tonTimer.Q, 'Timeout in XPIM move test');
    TEST_FINISHED();
END_IF
END_METHOD
Related:

FB_XTES_Flowswitch

{attribute 'analysis' := '-33'}
FUNCTION_BLOCK FB_XTES_Flowswitch
VAR_OUTPUT
    {attribute 'pytmc' := '
        pv: FLOW_OK
        field: ZNAM LOW
        field: ONAM OK
    '}
    bFlowOk AT %I*: BOOL;
END_VAR


END_FUNCTION_BLOCK

PRG_TEST

{attribute 'analysis' := '-33'}
PROGRAM PRG_TEST
VAR
    fbATMTest: FB_ATMTest;
    fbLICTest: FB_LICTest;
    fbPPMTest: FB_PPMTest;
    fbREFTest: FB_REFTest;
    fbSATTTest: FB_SATTTest;
    fbWFSTest: FB_WFSTest;
    fbXPIMTest: FB_XPIMTest;
    fbCheckPositionStateWriteTest: FB_CheckPositionStateWriteTest;

    fbSetupJson: FB_PMPSJsonTestHelper;
    astBeamParams: ARRAY[0..9] OF ST_DbStateParams;
    nIter: UINT;
END_VAR
// Setup a fake db export for the test suite
FOR nIter := 0 TO 9 DO
    astBeamParams[nIter].stBeamParams := PMPS_GVL.cstFullBeam;
    astBeamParams[nIter].sPmpsState := CONCAT('T', UINT_TO_STRING(nIter));
END_FOR
fbSetupJson(
    astBeamParams := astBeamParams,
    bExecute := TRUE,
    sDevNAme := 'DEVICE',
);
// Run the tests
TcUnit.RUN();

END_PROGRAM
Related: