The PLC program from the old repo's 04-plc/, flattened into one folder and
checked against the running system.
Verified during the move:
- build.py regenerates register-map.csv byte-identically (69 points)
- polled the live PLC: the SIMULATION build is what is deployed and
running, %MW21=2 wet weather, values moving, run hours accumulating
- addresses, %MW HR1024 segmentation and %QW17/%QW7 signedness all
match the map
Corrections against the old repo:
- 10_globals.st header cited WRPS-CTL-002 (the FDS); it means CTL-003
- build.py wrote the map to its parent directory; now beside itself
- deploy/README.md was a single-file folder; now DEPLOY.md
- dropped the empty editor-devices/remote/
- README no longer claims the simulation build is uncompiled - it is
the one running
Two open items are now stated plainly rather than buried:
- none of the 20 acceptance tests in CTL-003 have ever been run
- the OpenPLC Editor lived only on the retired dev-ubuntu host, so
there is currently NO route to deploy a new program (DEPLOY.md 0)
Documents the setpoint distinction: IO_MUX seeds %MW defaults once at
first scan, operators retune them live, and that tuning exists only in
the container volume - a restart reverts it.
128 lines
3.5 KiB
Smalltalk
128 lines
3.5 KiB
Smalltalk
(* =====================================================================
|
|
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
|