Foundation
Understand assets, instrumentation, I/O, PLCs, RTUs, and control narratives.
- Equipment list
- I/O list
- Control narrative
A standalone learning page that visualizes SCADA as layers of development: physical assets, instrumentation, control logic, communications, historian data, applications, cybersecurity, and water operations.
Each layer depends on the layer below it. If the lower layers are weak, everything above becomes unreliable, even if the interface looks modern.
This is the actual water system. If this layer is misunderstood, the rest of the SCADA design becomes fantasy.
A pump station pushes water into a pressure zone. A ground storage tank level controls pump start/stop logic. A chlorine analyzer confirms residual after chemical dosing.
This is the development sequence. Skipping to AI or dashboards before getting tags, quality, alarms, and security right is immature engineering.
Understand assets, instrumentation, I/O, PLCs, RTUs, and control narratives.
Connect field sites through reliable, segmented, and monitored communications.
Build clean HMI screens, alarm priorities, historian tags, and trends.
Improve data quality, redundancy, cybersecurity, auditing, and operating procedures.
Use historian trends, alarms, and operations context for smarter decisions.
The control system is not just a screen. It is a chain from field equipment to control logic to network to operator action.
This simplified structured text shows how a pump starts and stops using tank level, permissives, and a fail-to-start alarm.
(* Pump starts when tank is low and all permissives are true *)
Pump1_Permissive :=
Auto_Mode
AND NOT EStop_Active
AND NOT Low_Suction_Pressure
AND NOT Motor_Overload
AND NOT Pump1_Fault;
IF Tank_Level_FT <= Low_Start_Level_FT AND Pump1_Permissive THEN
Pump1_Start_Cmd := TRUE;
END_IF;
IF Tank_Level_FT >= High_Stop_Level_FT OR NOT Pump1_Permissive THEN
Pump1_Start_Cmd := FALSE;
END_IF;
IF Pump1_Start_Cmd AND NOT Pump1_Run_FB THEN
TON_FailStart(IN := TRUE, PT := T#10S);
ELSE
TON_FailStart(IN := FALSE);
END_IF;
Pump1_Fail_To_Start_Alarm := TON_FailStart.Q;
Protocols are the languages used between devices and systems. Pick based on purpose, not trendiness.
| Protocol | Use It For | Water Example | Do Not Ignore |
|---|---|---|---|
| Modbus | Simple field device reads/writes | Read flow meter registers into PLC | Register map, scaling, security |
| DNP3 | Remote utility telemetry | Remote tank RTU sends level event to SCADA | Time sync, event classes, polling strategy |
| OPC UA | Structured integration | SCADA publishes pressure tags to historian | Certificates, namespace design, access control |
| MQTT Sparkplug | Modern distributed telemetry | Remote site publishes pump status to broker | Topic discipline, birth/death messages, QoS |
| REST API | Apps, reporting, non-control integration | Dashboard queries daily pump runtime | Never use API as direct PLC control path |
Historian data should capture not only value, but trust. Tag, timestamp, value, and quality are the minimum foundation.
CREATE TABLE scada_tag (
tag_id BIGSERIAL PRIMARY KEY,
tag_name TEXT UNIQUE NOT NULL,
site_id TEXT NOT NULL,
equipment_id TEXT,
data_type TEXT NOT NULL,
unit TEXT,
description TEXT,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE historian_value (
tag_id BIGINT REFERENCES scada_tag(tag_id),
ts TIMESTAMPTZ NOT NULL,
value_num DOUBLE PRECISION,
value_text TEXT,
quality_code TEXT NOT NULL DEFAULT 'GOOD',
source TEXT,
PRIMARY KEY (tag_id, ts)
);
SELECT
date_trunc('hour', hv.ts) AS hour_bucket,
AVG(hv.value_num) AS avg_pressure_psi,
MIN(hv.value_num) AS min_pressure_psi,
MAX(hv.value_num) AS max_pressure_psi
FROM historian_value hv
JOIN scada_tag t ON t.tag_id = hv.tag_id
WHERE t.tag_name = 'PS101_DISCHARGE_PRESSURE_PSI'
AND hv.quality_code = 'GOOD'
AND hv.ts > NOW() - INTERVAL '24 hours'
GROUP BY hour_bucket
ORDER BY hour_bucket;
Cybersecurity is a cross-cutting layer. It protects every layer underneath it, especially the PLC/RTU control layer.
# Bad rule
ALLOW ANY FROM IT_NETWORK TO PLC_NETWORK
# Better rule concept
ALLOW TCP 443 FROM VPN_USERS TO JUMP_SERVER
ALLOW RDP FROM JUMP_SERVER TO ENGINEERING_WORKSTATION
ALLOW OPCUA 4840 FROM SCADA_SERVER TO OPCUA_SERVER
DENY ANY FROM INTERNET TO PLC_NETWORK
DENY ANY FROM IT_NETWORK TO PLC_NETWORK
LOG ALL DENIED TRAFFIC
CREATE TABLE scada_audit_log (
audit_id BIGSERIAL PRIMARY KEY,
event_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
username TEXT NOT NULL,
source_ip INET,
action TEXT NOT NULL,
asset TEXT,
old_value TEXT,
new_value TEXT,
result TEXT NOT NULL,
notes TEXT
);
Every tag and alarm should connect back to a physical operating consequence: pressure, storage, flow, chemical residual, equipment health, or customer impact.
Use this glossary as the vocabulary layer. Search by term or filter by category.
A facility that uses pumps to move water or maintain pressure.
Example: Booster pump station increases pressure for a high-elevation zone.
A water storage asset used for pressure stability, demand balancing, and emergency reserve.
Example: SCADA starts pumps when tank level falls below 14 ft.
Variable Frequency Drive. Controls motor speed instead of simply on/off operation.
Example: VFD adjusts pump speed to maintain 65 PSI.
Pressure Reducing Valve. Controls downstream pressure in a pressure zone.
Example: PRV holds downstream pressure near 55 PSI.
Programmable Logic Controller. The local controller that runs equipment logic.
Example: PLC starts Pump 1 if tank level is low and permissives are true.
Remote Terminal Unit. Field controller designed for remote telemetry and monitoring.
Example: Remote tank RTU reports tank level over cellular network.
Digital Input. A discrete signal read by a PLC, usually on/off.
Example: Pump run feedback contact is ON or OFF.
Digital Output. A PLC output used to command a discrete action.
Example: PLC energizes a relay to start Pump 1.
Analog Input. A continuous signal such as 4-20 mA representing a measurement.
Example: Tank level transmitter sends 4-20 mA to PLC.
Analog Output. A continuous command signal from PLC to equipment.
Example: PLC sends speed command to VFD.
A condition that must be true before equipment is allowed to operate.
Example: Pump can start only if suction pressure is adequate.
A protective logic condition that prevents or stops unsafe operation.
Example: Stop pump on motor overload or emergency stop.
A pump sequencing strategy where one pump leads and another supports or alternates.
Example: Pump 1 starts first, Pump 2 starts if pressure keeps dropping.
A target or threshold used by control logic.
Example: Start pump at 14 ft tank level and stop at 22 ft.
A buffer zone that prevents rapid on/off switching or alarm chattering.
Example: Start pump at 14 ft, but do not stop until 22 ft.
A simple protocol for reading and writing device registers.
Example: PLC reads flow meter holding register 40001.
Utility telemetry protocol designed for remote sites and event reporting.
Example: RTU sends timestamped high tank level event to SCADA.
A structured industrial data exchange standard used for system integration.
Example: Historian reads pressure tags from OPC UA server.
Industrial publish/subscribe messaging with structured device state.
Example: Remote pump station publishes telemetry to a broker.
A named data point in SCADA or historian.
Example: PS101_DISCHARGE_PRESSURE_PSI.
A flag indicating whether a value should be trusted.
Example: Tank level = 18.4 ft with BAD quality should not be trusted.
A time-series database for SCADA values, trends, events, and alarms.
Example: Review pressure trend during a main break.
A record of abnormal condition, acknowledgement, and return-to-normal.
Example: Low pressure alarm active at 2:15 AM, acknowledged at 2:18 AM.
Segmentation between operational technology and business IT systems.
Example: Office laptops cannot directly access PLC network.
A controlled network zone between IT and OT networks.
Example: Reporting server sits in DMZ, not directly inside PLC network.
A controlled server used as the approved entry point into a protected network.
Example: Vendor must access engineering workstation through jump server.
Multi-factor authentication. Requires more than a password.
Example: VPN login requires password plus authenticator approval.
A written approach defining alarm priority, response, suppression, and escalation.
Example: Low chlorine residual is critical, meter maintenance notice is low.
Standard Operating Procedure. Defines what operators should do for known scenarios.
Example: Low pressure SOP checks pump status, tank level, flow spike, and field confirmation.
A condition where the system is outside normal operation and needs active attention.
Example: Sudden tank drop, low pressure, high flow, and pump failure at same time.
These are practical teaching examples. They show structure and thinking. Production SCADA code must be reviewed by qualified controls, cybersecurity, and operations staff.
{
"site_id": "PS101",
"site_type": "booster_pump_station",
"equipment": [
{"id": "PUMP_1", "type": "pump", "motor_hp": 150, "vfd": true},
{"id": "PUMP_2", "type": "pump", "motor_hp": 150, "vfd": true},
{"id": "FLOW_METER_1", "type": "mag_meter", "unit": "MGD"},
{"id": "PRESSURE_TX_1", "type": "pressure_transmitter", "unit": "PSI"}
]
}
point_name,io_type,signal,unit,equipment,description
PUMP1_RUN_FB,DI,24VDC,,PUMP_1,Pump 1 run feedback
PUMP1_START_CMD,DO,24VDC,,PUMP_1,Pump 1 start command
DISCH_PRESSURE,AI,4-20mA,PSI,HEADER,Discharge header pressure
PUMP1_SPEED_CMD,AO,4-20mA,%,PUMP_1,VFD speed command
(* Simplified lead/lag pressure control *)
LowPressure := Discharge_Pressure_PSI < Low_Pressure_SP;
VeryLowPressure := Discharge_Pressure_PSI < Very_Low_Pressure_SP;
IF Auto_Mode AND LowPressure THEN
LeadPump_Start := TRUE;
END_IF;
IF Auto_Mode AND VeryLowPressure THEN
LagPump_Start := TRUE;
END_IF;
IF Discharge_Pressure_PSI > Stop_Pressure_SP THEN
LeadPump_Start := FALSE;
LagPump_Start := FALSE;
END_IF;
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient("10.10.20.15", port=502, timeout=3)
client.connect()
result = client.read_holding_registers(address=0, count=2, slave=1)
if result.isError():
raise RuntimeError("Modbus read failed")
flow_mgd = result.registers[0] / 100.0
pressure_psi = result.registers[1] / 10.0
print({"flow_mgd": flow_mgd, "pressure_psi": pressure_psi})
client.close()
import json, time
import paho.mqtt.client as mqtt
client = mqtt.Client(client_id="ps101_rtu")
client.connect("10.10.50.20", 1883, 60)
payload = {
"timestamp": int(time.time() * 1000),
"metrics": [
{"name": "Pump1_Run", "value": True, "type": "Boolean"},
{"name": "Discharge_Pressure_PSI", "value": 64.2, "type": "Float"},
{"name": "Flow_MGD", "value": 3.18, "type": "Float"}
]
}
client.publish("spBv1.0/water/DDATA/PS101/RTU01", json.dumps(payload), qos=1)
client.disconnect()
from opcua import Client
client = Client("opc.tcp://10.10.30.12:4840")
client.connect()
try:
pressure_node = client.get_node("ns=2;s=PS101.DischargePressurePSI")
pressure_psi = pressure_node.get_value()
print("Pressure:", pressure_psi)
finally:
client.disconnect()
{
"tag_name": "PS101_DISCHARGE_PRESSURE_PSI",
"site_id": "PS101",
"equipment_id": "DISCHARGE_HEADER",
"data_type": "float",
"unit": "psi",
"source_protocol": "OPC_UA",
"quality_code": "GOOD",
"alarm_limits": {
"low_low": 35,
"low": 45,
"high": 95,
"high_high": 110
}
}
CREATE TABLE scada_tag (
tag_id BIGSERIAL PRIMARY KEY,
tag_name TEXT UNIQUE NOT NULL,
site_id TEXT NOT NULL,
equipment_id TEXT,
unit TEXT,
description TEXT
);
CREATE TABLE historian_value (
tag_id BIGINT REFERENCES scada_tag(tag_id),
ts TIMESTAMPTZ NOT NULL,
value_num DOUBLE PRECISION,
value_text TEXT,
quality_code TEXT NOT NULL,
source TEXT,
PRIMARY KEY (tag_id, ts)
);
<div class="status-card normal">
<div class="label">Pump 1</div>
<div class="value">RUNNING</div>
<div class="meta">64.2 PSI · 3.18 MGD · Quality: GOOD</div>
</div>
<style>
.status-card { padding: 14px; border-radius: 14px; border: 1px solid #ddd; }
.status-card.normal { background: #f0fdf4; color: #166534; }
.status-card.alarm { background: #fef2f2; color: #991b1b; }
.label { font-size: 12px; opacity: .75; }
.value { font-size: 22px; font-weight: 800; }
.meta { font-size: 12px; margin-top: 4px; }
</style>
def evaluate_pressure_alarm(pressure_psi: float, quality: str) -> dict:
if quality != "GOOD":
return {"active": True, "priority": "HIGH", "alarm": "PRESSURE_DATA_BAD"}
if pressure_psi < 35:
return {"active": True, "priority": "CRITICAL", "alarm": "LOW_PRESSURE_CRITICAL"}
if pressure_psi < 45:
return {"active": True, "priority": "HIGH", "alarm": "LOW_PRESSURE_WARNING"}
return {"active": False}
INSERT INTO scada_audit_log
(username, source_ip, action, asset, old_value, new_value, result)
VALUES
('operator01', '10.20.5.44', 'SETPOINT_CHANGE',
'GST202_LOW_LEVEL_SP', '14.0', '13.5', 'SUCCESS');
{
"event_type": "LOW_PRESSURE",
"first_questions": [
"Which pressure zone is affected?",
"Are pumps running and available?",
"Are tanks dropping?",
"Is there abnormal high flow?",
"Is the data quality good or stale?"
],
"operator_actions": [
"Verify SCADA alarm and historian trend",
"Check pump station and tank status",
"Contact field crew for confirmation",
"Escalate if public health or service risk exists"
]
}