wrps-demo-kit/03-plc/src/23_fb_headroom.st
Clio Liu 13a05d0135 feat(plc): ST sources, generators and the Modbus contract
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.
2026-09-02 15:48:49 +10:00

97 lines
3.1 KiB
Smalltalk

(* =====================================================================
23_fb_headroom.st - FB_HEADROOM, section 4.4
Pure calculation. No control action, no alarms.
Reference condition, WRPS-PRO-001 7.3 and pass 1 test 14:
level 4.00 m, inflow 165 L/s, one pump at 120 L/s
NetInflow = 45.0 L/s -> %QW7 = 450
VolToSpill = (6.0 - 4.0) * 120 = 240 m3
TimeToSpill = 240 * 1000 / 45 = 5333.3 s -> %QW5 = 5333
===================================================================== *)
FUNCTION_BLOCK FB_HEADROOM
VAR_INPUT
Level : REAL; (* m *)
Inflow : REAL; (* L/s, unfiltered *)
TotalDischarge : REAL; (* L/s *)
END_VAR
VAR_OUTPUT
InflowFilt : REAL; (* L/s, 30 s lag *)
NetInflow : REAL; (* L/s *)
VolToSpill : REAL; (* m3 *)
VolToLSHH : REAL; (* m3 *)
TimeToSpill : INT; (* s, 32767 = drawing down *)
TimeToLSHH : INT; (* s, 32767 = drawing down *)
END_VAR
VAR CONSTANT
SCAN_S : REAL := 0.1; (* must match TASK INTERVAL *)
TAU_S : REAL := 30.0; (* inflow filter time constant *)
AREA_M2 : REAL := 120.0;
SPILL_M : REAL := 6.000;
LSHH_M : REAL := 5.500;
MIN_NET : REAL := 0.5; (* L/s, below this the figure is
meaningless - drawing down *)
NO_TIME : INT := 32767;
MAX_TIME : REAL := 32767.0;
END_VAR
VAR
Primed : BOOL := FALSE;
t : REAL;
END_VAR
(* --- Inflow filter. First-order lag, 30 s.
Primed on the first scan rather than ramping from zero: without
this the reference figure in test 14 would take ~2 minutes to
settle and would not reproduce exactly on demand. ---------------- *)
IF NOT Primed THEN
InflowFilt := Inflow;
Primed := TRUE;
ELSE
InflowFilt := InflowFilt + (Inflow - InflowFilt) * SCAN_S / TAU_S;
END_IF;
NetInflow := InflowFilt - TotalDischarge;
VolToSpill := (SPILL_M - Level) * AREA_M2;
VolToLSHH := (LSHH_M - Level) * AREA_M2;
IF VolToSpill < 0.0 THEN
VolToSpill := 0.0;
END_IF;
IF VolToLSHH < 0.0 THEN
VolToLSHH := 0.0;
END_IF;
(* --- Time to spill weir ------------------------------------------- *)
IF NetInflow <= MIN_NET THEN
TimeToSpill := NO_TIME;
ELSE
t := VolToSpill * 1000.0 / NetInflow;
IF t >= MAX_TIME THEN
TimeToSpill := NO_TIME;
ELSIF t < 0.0 THEN
TimeToSpill := 0;
ELSE
TimeToSpill := REAL_TO_INT(t);
END_IF;
END_IF;
(* --- Time to LSHH -------------------------------------------------- *)
IF NetInflow <= MIN_NET THEN
TimeToLSHH := NO_TIME;
ELSE
t := VolToLSHH * 1000.0 / NetInflow;
IF t >= MAX_TIME THEN
TimeToLSHH := NO_TIME;
ELSIF t < 0.0 THEN
TimeToLSHH := 0;
ELSE
TimeToLSHH := REAL_TO_INT(t);
END_IF;
END_IF;
END_FUNCTION_BLOCK