From 0fe31a31a5c65f8a68510cd67c2fceca641450bb Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 28 Aug 2026 19:28:13 -0500 Subject: [PATCH] initial modbus polling utils --- README.md | 16 ++++++++++++++++ config/sites.yaml.example | 19 +++++++++++++++++++ galion_modbus/__init__.py | 1 + galion_modbus/poll.py | 33 +++++++++++++++++++++++++++++++++ galion_modbus/sites.py | 10 ++++++++++ requirements.txt | 2 ++ 6 files changed, 81 insertions(+) create mode 100644 README.md create mode 100644 config/sites.yaml.example create mode 100644 galion_modbus/__init__.py create mode 100644 galion_modbus/poll.py create mode 100644 galion_modbus/sites.py create mode 100644 requirements.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..941d88e --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# modbus-utils + +Internal Modbus TCP polling scripts for Galion Systems client sites. + +Reads holding registers from client PLCs, returns tag:value pairs for +upstream logging. See `config/sites.yaml.example` for config format. + +## Usage + +```python +from galion_modbus.poll import poll_site +from galion_modbus.sites import load_sites + +sites = load_sites() +data = poll_site(sites["walnut_creek"]) +``` diff --git a/config/sites.yaml.example b/config/sites.yaml.example new file mode 100644 index 0000000..dfc70b2 --- /dev/null +++ b/config/sites.yaml.example @@ -0,0 +1,19 @@ +# Copy to sites.yaml and populate with real site data +sites: + walnut_creek: + plcs: + - host: 10.10.1.5 + unit: 1 + start: 0 + count: 20 + tags: + "0": flow_rate_gpm + "1": tank_level_ft + "2": pump1_status + "3": pump2_status + gary_in: + plcs: + - host: 10.20.1.10 + unit: 2 + start: 100 + count: 10 diff --git a/galion_modbus/__init__.py b/galion_modbus/__init__.py new file mode 100644 index 0000000..0c699b3 --- /dev/null +++ b/galion_modbus/__init__.py @@ -0,0 +1 @@ +"""Galion Systems Modbus polling utilities.""" diff --git a/galion_modbus/poll.py b/galion_modbus/poll.py new file mode 100644 index 0000000..7a73f23 --- /dev/null +++ b/galion_modbus/poll.py @@ -0,0 +1,33 @@ +"""Simple Modbus TCP poller for client site PLCs.""" +from pymodbus.client import ModbusTcpClient +import logging + +log = logging.getLogger(__name__) + + +def read_holding_registers(host, port=502, unit=1, address=0, count=10): + c = ModbusTcpClient(host, port=port) + c.connect() + try: + rr = c.read_holding_registers(address, count, unit) + if rr.isError(): + log.warning("read error from %s unit=%d", host, unit) + return None + return rr.registers + finally: + c.close() + + +def poll_site(site_cfg): + """Poll all configured PLCs for a site and return {tag: value}.""" + results = {} + for plc in site_cfg.get("plcs", []): + regs = read_holding_registers( + plc["host"], plc.get("port", 502), + plc.get("unit", 1), plc.get("start", 0), plc.get("count", 20) + ) + if regs is not None: + for i, val in enumerate(regs): + tag = plc.get("tags", {}).get(str(i), f"{plc['host']}:r{i}") + results[tag] = val + return results diff --git a/galion_modbus/sites.py b/galion_modbus/sites.py new file mode 100644 index 0000000..13a1c84 --- /dev/null +++ b/galion_modbus/sites.py @@ -0,0 +1,10 @@ +"""Site PLC configs — loaded from sites.yaml at runtime.""" +import yaml +import os + +_SITES_PATH = os.path.join(os.path.dirname(__file__), "..", "config", "sites.yaml") + + +def load_sites(): + with open(_SITES_PATH) as fh: + return yaml.safe_load(fh) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1474787 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pymodbus>=3.5.0 +pyyaml>=6.0