(* ===================================================================== 21_fb_duty_selector.st - FB_DUTY_SELECT, section 4.2 Decides WHICH units run, never HOW MANY. Ranking, best first: 1. unavailable units excluded entirely 2. not-due-for-service ranks above due-for-service 3. within a group, lower run hours first 4. ties break by ascending pump number (deterministic for demos) The two rules that outrank the ranking: - never stop a running unit to start a better-ranked one - service-due is a preference, never a veto ===================================================================== *) FUNCTION_BLOCK FB_DUTY_SELECT VAR_INPUT Available : ARRAY[1..3] OF BOOL; RunHours : ARRAY[1..3] OF REAL; ServiceDue : ARRAY[1..3] OF BOOL; RunningNow : ARRAY[1..3] OF BOOL; PumpsRequired : INT; END_VAR VAR_OUTPUT RunRequest : ARRAY[1..3] OF BOOL; DutyPump : INT; (* 0 = none, 1-3 *) END_VAR VAR CONSTANT (* Service-due penalty. Larger than any credible run-hours value, so a due unit always ranks below every not-due unit while still remaining in the list - rule 2 is a preference, not a veto. *) SERVICE_PENALTY : REAL := 1000000.0; END_VAR VAR Rank : ARRAY[1..3] OF INT; (* pump numbers, best first *) Used : ARRAY[1..3] OF BOOL; Sel : ARRAY[1..3] OF BOOL; i : INT; k : INT; best : INT; bestKey : REAL; key : REAL; nRanked : INT; slots : INT; cnt : INT; END_VAR (* --- 1. Reset working state --------------------------------------- *) FOR i := 1 TO 3 DO Used[i] := FALSE; Sel[i] := FALSE; Rank[i] := 0; END_FOR; nRanked := 0; (* --- 2. Build the ranked list by repeated selection. Scanning i ascending with a strict "<" test means an equal key never displaces an earlier pump, which is rule 4. ------------ *) FOR k := 1 TO 3 DO best := 0; bestKey := 0.0; FOR i := 1 TO 3 DO IF Available[i] AND NOT Used[i] THEN key := RunHours[i]; IF ServiceDue[i] THEN key := key + SERVICE_PENALTY; END_IF; IF (best = 0) OR (key < bestKey) THEN best := i; bestKey := key; END_IF; END_IF; END_FOR; IF best > 0 THEN nRanked := nRanked + 1; Rank[nRanked] := best; Used[best] := TRUE; END_IF; END_FOR; (* --- 3. Clamp the demand ------------------------------------------ *) slots := PumpsRequired; IF slots < 0 THEN slots := 0; END_IF; IF slots > 3 THEN slots := 3; END_IF; (* --- 4. Pass A: units already running keep their slots. Walking in rank order means that if the demand has dropped it is the worst-ranked running unit that loses its slot. -------- *) cnt := 0; FOR k := 1 TO nRanked DO i := Rank[k]; IF RunningNow[i] AND (cnt < slots) THEN Sel[i] := TRUE; cnt := cnt + 1; END_IF; END_FOR; (* --- 5. Pass B: fill the slots that remain, by rank --------------- *) FOR k := 1 TO nRanked DO i := Rank[k]; IF (NOT Sel[i]) AND (cnt < slots) THEN Sel[i] := TRUE; cnt := cnt + 1; END_IF; END_FOR; (* --- 6. Publish. DutyPump is the best-ranked selected unit. ------- *) DutyPump := 0; FOR k := 1 TO nRanked DO i := Rank[k]; IF Sel[i] AND (DutyPump = 0) THEN DutyPump := i; END_IF; END_FOR; FOR i := 1 TO 3 DO RunRequest[i] := Sel[i]; END_FOR; END_FUNCTION_BLOCK