(* =====================================================================
   50_prog_io_mux.st - PROGRAM IO_MUX, section 8.1

   The only POU besides 10_globals.st permitted to touch located
   variables.  Runs BEFORE CONTROL in the task list.

   Field build:      %IW / %IX  ->  process image globals
   Simulation build: PROGRAM SIMULATION outputs -> the same globals

   Because IO_MUX runs before CONTROL, the %QW / %QX values it
   publishes are the ones CONTROL computed on the PREVIOUS scan.  At a
   100 ms task that is a 100 ms publication lag on outputs only; it
   does not affect any control decision.  Noted in README.md.
   ===================================================================== *)

PROGRAM IO_MUX
VAR_EXTERNAL
    (* located - field inputs *)
    IW_LIT101 : INT;  IW_FIT201 : INT;  IW_FIT301 : INT;  IW_PIT302 : INT;
    IW_PIT311 : INT;  IW_PIT321 : INT;  IW_PIT331 : INT;
    IW_VE314  : INT;  IW_VE324  : INT;  IW_VE334  : INT;

    IX_LSHH102 : BOOL; IX_LSLL103 : BOOL; IX_LSH104 : BOOL;
    IX_TE312   : BOOL; IX_TE322   : BOOL; IX_TE332  : BOOL;
    IX_MSE313  : BOOL; IX_MSE323  : BOOL; IX_MSE333 : BOOL;
    IX_XA502   : BOOL;

    (* located - outputs *)
    QX_RunCmd1 : BOOL; QX_RunCmd2 : BOOL; QX_RunCmd3 : BOOL;
    QX_Running1 : BOOL; QX_Running2 : BOOL; QX_Running3 : BOOL;
    QX_Avail1 : BOOL; QX_Avail2 : BOOL; QX_Avail3 : BOOL;
    QX_InAuto : BOOL; QX_HighLevel : BOOL; QX_SpillActive : BOOL;
    QX_Tripped1 : BOOL; QX_Tripped2 : BOOL; QX_Tripped3 : BOOL;

    QW_Level : INT; QW_Inflow : INT; QW_Discharge : INT;
    QW_PumpsRunning : INT; QW_Speed : INT;
    QW_TimeToSpill : INT; QW_TimeToLSHH : INT; QW_NetAccum : INT;
    QW_RunHours1 : INT; QW_RunHours2 : INT; QW_RunHours3 : INT;
    QW_VolToSpill : INT; QW_StationState : INT;
    QW_PumpState1 : INT; QW_PumpState2 : INT; QW_PumpState3 : INT;
    QW_DutyPump : INT; QW_AlarmWord : INT; QW_CmdAck : INT;

    (* located - commands and setpoints *)
    MW_Mode : INT; MW_CmdWord : INT; MW_CmdParam : INT;
    MW_SpLevel : INT; MW_StartDuty : INT; MW_StartP2 : INT;
    MW_StartP3 : INT; MW_StopAll : INT; MW_HighAlarm : INT;
    MW_MinSpeed : INT; MW_ServiceHrs : INT;

    (* located - simulation control, section 8.2 *)
    MW_SimInflow : INT; MW_SimMode : INT;
    MW_SimReset : INT; MW_SimTimeScale : INT;

    (* process image *)
    g_LevelRaw_mm : INT; g_Level_mm : INT; g_Level_m : REAL;
    g_Inflow_Lps : REAL; g_Disch_Lps : REAL; g_ManifoldP_kPa : REAL;
    g_PumpP_kPa : ARRAY[1..3] OF REAL;
    g_Vib_mms : ARRAY[1..3] OF REAL;
    g_LSHH : BOOL; g_LSLL_Wet : BOOL; g_SpillDetected : BOOL;
    g_ThermalOK : ARRAY[1..3] OF BOOL;
    g_SealLeak : ARRAY[1..3] OF BOOL;
    g_MainsOK : BOOL;

    g_cmd_Mode : INT; g_cmd_Word : INT; g_cmd_Param : INT;
    g_sp_Level : INT; g_sp_StartDuty : INT; g_sp_StartP2 : INT;
    g_sp_StartP3 : INT; g_sp_StopAll : INT; g_sp_HighAlarm : INT;
    g_sp_MinSpeed : INT; g_sp_ServiceHrs : INT;

    g_o_RunCmd : ARRAY[1..3] OF BOOL;
    g_o_Running : ARRAY[1..3] OF BOOL;
    g_o_Available : ARRAY[1..3] OF BOOL;
    g_o_Tripped : ARRAY[1..3] OF BOOL;
    g_o_InAuto : BOOL; g_o_HighLevel : BOOL; g_o_SpillActive : BOOL;
    g_o_Level_mm : INT; g_o_Inflow_x10 : INT; g_o_Disch_x10 : INT;
    g_o_PumpsRun : INT; g_o_Speed_x10 : INT;
    g_o_TimeToSpill : INT; g_o_TimeToLSHH : INT; g_o_NetAccum : INT;
    g_o_RunHours : ARRAY[1..3] OF INT;
    g_o_VolToSpill : INT; g_o_StationState : INT;
    g_o_PumpState : ARRAY[1..3] OF INT;
    g_o_DutyPump : INT; g_o_AlarmWord : INT; g_o_CmdAck : INT;

    (* defaults *)
    DEF_MODE : INT; DEF_SP_LEVEL : INT; DEF_START_DUTY : INT;
    DEF_START_P2 : INT; DEF_START_P3 : INT; DEF_STOP_ALL : INT;
    DEF_HIGH_ALARM : INT; DEF_MIN_SPEED : INT; DEF_SERVICE_HRS : INT;

    (* simulation image and control, section 8 *)
    g_SimActive : BOOL; g_sim_ClearReset : BOOL;
    g_simcmd_Inflow : INT; g_simcmd_Mode : INT;
    g_simcmd_Reset : INT; g_simcmd_TimeScale : INT;
    g_sim_Level_mm : INT; g_sim_Inflow_x10 : INT; g_sim_Disch_x10 : INT;
    g_sim_ManifoldP : INT;
    g_sim_PumpP : ARRAY[1..3] OF INT;
    g_sim_Vib_x10 : ARRAY[1..3] OF INT;
    g_sim_LSHH : BOOL; g_sim_LSLL_Wet : BOOL; g_sim_Spill : BOOL;
    g_sim_ThermalOK : ARRAY[1..3] OF BOOL;
    g_sim_SealLeak : ARRAY[1..3] OF BOOL;
    g_sim_MainsOK : BOOL;
END_VAR

VAR
    Seeded : BOOL := FALSE;
END_VAR

(* ---------------------------------------------------------------------
   Seed the %MW setpoint defaults once, at first scan.

   Section 9 forbids relying on retained variables, so after a runtime
   restart every %MW reads 0.  Writing the defaults once here means
   CI Server sees real values rather than zeros, and CONTROL's
   validation does not reject an all-zero image on every scan.

   This is a one-shot write, not a per-scan overwrite: everything
   CI Server writes afterwards survives, per section 2.
   --------------------------------------------------------------------- *)
IF NOT Seeded THEN
    Seeded        := TRUE;
    MW_Mode       := DEF_MODE;
    MW_CmdWord    := 0;
    MW_CmdParam   := 0;
    MW_SpLevel    := DEF_SP_LEVEL;
    MW_StartDuty  := DEF_START_DUTY;
    MW_StartP2    := DEF_START_P2;
    MW_StartP3    := DEF_START_P3;
    MW_StopAll    := DEF_STOP_ALL;
    MW_HighAlarm  := DEF_HIGH_ALARM;
    MW_MinSpeed   := DEF_MIN_SPEED;
    MW_ServiceHrs := DEF_SERVICE_HRS;

    (* Simulation defaults, section 8.2: manual inflow at the dry
       weather average, time scale 1.  Unused in the field build. *)
    MW_SimInflow    := 750;
    MW_SimMode      := 0;
    MW_SimReset     := 0;
    MW_SimTimeScale := 1;
END_IF;


(* ---------------------------------------------------------------------
   Publish the previous scan's results to %QW / %QX
   --------------------------------------------------------------------- *)
QX_RunCmd1     := g_o_RunCmd[1];
QX_RunCmd2     := g_o_RunCmd[2];
QX_RunCmd3     := g_o_RunCmd[3];
QX_Running1    := g_o_Running[1];
QX_Running2    := g_o_Running[2];
QX_Running3    := g_o_Running[3];
QX_Avail1      := g_o_Available[1];
QX_Avail2      := g_o_Available[2];
QX_Avail3      := g_o_Available[3];
QX_InAuto      := g_o_InAuto;
QX_HighLevel   := g_o_HighLevel;
QX_SpillActive := g_o_SpillActive;
QX_Tripped1    := g_o_Tripped[1];
QX_Tripped2    := g_o_Tripped[2];
QX_Tripped3    := g_o_Tripped[3];

QW_Level        := g_o_Level_mm;
QW_Inflow       := g_o_Inflow_x10;
QW_Discharge    := g_o_Disch_x10;
QW_PumpsRunning := g_o_PumpsRun;
QW_Speed        := g_o_Speed_x10;
QW_TimeToSpill  := g_o_TimeToSpill;
QW_TimeToLSHH   := g_o_TimeToLSHH;
QW_NetAccum     := g_o_NetAccum;
QW_RunHours1    := g_o_RunHours[1];
QW_RunHours2    := g_o_RunHours[2];
QW_RunHours3    := g_o_RunHours[3];
QW_VolToSpill   := g_o_VolToSpill;
QW_StationState := g_o_StationState;
QW_PumpState1   := g_o_PumpState[1];
QW_PumpState2   := g_o_PumpState[2];
QW_PumpState3   := g_o_PumpState[3];
QW_DutyPump     := g_o_DutyPump;
QW_AlarmWord    := g_o_AlarmWord;
QW_CmdAck       := g_o_CmdAck;


(* ---------------------------------------------------------------------
   Commands and setpoints in
   --------------------------------------------------------------------- *)
g_cmd_Mode      := MW_Mode;
g_cmd_Word      := MW_CmdWord;
g_cmd_Param     := MW_CmdParam;
g_sp_Level      := MW_SpLevel;
g_sp_StartDuty  := MW_StartDuty;
g_sp_StartP2    := MW_StartP2;
g_sp_StartP3    := MW_StartP3;
g_sp_StopAll    := MW_StopAll;
g_sp_HighAlarm  := MW_HighAlarm;
g_sp_MinSpeed   := MW_MinSpeed;
g_sp_ServiceHrs := MW_ServiceHrs;


(* ---------------------------------------------------------------------
   Simulation control out, and the reset acknowledgement.

   SIMULATION may not touch located variables, so clearing %MW22 after
   a reset happens here.
   --------------------------------------------------------------------- *)
g_simcmd_Inflow    := MW_SimInflow;
g_simcmd_Mode      := MW_SimMode;
g_simcmd_Reset     := MW_SimReset;
g_simcmd_TimeScale := MW_SimTimeScale;

IF g_sim_ClearReset THEN
    MW_SimReset := 0;
END_IF;


(* ---------------------------------------------------------------------
   THE MUX, section 8.1.

   Field build:      g_SimActive is FALSE (SIMULATION is not compiled
                     in and nothing ever sets it), so the located
                     inputs are used.
   Simulation build: SIMULATION sets it TRUE every scan and the
                     process image comes from g_sim_* instead.

   CONTROL sees identical globals either way and cannot tell which
   source it is running on - that is the point of section 8.1.
   --------------------------------------------------------------------- *)
IF g_SimActive THEN

    g_LevelRaw_mm   := g_sim_Level_mm;
    g_Level_mm      := g_sim_Level_mm;
    g_Level_m       := INT_TO_REAL(g_sim_Level_mm) / 1000.0;
    g_Inflow_Lps    := INT_TO_REAL(g_sim_Inflow_x10) / 10.0;
    g_Disch_Lps     := INT_TO_REAL(g_sim_Disch_x10) / 10.0;
    g_ManifoldP_kPa := INT_TO_REAL(g_sim_ManifoldP);

    g_PumpP_kPa[1]  := INT_TO_REAL(g_sim_PumpP[1]);
    g_PumpP_kPa[2]  := INT_TO_REAL(g_sim_PumpP[2]);
    g_PumpP_kPa[3]  := INT_TO_REAL(g_sim_PumpP[3]);

    g_Vib_mms[1]    := INT_TO_REAL(g_sim_Vib_x10[1]) / 10.0;
    g_Vib_mms[2]    := INT_TO_REAL(g_sim_Vib_x10[2]) / 10.0;
    g_Vib_mms[3]    := INT_TO_REAL(g_sim_Vib_x10[3]) / 10.0;

    g_LSHH          := g_sim_LSHH;
    g_LSLL_Wet      := g_sim_LSLL_Wet;
    g_SpillDetected := g_sim_Spill;

    g_ThermalOK[1]  := g_sim_ThermalOK[1];
    g_ThermalOK[2]  := g_sim_ThermalOK[2];
    g_ThermalOK[3]  := g_sim_ThermalOK[3];

    g_SealLeak[1]   := g_sim_SealLeak[1];
    g_SealLeak[2]   := g_sim_SealLeak[2];
    g_SealLeak[3]   := g_sim_SealLeak[3];

    g_MainsOK       := g_sim_MainsOK;

ELSE

(* ---------------------------------------------------------------------
   FIELD SOURCE - analogue inputs, scaled to engineering units
   --------------------------------------------------------------------- *)
g_LevelRaw_mm   := IW_LIT101;
g_Level_mm      := IW_LIT101;
g_Level_m       := INT_TO_REAL(IW_LIT101) / 1000.0;
g_Inflow_Lps    := INT_TO_REAL(IW_FIT201) / 10.0;
g_Disch_Lps     := INT_TO_REAL(IW_FIT301) / 10.0;
g_ManifoldP_kPa := INT_TO_REAL(IW_PIT302);

g_PumpP_kPa[1]  := INT_TO_REAL(IW_PIT311);
g_PumpP_kPa[2]  := INT_TO_REAL(IW_PIT321);
g_PumpP_kPa[3]  := INT_TO_REAL(IW_PIT331);

g_Vib_mms[1]    := INT_TO_REAL(IW_VE314) / 10.0;
g_Vib_mms[2]    := INT_TO_REAL(IW_VE324) / 10.0;
g_Vib_mms[3]    := INT_TO_REAL(IW_VE334) / 10.0;

(* --- discrete inputs.  Sense conventions are section 2.1's, and are
       applied here so that CONTROL never has to know them. --------- *)
g_LSHH          := IX_LSHH102;      (* TRUE = wet                      *)
g_LSLL_Wet      := IX_LSLL103;      (* TRUE = wet; FALSE = dry, so a
                                       broken wire stops the station   *)
g_SpillDetected := IX_LSH104;

g_ThermalOK[1]  := IX_TE312;        (* TRUE = healthy                  *)
g_ThermalOK[2]  := IX_TE322;
g_ThermalOK[3]  := IX_TE332;

g_SealLeak[1]   := IX_MSE313;       (* TRUE = leak                     *)
g_SealLeak[2]   := IX_MSE323;
g_SealLeak[3]   := IX_MSE333;

g_MainsOK       := IX_XA502;        (* TRUE = healthy                  *)

END_IF;

END_PROGRAM
