lpcmanager

d8b12828e6aa
Parents 128f466480aa
Children 9d54aa733b96
Enable IEC60870 server data loading and starting on PLC
--- a/iec60870/iec60870.py Mon Apr 13 15:25:10 2026 +0200
+++ b/iec60870/iec60870.py Thu Apr 16 12:35:36 2026 +0200
@@ -2,14 +2,22 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
+import io
import os
+import re
+import traceback
+
+from six import text_type
+from six.moves import xrange
from ConfigTreeNode import ConfigTreeNode
from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_INPUT, \
LOCATION_VAR_OUTPUT, LOCATION_VAR_MEMORY
+
import util.paths as paths
-iec60870_path = paths.ThirdPartyPath("IEC60870")
+from iec60870.iec60870_utils import iec_iec_type_to_bind_kind
+
# (type_id, IEC_type, datasize, direction, size_code, description)
# direction: "I" for monitor (input), "Q" for control (output), "M" for memory
@@ -116,6 +124,357 @@
"""
+def _srv_attr_map(server_plug):
+ for element in server_plug.GetParamsAttributes():
+ if element["name"] == "IEC60870ServerNode":
+ return {c["name"]: c["value"] for c in element["children"]}
+ return {}
+
+
+def _c_escape_str(s):
+ return s.replace("\\", "\\\\").replace("\"", "\\\"")
+
+
+def _data_point_ct(child, index):
+ """Attribute value from IEC60870DataPoint element by XSD order (0=ASDU, 1=IOA, 2=count)."""
+ for element in child.GetParamsAttributes():
+ if element["name"] == "IEC60870DataPoint":
+ return element["children"][index]["value"]
+ raise KeyError("IEC60870DataPoint")
+
+
+def _location_tuple_from_tree_string(locstr):
+ """
+ Map a plugin tree location string (e.g. D4.0.0, X4.0.2) to the numeric LOC tuple
+ in LOCATED_VARIABLES.h / ProjectController.GetLocations().
+ """
+ if not locstr:
+ return None
+ i = 0
+ while i < len(locstr) and not locstr[i].isdigit():
+ i += 1
+ body = locstr[i:]
+ if not body:
+ return None
+ return tuple(int(x) for x in body.split("."))
+
+
+def _find_name_for_loc_tuple(target_tuple, iterable_of_locdicts):
+ """C symbol NAME for located var with LOC == target_tuple."""
+ if not target_tuple:
+ return None
+ want = tuple(target_tuple)
+ for locdic in iterable_of_locdicts:
+ loc = locdic.get("LOC")
+ if loc is None:
+ continue
+ if tuple(loc) == want:
+ return str(locdic["NAME"])
+ return None
+
+
+def _resolve_diagnostics_by_suffix_tail(srv_loc, iterable_of_locdicts):
+ """
+ Match %MD…0 / %MD…1 / %MX…2 when matiec LOC has a longer prefix than
+ GetCurrentLocation() but ends with the same address tail (e.g. extra
+ leading indices from configuration / resource).
+ """
+ if not iterable_of_locdicts:
+ return None, None, None
+ srv_loc = tuple(srv_loc)
+ by_loc = {}
+ for locdic in iterable_of_locdicts:
+ loc = locdic.get("LOC")
+ if not loc:
+ continue
+ by_loc[tuple(loc)] = locdic
+ prefixes = set()
+ for locdic in iterable_of_locdicts:
+ loc = locdic.get("LOC")
+ if loc and len(loc) >= 2:
+ prefixes.add(tuple(loc[:-1]))
+ # Prefer longest P first so a resource-prefixed path wins over a shorter tail.
+ for P in sorted(prefixes, key=lambda x: -len(x)):
+ if len(P) < len(srv_loc):
+ continue
+ if P[-len(srv_loc):] != srv_loc:
+ continue
+ k0 = P + (0,)
+ k1 = P + (1,)
+ k2 = P + (2,)
+ d0 = by_loc.get(k0)
+ d1 = by_loc.get(k1)
+ d2 = by_loc.get(k2)
+ if not (d0 and d1 and d2):
+ continue
+ if (d0.get("IEC_TYPE") == "UDINT" and d1.get("IEC_TYPE") == "UDINT"
+ and d2.get("IEC_TYPE") == "BOOL"):
+ return str(d0["NAME"]), str(d1["NAME"]), str(d2["NAME"])
+ return None, None, None
+
+
+def _safe_text(x):
+ """Python 2/3: unified unicode/str for diag file content."""
+ if isinstance(x, text_type):
+ return x
+ if isinstance(x, bytes):
+ return x.decode("utf-8", "replace")
+ return text_type(x)
+
+
+# Beremiz ProjectController.GetLocations() only accepts LOC without spaces
+# (see LOC pattern [,0-9]*). Some matiec builds emit "4, 0, 0" and return
+# zero entries while LOCATED_VARIABLES.h is non-empty. Parse those here.
+_LOOSE_LOCATED_LINE = re.compile(
+ r"__LOCATED_VAR\s*\(\s*"
+ r"(?P<IEC_TYPE>[A-Z][A-Z0-9_]*)\s*,\s*"
+ r"(?P<NAME>[_A-Za-z0-9]+)\s*,\s*"
+ r"(?P<DIR>[QMI])\s*"
+ r"(?:,\s*(?P<SIZE>[XBWDL])\s*)?"
+ r",\s*(?P<LOC>[\d,\s]+)\)\s*"
+ r"(?:;)?\s*(?://.*)?"
+)
+
+
+def _parse_located_variables_h_loose(filepath):
+ """
+ Same structure as ProjectController.GetLocations() resdicts, with
+ whitespace-tolerant LOC lists.
+ """
+ locations = []
+ if not filepath or not os.path.isfile(filepath):
+ return locations
+ try:
+ with open(filepath, "r") as f:
+ lines = f.readlines()
+ except Exception:
+ return locations
+ for line in lines:
+ line = line.strip()
+ if "__LOCATED_VAR" not in line:
+ continue
+ if line.startswith("//") or line.startswith("/*"):
+ continue
+ m = _LOOSE_LOCATED_LINE.search(line)
+ if not m:
+ continue
+ resdict = m.groupdict()
+ loc_raw = resdict["LOC"]
+ loc_raw = loc_raw.replace(" ", "").replace("\t", "")
+ if not loc_raw:
+ continue
+ try:
+ resdict["LOC"] = tuple(map(int, loc_raw.split(",")))
+ except Exception:
+ continue
+ if not resdict.get("SIZE"):
+ resdict["SIZE"] = "X"
+ locations.append(resdict)
+ return locations
+
+
+def _write_iec60870_diag_failure_file(
+ buildpath, srv, locations, project_locations, srv_get_locs):
+ """Write build/iec60870_diag_failed.txt (UTF-8). Always leaves non-empty text."""
+ p = os.path.join(buildpath or ".", "iec60870_diag_failed.txt")
+ lines_u = []
+ try:
+ lines_u.append(_safe_text("IEC60870 - diagnostic variable resolution failed"))
+ lines_u.append(_safe_text(""))
+ lines_u.append(_safe_text("server_path (GetCurrentLocation): %s") % _safe_text(
+ ".".join(map(str, srv.GetCurrentLocation()))))
+ root = srv.GetCTRoot()
+ lv_file = os.path.join(root._getBuildPath(), "LOCATED_VARIABLES.h")
+ lines_u.append(_safe_text("buildpath (this run): %s") % _safe_text(buildpath))
+ tree = srv.GetVariableLocationTree()
+ ch = tree.get("children") or []
+ lines_u.append(_safe_text("tree diagnostic location strings (first 3 children):"))
+ for i in range(min(3, len(ch))):
+ loc_raw = ch[i].get("location", "") if hasattr(ch[i], "get") else ""
+ lines_u.append(_safe_text(" [%s] %s") % (i, _safe_text(repr(loc_raw))))
+ t0 = _location_tuple_from_tree_string(
+ ch[0].get("location", "")) if len(ch) > 0 else None
+ t1 = _location_tuple_from_tree_string(
+ ch[1].get("location", "")) if len(ch) > 1 else None
+ t2 = _location_tuple_from_tree_string(
+ ch[2].get("location", "")) if len(ch) > 2 else None
+ lines_u.append(_safe_text("parsed tuples (matiec should match LOC): %s, %s, %s") % (
+ t0, t1, t2))
+ lines_u.append(_safe_text(""))
+ lines_u.append(_safe_text("LOCATED_VARIABLES.h: %s") % _safe_text(lv_file))
+ lines_u.append(_safe_text("file exists: %s") % _safe_text(os.path.isfile(lv_file)))
+ lines_u.append(_safe_text("len(locations pass to CTNGenerate_C): %s") % (
+ len(locations) if locations else 0,))
+ lines_u.append(_safe_text("len(GetCTRoot().GetLocations()): %s") % (
+ len(project_locations) if project_locations else 0,))
+ lines_u.append(_safe_text("len(srv.GetLocations()): %s") % len(srv_get_locs))
+ lv_bp = os.path.join(buildpath or ".", "LOCATED_VARIABLES.h")
+ loose_n = len(_parse_located_variables_h_loose(lv_bp))
+ lines_u.append(_safe_text("len(iec60870 loose LOCATED_VARIABLES parse): %s") % loose_n)
+ lines_u.append(_safe_text(""))
+ lines_u.append(_safe_text(
+ "Memory vars with LOC ending in last index 0, 1, or 2 (sample):"))
+ seen = set()
+ n = 0
+ for locdic in (project_locations or []) + (locations or []):
+ loc = locdic.get("LOC")
+ if not loc:
+ continue
+ if loc[-1] not in (0, 1, 2):
+ continue
+ if locdic.get("DIR") != "M":
+ continue
+ key = tuple(loc)
+ if key in seen:
+ continue
+ seen.add(key)
+ lines_u.append(_safe_text(
+ " NAME=%s IEC=%s SIZE=%s LOC=%s") % (
+ _safe_text(locdic.get("NAME")),
+ _safe_text(locdic.get("IEC_TYPE")),
+ _safe_text(locdic.get("SIZE")),
+ _safe_text(loc),
+ ))
+ n += 1
+ if n >= 80:
+ break
+ if loose_n == 0 and os.path.isfile(lv_bp):
+ try:
+ with open(lv_bp, "rb") as rf:
+ head = rf.read(2500)
+ lines_u.append(_safe_text(
+ "--- LOCATED_VARIABLES.h (first 2500 bytes, repr) ---"))
+ lines_u.append(_safe_text(repr(head)))
+ if not head:
+ lines_u.append(_safe_text(
+ "NOTE: file is empty (0 bytes). matiec may not have "
+ "written it yet for this build step, or the toolchain "
+ "touched a stub. Plugin uses intrinsic static "
+ "diagnostic counters in IEC104_*.c (not IEC-visible)."))
+ except Exception:
+ pass
+ except Exception as exc:
+ lines_u = [
+ _safe_text("IEC60870 - diagnostic dump failed while collecting data"),
+ _safe_text(repr(exc)),
+ _safe_text(traceback.format_exc()),
+ ]
+ body = _safe_text("\n").join(lines_u) + _safe_text("\n")
+ try:
+ with io.open(p, "w", encoding="utf-8") as out:
+ out.write(body)
+ except Exception as exc2:
+ try:
+ with io.open(p, "w", encoding="utf-8") as out:
+ out.write(_safe_text(
+ "Could not write UTF-8 diag file: %s\n%s"
+ % (repr(exc2), traceback.format_exc())))
+ except Exception:
+ pass
+
+
+def _resolve_server_diagnostic_names(srv, locations, project_locations):
+ """
+ Resolve read/write/connection to PLC symbol names, or request intrinsic storage.
+
+ Returns (rd, wr, cn, mode) with mode \"plc\" (bind to matiec globals) or
+ \"intrinsic\" (server-local static UDINT/IEC_BOOL in IEC104_*.c — used when
+ LOCATED_VARIABLES.h is empty or names cannot be resolved, to avoid bogus
+ externs like __MD4_0_0 that the linker does not provide).
+ """
+ loc_sources = []
+ for src in (locations, project_locations):
+ if src and src not in loc_sources:
+ loc_sources.append(src)
+
+ tree = srv.GetVariableLocationTree()
+ children = tree.get("children") or []
+ if len(children) >= 3:
+ t0 = _location_tuple_from_tree_string(children[0].get("location", ""))
+ t1 = _location_tuple_from_tree_string(children[1].get("location", ""))
+ t2 = _location_tuple_from_tree_string(children[2].get("location", ""))
+ if t0 and t1 and t2:
+ for src in loc_sources:
+ if not src:
+ continue
+ rd = _find_name_for_loc_tuple(t0, src)
+ wr = _find_name_for_loc_tuple(t1, src)
+ cn = _find_name_for_loc_tuple(t2, src)
+ if rd and wr and cn:
+ return rd, wr, cn, "plc"
+
+ srv_loc = tuple(srv.GetCurrentLocation())
+ for src in loc_sources:
+ if not src:
+ continue
+ rd = wr = cn = None
+ for locdic in src:
+ loc = locdic.get("LOC")
+ if loc is None:
+ continue
+ loc = tuple(loc)
+ if len(loc) != len(srv_loc) + 1:
+ continue
+ if loc[:-1] != srv_loc:
+ continue
+ idx = loc[-1]
+ name = str(locdic["NAME"])
+ if idx == 0:
+ rd = name
+ elif idx == 1:
+ wr = name
+ elif idx == 2:
+ cn = name
+ if rd and wr and cn:
+ return rd, wr, cn, "plc"
+
+ merged = []
+ seen_loc = set()
+ for src in loc_sources:
+ if not src:
+ continue
+ for d in src:
+ loc = d.get("LOC")
+ if loc is None:
+ continue
+ t = tuple(loc)
+ if t in seen_loc:
+ continue
+ seen_loc.add(t)
+ merged.append(d)
+ rd, wr, cn = _resolve_diagnostics_by_suffix_tail(srv_loc, merged)
+ if rd and wr and cn:
+ return rd, wr, cn, "plc"
+
+ fallback = srv.GetLocations()
+ if fallback:
+ rd, wr, cn = _resolve_diagnostics_by_suffix_tail(srv_loc, fallback)
+ if rd and wr and cn:
+ return rd, wr, cn, "plc"
+ rd = wr = cn = None
+ for locdic in fallback:
+ loc = locdic.get("LOC")
+ if loc is None:
+ continue
+ loc = tuple(loc)
+ if len(loc) != len(srv_loc) + 1:
+ continue
+ if loc[:-1] != srv_loc:
+ continue
+ idx = loc[-1]
+ name = str(locdic["NAME"])
+ if idx == 0:
+ rd = name
+ elif idx == 1:
+ wr = name
+ elif idx == 2:
+ cn = name
+ if rd and wr and cn:
+ return rd, wr, cn, "plc"
+
+ return None, None, None, "intrinsic"
+
+
#
# D A T A P O I N T
#
@@ -471,35 +830,230 @@
a3=port1, a4=addr1, a5=addr2)
self.FatalError(error_message)
- # define a unique name for the generated C and h files
- current_location = self.GetCurrentLocation()
+ loc_prefix = "_".join(map(str, self.GetCurrentLocation()))
+ servers = [ch for ch in self.IECSortedChildren() if getattr(
+ ch, "PlugType", None) == "IEC60870Server"]
+
+ if not servers:
+ return [], "", False
+
+ IEC60870Path = paths.ThirdPartyPath("IEC60870")
+
+ for ch in servers:
+ tls = _srv_attr_map(ch).get("Use_TLS", False)
+ if str(tls).lower() in ("true", "1", "yes"):
+ self.FatalError(
+ _("IEC60870: TLS is not supported by the generated "
+ "CS104 runtime. Disable Use_TLS on server \"%s\".\n") %
+ ch.BaseParams.getName())
+
+ extern_lines = []
+ extern_seen = set()
+ bindings_rows = []
+ srv_rows = []
+ diag_static_lines = []
+
+ def add_extern(iec_type, name):
+ key = (iec_type, name)
+ if key in extern_seen:
+ return
+ extern_seen.add(key)
+ extern_lines.append(
+ "extern %(t)s %(n)s;" % {"t": iec_type, "n": name})
+
+ max_remote = int(self.GetParamsAttributes()[0]["children"][0]["value"])
+
+ project_locs = self.GetCTRoot().GetLocations()
+ lv_path = os.path.join(buildpath, "LOCATED_VARIABLES.h")
+ loose_locs = _parse_located_variables_h_loose(lv_path)
+ if len(loose_locs) > len(project_locs or []):
+ project_locs = loose_locs
+
+ for server_id, srv in enumerate(servers):
+ smap = _srv_attr_map(srv)
+ rd_name, wr_name, conn_name, diag_mode = _resolve_server_diagnostic_names(
+ srv, locations, project_locs)
- prefix = "_".join(map(str, current_location))
- gen_iec60870_c_path = os.path.join(buildpath, "iec60870_%s.c" % prefix)
- gen_iec60870_h_path = os.path.join(buildpath, "iec60870_%s.h" % prefix)
- c_filename = os.path.join(os.path.split(__file__)[0], "iec60870_runtime.c")
- h_filename = os.path.join(os.path.split(__file__)[0], "iec60870_runtime.h")
+ if diag_mode != "intrinsic" and not (
+ rd_name and wr_name and conn_name):
+ _write_iec60870_diag_failure_file(
+ buildpath, srv, locations, project_locs,
+ srv.GetLocations())
+ dbg = os.path.join(buildpath, "iec60870_diag_failed.txt")
+ try:
+ self.GetCTRoot().logger.write_error(
+ _("IEC60870: diagnostic bind failed for server at %s. "
+ "Details: %s\n") % (
+ ".".join(map(str, srv.GetCurrentLocation())),
+ dbg))
+ except Exception:
+ pass
+ self.FatalError(
+ _("IEC60870: Could not resolve server diagnostic variables "
+ "(read/write/connection) for server at %s.\n"
+ "See \"%s\" in the project build folder.\n") % (
+ ".".join(map(str, srv.GetCurrentLocation())),
+ dbg))
+
+ if diag_mode == "intrinsic":
+ rd_name = "iec60870_diag_rd_%d" % server_id
+ wr_name = "iec60870_diag_wr_%d" % server_id
+ conn_name = "iec60870_diag_conn_%d" % server_id
+ diag_static_lines.append(
+ "static UDINT %(rd)s = 0U;\n"
+ "static UDINT %(wr)s = 0U;\n"
+ "static IEC_BOOL %(cn)s = (IEC_BOOL)0;\n" % {
+ "rd": rd_name,
+ "wr": wr_name,
+ "cn": conn_name,
+ })
+ else:
+ add_extern("UDINT", rd_name)
+ add_extern("UDINT", wr_name)
+ add_extern("BOOL", conn_name)
+
+ ip = smap.get("Local_IP_Address", "#ANY#")
+ if ip in ("", "*", "#ANY#"):
+ ip_c = "0.0.0.0"
+ else:
+ ip_c = _c_escape_str(str(ip))
+
+ port = int(smap.get("Local_Port_Number", 2404))
+ common = int(smap.get("Common_Address", 1))
+ ca_sz = int(smap.get("CA_Size", 2))
+ ioa_sz = int(smap.get("IOA_Size", 3))
+ cot_has_oa = str(smap.get("COT_Has_OA", True)).lower() in (
+ "true", "1", "yes")
+ oa = int(smap.get("OA_Value", 10))
+ apci_k = int(smap["APCI_k"])
+ apci_w = int(smap["APCI_w"])
+ t0 = int(smap["Timeout_t0"])
+ t1 = int(smap["Timeout_t1"])
+ t2 = int(smap["Timeout_t2"])
+ t3 = int(smap["Timeout_t3"])
+
+ loc_label = _c_escape_str(
+ ".".join(map(str, srv.GetCurrentLocation())))
- # TODO: get located variables and merge them with template files
- loc_vars = []
- loc_dict = {"locstr": "_".join(map(str, self.GetCurrentLocation()))}
- loc_dict["loc_vars"] = "\n".join(loc_vars)
-
- # create output files using templates
- iec60870_main = open(h_filename).read() % loc_dict
- f = open(gen_iec60870_h_path, 'w')
- f.write(iec60870_main)
- f.close()
+ srv_rows.append(
+ """
+ { .loc_label = "%(loc)s",
+ .common_address = %(co)d,
+ .ip_str = "%(ipc)s",
+ .port = %(port)d,
+ .max_open = %(maxc)d,
+ .ca_sz = %(casz)d,
+ .ioa_sz = %(ioasz)d,
+ .cot_two_byte = %(cotoa)d,
+ .oa = %(oa)d,
+ .apci_k = %(apk)d, .apci_w = %(apw)d, .t0 = %(t0)d, .t1 = %(t1)d,
+ .t2 = %(t2)d, .t3 = %(t3)d,
+ .rd_ctr = &(%(rd)s),
+ .wr_ctr = &(%(wr)s),
+ .conn_bool = &(%(cn)s),
+ .slave = NULL,
+ .init_st = 0
+ },""" % {
+ "loc": loc_label,
+ "co": common,
+ "ipc": ip_c,
+ "port": port,
+ "maxc": max_remote,
+ "casz": ca_sz,
+ "ioasz": ioa_sz,
+ "cotoa": 1 if cot_has_oa else 0,
+ "oa": oa,
+ "apk": apci_k,
+ "apw": apci_w,
+ "t0": t0,
+ "t1": t1,
+ "t2": t2,
+ "t3": t3,
+ "rd": rd_name,
+ "wr": wr_name,
+ "cn": conn_name,
+ })
- iec60870_main = open(c_filename).read() % loc_dict
- f = open(gen_iec60870_c_path, 'w')
- f.write(iec60870_main)
- f.close()
-
+ for dp in srv.IECSortedChildren():
+ asdu_str = _data_point_ct(dp, 0)
+ ioa0 = int(_data_point_ct(dp, 1))
+ npoints = int(_data_point_ct(dp, 2))
+ tid, _dt, _ds, direction, _sc, _d = iec60870_asdu_types[asdu_str]
+ is_cmd = 1 if direction == "Q" else 0
+ for iecvar in dp.GetLocations():
+ loc = iecvar["LOC"]
+ if len(loc) < 4:
+ continue
+ ioa_v = int(loc[-1])
+ if ioa_v < ioa0 or ioa_v >= ioa0 + npoints:
+ continue
+ nm = str(iecvar["NAME"])
+ iet = iecvar["IEC_TYPE"]
+ add_extern(iet, nm)
+ bk = iec_iec_type_to_bind_kind(iet)
+ bindings_rows.append(
+ " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bk)d,"
+ " (void *)&(%(nm)s) }," % {
+ "sid": server_id,
+ "tid": tid,
+ "ioa": ioa_v,
+ "isc": is_cmd,
+ "bk": bk,
+ "nm": nm,
+ })
+
+ num_srv = len(servers)
+ if not bindings_rows:
+ bindings_rows.append(
+ " { -1, 0, 0, 0, 0, NULL }, /* placeholder */")
+ num_bind = len(bindings_rows)
+ extern_block = "\n".join(extern_lines)
+ bindings_body = "\n".join(bindings_rows)
+
+ srv_def = (
+ "static iec60870_srv_t iec60870_srv[IEC60870_NUM_SERVERS_%s] = {%s\n};"
+ % (loc_prefix, "".join(srv_rows)))
+
+ diag_static_block = "".join(diag_static_lines)
+ if diag_static_block:
+ diag_static_block = (
+ "/* Intrinsic diagnostic storage — not mapped to IEC %%MD/%%MX */\n"
+ + diag_static_block)
+
+ tpl = {
+ "locstr": loc_prefix,
+ "diag_static_block": diag_static_block,
+ "extern_block": extern_block,
+ "bindings_rows": bindings_body,
+ "srv_def": srv_def,
+ "num_servers": str(num_srv),
+ "num_bindings": str(num_bind),
+ "max_remote_clients": str(max_remote),
+ }
+
+ c_src = os.path.join(os.path.split(__file__)[0], "iec60870_runtime.c")
+ h_src = os.path.join(os.path.split(__file__)[0], "iec60870_runtime.h")
+ gen_c = os.path.join(buildpath, "IEC104_%s.c" % loc_prefix)
+ gen_h = os.path.join(buildpath, "IEC104_%s.h" % loc_prefix)
+
+ with open(h_src, "r") as f:
+ h_text = f.read() % tpl
+ with open(gen_h, "w") as fh:
+ fh.write(h_text)
+
+ with open(c_src, "r") as f:
+ c_text = f.read() % tpl
+ with open(gen_c, "w") as fc:
+ fc.write(c_text)
+
LDFLAGS = []
- LDFLAGS.append(" \"-L" + iec60870_path + "\"")
- #TODO: fix lib name
- LDFLAGS.append(" \"" + os.path.join(iec60870_path, "liblib60870.a") + "\"")
- LDFLAGS.append(" \"-Wl,-rpath," + iec60870_path + "\"")
+ LDFLAGS.append(' "-L' + IEC60870Path + '"')
+ LDFLAGS.append(' "' + os.path.join(IEC60870Path, "liblib60870.a") + '"')
+ LDFLAGS.append(' "-lpthread"')
- return [(gen_iec60870_c_path, ' -I"' + iec60870_path + '"')], LDFLAGS, False
+ cflags = ' -I"' + IEC60870Path + '"'
+ return (
+ [(gen_c, cflags)],
+ LDFLAGS,
+ True,
+ )
--- a/iec60870/iec60870_runtime.c Mon Apr 13 15:25:10 2026 +0200
+++ b/iec60870/iec60870_runtime.c Thu Apr 16 12:35:36 2026 +0200
@@ -1,69 +1,329 @@
-/* iec60870 runtime C extension
- * TODO: plugin-specific init/publish/retrieve/cleanup functions
- * which Beremiz runtime can link and call.
- */
-
+/* Generated IEC 60870-5-104 (CS104) server runtime — instance %(locstr)s */
#include <stdio.h>
+#include <stdlib.h>
#include <string.h>
-#include <errno.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include "iec_types_all.h"
+#include "iec60870_common.h"
#include "cs104_slave.h"
+#include "cs101_information_objects.h"
+#include "IEC104_%(locstr)s.h"
-CS104_Slave slave;
-CS101_AppLayerParameters alParams;
+/* Referenced from __init_* before definition; silences -Wimplicit-function-declaration */
+int __cleanup_%(locstr)s(void);
+
+/* Filled when PLC located symbols are unavailable (empty LOCATED_VARIABLES.h etc.) */
+%(diag_static_block)s
+struct iec60870_binding {
+ int server_index;
+ int type_id;
+ int ioa;
+ int is_command;
+ int bind_kind;
+ void *iec_var;
+};
+
+typedef struct {
+ const char *loc_label;
+ int common_address;
+ char ip_str[128];
+ int port;
+ int max_open;
+ int ca_sz;
+ int ioa_sz;
+ int cot_two_byte;
+ int oa;
+ int apci_k, apci_w, t0, t1, t2, t3;
+ UDINT *rd_ctr;
+ UDINT *wr_ctr;
+ BOOL *conn_bool;
+ CS104_Slave slave;
+ int init_st;
+} iec60870_srv_t;
+
+static struct iec60870_binding iec60870_bindings[] = {
+%(bindings_rows)s
+};
+
+static const size_t iec60870_binding_count = IEC60870_NUM_BINDINGS_%(locstr)s;
+
+%(extern_block)s
+
+%(srv_def)s
+
+static void iec_rd_inc(iec60870_srv_t *s) {
+ if (s->rd_ctr)
+ *s->rd_ctr += 1U;
+}
+
+static void iec_wr_inc(iec60870_srv_t *s) {
+ if (s->wr_ctr)
+ *s->wr_ctr += 1U;
+}
+
+static bool iec_get_bool_var(void *p) {
+ return *(IEC_BOOL *)p != 0;
+}
+
+static void iec_set_bool_var(void *p, bool v) {
+ *(IEC_BOOL *)p = v ? (IEC_BOOL)1 : (IEC_BOOL)0;
+}
+
+static InformationObject iec_make_monitor_io(int type_id, int ioa, struct iec60870_binding *b) {
+ void *mem = malloc((size_t)InformationObject_getMaxSizeInMemory());
+ if (!mem)
+ return NULL;
+ QualityDescriptor q = IEC60870_QUALITY_GOOD;
+
+ switch (type_id) {
+ case M_SP_NA_1: {
+ bool val = iec_get_bool_var(b->iec_var);
+ return (InformationObject)SinglePointInformation_create(
+ (SinglePointInformation)mem, ioa, val, q);
+ }
+ case M_DP_NA_1: {
+ DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3);
+ return (InformationObject)DoublePointInformation_create(
+ (DoublePointInformation)mem, ioa, dv, q);
+ }
+ case M_ST_NA_1: {
+ int sv = (int)*(IEC_INT *)b->iec_var;
+ return (InformationObject)StepPositionInformation_create(
+ (StepPositionInformation)mem, ioa, sv, false, q);
+ }
+ case M_ME_NA_1: {
+ float nv = NormalizedValue_fromScaled(
+ (int)(int16_t)*(IEC_UINT *)b->iec_var);
+ return (InformationObject)MeasuredValueNormalized_create(
+ (MeasuredValueNormalized)mem, ioa, nv, q);
+ }
+ case M_ME_NB_1: {
+ int sv = (int)*(IEC_INT *)b->iec_var;
+ return (InformationObject)MeasuredValueScaled_create(
+ (MeasuredValueScaled)mem, ioa, sv, q);
+ }
+ case M_ME_NC_1: {
+ float fv = *(IEC_REAL *)b->iec_var;
+ return (InformationObject)MeasuredValueShort_create(
+ (MeasuredValueShort)mem, ioa, fv, q);
+ }
+ default:
+ free(mem);
+ return NULL;
+ }
+}
+
+static bool iec_send_monitor(IMasterConnection conn, iec60870_srv_t *srv, struct iec60870_binding *b) {
+ CS101_AppLayerParameters al = CS104_Slave_getAppLayerParameters(srv->slave);
+ int oa = srv->cot_two_byte ? srv->oa : 0;
+
+ InformationObject io = iec_make_monitor_io(b->type_id, b->ioa, b);
+ if (!io)
+ return false;
+
+ CS101_ASDU asdu = CS101_ASDU_create(al, false, CS101_COT_INTERROGATED_BY_STATION, oa,
+ srv->common_address, false, false);
+ if (!asdu) {
+ InformationObject_destroy(io);
+ return false;
+ }
+ if (!CS101_ASDU_addInformationObject(asdu, io)) {
+ InformationObject_destroy(io);
+ CS101_ASDU_destroy(asdu);
+ return false;
+ }
+ if (!IMasterConnection_sendASDU(conn, asdu)) {
+ CS101_ASDU_destroy(asdu);
+ return false;
+ }
+ CS101_ASDU_destroy(asdu);
+ iec_rd_inc(srv);
+ return true;
+}
+
+static bool iec_interrogation_handler(void *parameter, IMasterConnection connection, CS101_ASDU asdu, uint8_t qoi) {
+ (void)asdu;
+ iec60870_srv_t *srv = (iec60870_srv_t *)parameter;
+ size_t idx = (size_t)(srv - iec60870_srv);
+ size_t i;
+
+ if (qoi == IEC60870_QOI_STATION) {
+ for (i = 0; i < iec60870_binding_count; i++) {
+ struct iec60870_binding *b = &iec60870_bindings[i];
+ if (b->server_index < 0)
+ continue;
+ if ((size_t)b->server_index != idx)
+ continue;
+ if (b->is_command)
+ continue;
+ iec_send_monitor(connection, srv, b);
+ }
+ }
+ return true;
+}
+
+static bool iec_handle_command(iec60870_srv_t *srv, IMasterConnection connection, CS101_ASDU asdu) {
+ TypeID tid = CS101_ASDU_getTypeID(asdu);
+ InformationObject io = CS101_ASDU_getElement(asdu, 0);
+ size_t srv_idx = (size_t)(srv - iec60870_srv);
+ size_t i;
+ struct iec60870_binding *match = NULL;
+
+ if (!io)
+ return false;
+ {
+ int ioa = InformationObject_getObjectAddress(io);
+ for (i = 0; i < iec60870_binding_count; i++) {
+ if (iec60870_bindings[i].server_index < 0)
+ continue;
+ if ((size_t)iec60870_bindings[i].server_index != srv_idx)
+ continue;
+ if (!iec60870_bindings[i].is_command)
+ continue;
+ if (iec60870_bindings[i].ioa == ioa && iec60870_bindings[i].type_id == (int)tid) {
+ match = &iec60870_bindings[i];
+ break;
+ }
+ }
+ }
+ if (!match)
+ return false;
+
+ switch (tid) {
+ case C_SC_NA_1: {
+ SingleCommand sc = (SingleCommand)io;
+ iec_set_bool_var(match->iec_var, SingleCommand_getState(sc));
+ break;
+ }
+ case C_DC_NA_1: {
+ DoubleCommand dc = (DoubleCommand)io;
+ *(IEC_BYTE *)match->iec_var = (IEC_BYTE)(DoubleCommand_getState(dc) & 0xff);
+ break;
+ }
+ case C_RC_NA_1: {
+ StepCommand sc = (StepCommand)io;
+ *(IEC_BYTE *)match->iec_var = (IEC_BYTE)((int)StepCommand_getState(sc) & 0xff);
+ break;
+ }
+ case C_SE_NA_1: {
+ SetpointCommandNormalized sn = (SetpointCommandNormalized)io;
+ {
+ float fv = SetpointCommandNormalized_getValue(sn);
+ int scv = NormalizedValue_toScaled(fv);
+ *(IEC_UINT *)match->iec_var = (uint16_t)scv;
+ }
+ break;
+ }
+ case C_SE_NB_1: {
+ SetpointCommandScaled ss = (SetpointCommandScaled)io;
+ *(IEC_INT *)match->iec_var = (int16_t)SetpointCommandScaled_getValue(ss);
+ break;
+ }
+ case C_SE_NC_1: {
+ SetpointCommandShort sf = (SetpointCommandShort)io;
+ *(IEC_REAL *)match->iec_var = SetpointCommandShort_getValue(sf);
+ break;
+ }
+ default:
+ return false;
+ }
+
+ iec_wr_inc(srv);
+ IMasterConnection_sendACT_CON(connection, asdu, false);
+ return true;
+}
+
+static bool iec_asdu_handler(void *parameter, IMasterConnection connection, CS101_ASDU asdu) {
+ iec60870_srv_t *srv = (iec60870_srv_t *)parameter;
+ TypeID tid = CS101_ASDU_getTypeID(asdu);
+
+ if ((int)tid >= C_SC_NA_1 && (int)tid <= C_SE_NC_1)
+ return iec_handle_command(srv, connection, asdu);
+ iec_rd_inc(srv);
+ return false;
+}
+
+static void iec_connection_event(void *parameter, IMasterConnection connection, CS104_PeerConnectionEvent event) {
+ (void)connection;
+ iec60870_srv_t *srv = (iec60870_srv_t *)parameter;
+ if (srv->conn_bool) {
+ if (event == CS104_CON_EVENT_ACTIVATED)
+ iec_set_bool_var(srv->conn_bool, true);
+ else if (event == CS104_CON_EVENT_DEACTIVATED || event == CS104_CON_EVENT_CONNECTION_CLOSED)
+ iec_set_bool_var(srv->conn_bool, CS104_Slave_getOpenConnections(srv->slave) > 0);
+ }
+}
int __init_%(locstr)s(int argc, char **argv) {
- fprintf(stderr, "iec60870 extension: __init_iec60870\n");
+ int si;
+ (void)argc;
+ (void)argv;
- // TODO: multi-server/client support?
- /* create a new slave/server instance with default connection parameters and
- * default message queue size */
- slave = CS104_Slave_create(10, 10);
-
+ for (si = 0; si < IEC60870_NUM_SERVERS_%(locstr)s; si++) {
+ iec60870_srv_t *s = &iec60870_srv[si];
- CS104_Slave_setLocalAddress(slave, "0.0.0.0");
-
-
- /* Set mode to a single redundancy group
- * NOTE: library has to be compiled with CONFIG_CS104_SUPPORT_SERVER_MODE_SINGLE_REDUNDANCY_GROUP enabled (=1)
- */
- CS104_Slave_setServerMode(slave, CS104_MODE_SINGLE_REDUNDANCY_GROUP);
-
+ s->slave = CS104_Slave_create(200, 200);
+ if (!s->slave) {
+ fprintf(stderr, "IEC60870 %%s: CS104_Slave_create failed\\n", s->loc_label);
+ goto fail;
+ }
+ CS104_Slave_setLocalAddress(s->slave, s->ip_str);
+ CS104_Slave_setLocalPort(s->slave, s->port);
+ CS104_Slave_setMaxOpenConnections(s->slave, s->max_open);
- /* get the connection parameters - we need them to create correct ASDUs -
- * you can also modify the parameters here when default parameters are not to be used */
- alParams = CS104_Slave_getAppLayerParameters(slave);
-
-
- /* when you have to tweak the APCI parameters (t0-t3, k, w) you can access them here */
- CS104_APCIParameters apciParams = CS104_Slave_getConnectionParameters(slave);
-
+ {
+ CS101_AppLayerParameters al = CS104_Slave_getAppLayerParameters(s->slave);
+ al->sizeOfCA = s->ca_sz;
+ al->sizeOfIOA = s->ioa_sz;
+ al->sizeOfCOT = s->cot_two_byte ? 2 : 1;
+ al->originatorAddress = s->oa;
+ }
+ {
+ CS104_APCIParameters ap = CS104_Slave_getConnectionParameters(s->slave);
+ ap->k = s->apci_k;
+ ap->w = s->apci_w;
+ ap->t0 = s->t0;
+ ap->t1 = s->t1;
+ ap->t2 = s->t2;
+ ap->t3 = s->t3;
+ }
+ CS104_Slave_setInterrogationHandler(s->slave, iec_interrogation_handler, s);
+ CS104_Slave_setASDUHandler(s->slave, iec_asdu_handler, s);
+ CS104_Slave_setConnectionEventHandler(s->slave, iec_connection_event, s);
+ CS104_Slave_start(s->slave);
+ s->init_st = 1;
+ }
+ return 0;
- // printf("APCI parameters:\n");
- // printf(" t0: %%i\n", apciParams->t0);
- // printf(" t1: %%i\n", apciParams->t1);
- // printf(" t2: %%i\n", apciParams->t2);
- // printf(" t3: %%i\n", apciParams->t3);
- // printf(" k: %%i\n", apciParams->k);
- // printf(" w: %%i\n", apciParams->w);
+fail:
+ __cleanup_%(locstr)s();
+ return -1;
+}
- // TODO: initialize resources, threads, state
-
- return 0;
+void __retrieve_%(locstr)s(void) {
+ int si;
+ for (si = 0; si < IEC60870_NUM_SERVERS_%(locstr)s; si++) {
+ iec60870_srv_t *s = &iec60870_srv[si];
+ if (s->slave && s->conn_bool)
+ iec_set_bool_var(s->conn_bool, CS104_Slave_getOpenConnections(s->slave) > 0);
+ }
}
void __publish_%(locstr)s(void) {
- // TODO: sync data from PLC to extension outputs (if needed)
- fprintf(stderr, "iec60870 extension: __publish_iec60870\n");
-}
-
-void __retrieve_%(locstr)s(void) {
- // TODO: sync data from extension inputs to PLC (if needed)
}
int __cleanup_%(locstr)s(void) {
-
- fprintf(stderr, "iec60870 extension: __cleanup_iec60870\n");
- // TODO: release resources, stop threads
+ int si;
+ for (si = 0; si < IEC60870_NUM_SERVERS_%(locstr)s; si++) {
+ iec60870_srv_t *s = &iec60870_srv[si];
+ if (s->slave) {
+ CS104_Slave_stop(s->slave);
+ CS104_Slave_destroy(s->slave);
+ s->slave = NULL;
+ }
+ s->init_st = 0;
+ }
return 0;
}
--- a/iec60870/iec60870_runtime.h Mon Apr 13 15:25:10 2026 +0200
+++ b/iec60870/iec60870_runtime.h Thu Apr 16 12:35:36 2026 +0200
@@ -1,6 +1,10 @@
+/* Template expanded by IEC60870 RootClass.CTNGenerate_C — instance %(locstr)s */
-/*******************/
-/*located variables*/
-/*******************/
+#ifndef IEC60870_RUNTIME_%(locstr)s_H
+#define IEC60870_RUNTIME_%(locstr)s_H
-%(loc_vars)s
\ No newline at end of file
+#define IEC60870_NUM_SERVERS_%(locstr)s %(num_servers)s
+#define IEC60870_NUM_BINDINGS_%(locstr)s %(num_bindings)s
+#define IEC60870_MAX_REMOTE_CLIENTS_%(locstr)s %(max_remote_clients)s
+
+#endif /* IEC60870_RUNTIME_%(locstr)s_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iec60870/iec60870_utils.py Thu Apr 16 12:35:36 2026 +0200
@@ -0,0 +1,38 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""Helpers for IEC 60870-5-104 Beremiz plugin (CS104 server runtime generation)."""
+from __future__ import absolute_import
+
+try:
+ from six.moves import xrange
+except ImportError:
+ xrange = range
+
+
+def GetCTVal(child, index):
+ """Return attribute value from first XSD element by child index order."""
+ return child.GetParamsAttributes()[0]["children"][index]["value"]
+
+
+def GetCTValByName(child, elem_name, attr_name):
+ """Fetch attribute value by element name and attribute name."""
+ for element in child.GetParamsAttributes():
+ if element["name"] != elem_name:
+ continue
+ for ch in element["children"]:
+ if ch["name"] == attr_name:
+ return ch["value"]
+ raise KeyError((elem_name, attr_name))
+
+
+def iec_iec_type_to_bind_kind(iec_type):
+ """Maps PLC located variable IEC type to iec60870_bind_kind (see iec60870_runtime.c)."""
+ m = {
+ "BOOL": 0,
+ "BYTE": 1,
+ "INT": 2,
+ "WORD": 3,
+ "REAL": 4,
+ "UDINT": 5,
+ }
+ return m.get(iec_type, 0)