--- a/iec60870/iec60870.py Thu Apr 16 12:35:36 2026 +0200
+++ b/iec60870/iec60870.py Wed Apr 22 09:49:11 2026 +0200
@@ -2,17 +2,11 @@
from __future__ import absolute_import
-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
+from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY import util.paths as paths
@@ -37,12 +31,6 @@
"C_SE_NC_1 - Setpoint float": (50, "REAL", 32, "Q", "D", "Set Point Short Float"),
- "I": LOCATION_VAR_INPUT,
- "Q": LOCATION_VAR_OUTPUT,
- "M": LOCATION_VAR_MEMORY,
# XSD fragment for the shared IEC 60870-5-104 connection parameters
# (used in both server and client node XSD definitions)
_IEC60870_CONN_PARAMS_XSD = """\
@@ -143,6 +131,26 @@
raise KeyError("IEC60870DataPoint")
+def _iec_c_storage_type(iet): + return _IEC_TO_C_STORAGE.get(str(iet), "IEC_BOOL") +def _iec_c_storage_zero_init(c_type): + if c_type == "IEC_REAL": + return "(IEC_REAL)0.0f" + return "(%s)0" % c_type 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
@@ -213,15 +221,6 @@
- """Python 2/3: unified unicode/str for diag file content."""
- if isinstance(x, text_type):
- if isinstance(x, bytes):
- return x.decode("utf-8", "replace")
# 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.
@@ -236,6 +235,11 @@
+def _matiec_located_storage_cname(iec_name): + """Matiec storage symbol for located var NAME is __##NAME.""" + return "__" + str(iec_name) def _parse_located_variables_h_loose(filepath):
Same structure as ProjectController.GetLocations() resdicts, with
@@ -273,115 +277,8 @@
-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.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()))))
- 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") % (
- 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):"))
- for locdic in (project_locations or []) + (locations or []):
- loc = locdic.get("LOC")
- if loc[-1] not in (0, 1, 2):
- if locdic.get("DIR") != "M":
- 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")),
- if loose_n == 0 and os.path.isfile(lv_bp):
- with open(lv_bp, "rb") as rf:
- lines_u.append(_safe_text(
- "--- LOCATED_VARIABLES.h (first 2500 bytes, repr) ---"))
- lines_u.append(_safe_text(repr(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 as exc:
- _safe_text("IEC60870 - diagnostic dump failed while collecting data"),
- _safe_text(traceback.format_exc()),
- body = _safe_text("\n").join(lines_u) + _safe_text("\n")
- with io.open(p, "w", encoding="utf-8") as out:
- except Exception as exc2:
- with io.open(p, "w", encoding="utf-8") as out:
- "Could not write UTF-8 diag file: %s\n%s"
- % (repr(exc2), traceback.format_exc())))
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).
+ """Return (rd_name, wr_name, conn_name, mode) with mode \"plc\" or \"intrinsic\".""" for src in (locations, project_locations):
if src and src not in loc_sources:
@@ -525,7 +422,8 @@
type_id, datatype, datasize, direction, size_code, desc = \
iec60870_asdu_types[asdu_type_str]
- loc_type = LOCATION_TYPES[direction]
+ # Extension-owned %M memory (Modbus-style): no BSP %IX/%QX required. + loc_type = LOCATION_VAR_MEMORY for offset in range(ioa, ioa + count):
@@ -852,14 +750,18 @@
+ plc_memory_storage_lines = [] def add_extern(iec_type, name):
+ storage = _matiec_located_storage_cname(name) + key = ("stor", iec_type, name) - "extern %(t)s %(n)s;" % {"t": iec_type, "n": name})
+ "extern %(t)s %(s)s;" % {"t": iec_type, "s": storage}) max_remote = int(self.GetParamsAttributes()[0]["children"][0]["value"])
@@ -874,27 +776,6 @@
rd_name, wr_name, conn_name, diag_mode = _resolve_server_diagnostic_names(
srv, locations, project_locs)
- if diag_mode != "intrinsic" and not (
- rd_name and wr_name and conn_name):
- _write_iec60870_diag_failure_file(
- buildpath, srv, locations, project_locs,
- dbg = os.path.join(buildpath, "iec60870_diag_failed.txt")
- self.GetCTRoot().logger.write_error(
- _("IEC60870: diagnostic bind failed for server at %s. "
- ".".join(map(str, srv.GetCurrentLocation())),
- _("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())),
if diag_mode == "intrinsic":
rd_name = "iec60870_diag_rd_%d" % server_id
wr_name = "iec60870_diag_wr_%d" % server_id
@@ -935,6 +816,16 @@
loc_label = _c_escape_str(
".".join(map(str, srv.GetCurrentLocation())))
+ if diag_mode == "intrinsic": + rd_ref = "&(%s)" % rd_name + wr_ref = "&(%s)" % wr_name + cn_ref = "&(%s)" % conn_name + # PLC diagnostics: use address of matiec storage (__##name). + rd_ref = "&(%s)" % _matiec_located_storage_cname(rd_name) + wr_ref = "&(%s)" % _matiec_located_storage_cname(wr_name) + cn_ref = "&(%s)" % _matiec_located_storage_cname(conn_name) { .loc_label = "%(loc)s",
@@ -948,9 +839,9 @@
.apci_k = %(apk)d, .apci_w = %(apw)d, .t0 = %(t0)d, .t1 = %(t1)d,
.t2 = %(t2)d, .t3 = %(t3)d,
- .conn_bool = &(%(cn)s),
+ .conn_bool = %(cn_ref)s, @@ -969,11 +860,45 @@
+ srv_tree_loc = tuple(srv.GetCurrentLocation()) + if diag_mode == "intrinsic": + for iecvar in srv.GetLocations(): + loc = iecvar.get("LOC") + if not loc or len(loc) < 3: + if tail not in (0, 1, 2): + if len(pfx) < len(srv_tree_loc): + if pfx[-len(srv_tree_loc):] != srv_tree_loc: + nm = str(iecvar["NAME"]).strip() + if nm in loc_vars_seen: + iet = iecvar.get("IEC_TYPE") + if tail == 0 and iet == "UDINT": + "UDINT *%s = &iec60870_diag_rd_%d;\n" % ( + elif tail == 1 and iet == "UDINT": + "UDINT *%s = &iec60870_diag_wr_%d;\n" % ( + elif tail == 2 and iet in ("BOOL", "IEC_BOOL"): + "IEC_BOOL *%s = &iec60870_diag_conn_%d;\n" % ( for dp in srv.IECSortedChildren():
asdu_str = _data_point_ct(dp, 0)
ioa0 = int(_data_point_ct(dp, 1))
@@ -982,24 +907,35 @@
is_cmd = 1 if direction == "Q" else 0
for iecvar in dp.GetLocations():
if ioa_v < ioa0 or ioa_v >= ioa0 + npoints:
- nm = str(iecvar["NAME"])
+ nm = str(iecvar["NAME"]).strip()
bk = iec_iec_type_to_bind_kind(iet)
+ bind_idx = len(bindings_rows) + c_stor = _iec_c_storage_type(iet) + stor_name = "iec60870_plcmem_%s_%d" % (loc_prefix, bind_idx) + zinit = _iec_c_storage_zero_init(c_stor) + plc_memory_storage_lines.append( + "static %(ct)s %(sn)s = %(z)s;\n" % { + "ct": c_stor, "sn": stor_name, "z": zinit}) + if nm not in loc_vars_seen: + "%(ct)s *%(nm)s = &%(sn)s;\n" % { + "ct": c_stor, "nm": nm, "sn": stor_name}) - " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bk)d,"
- " (void *)&(%(nm)s) }," % {
+ " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bk)d, " + "(void *)&%(sn)s }," % {
@@ -1017,12 +953,22 @@
diag_static_block = "".join(diag_static_lines)
- "/* Intrinsic diagnostic storage — not mapped to IEC %%MD/%%MX */\n"
+ "/* Intrinsic diagnostic counters / connection flag */\n" + plc_memory_storage_block = "".join(plc_memory_storage_lines) + loc_vars_block = "".join(loc_vars_lines) + "/* Located variables -> extension-owned CS104 buffers */\n" "diag_static_block": diag_static_block,
+ "plc_memory_storage_block": plc_memory_storage_block, + "loc_vars_block": loc_vars_block, + "pous_include_block": "", "extern_block": extern_block,
"bindings_rows": bindings_body,
--- a/iec60870/iec60870_runtime.c Thu Apr 16 12:35:36 2026 +0200
+++ b/iec60870/iec60870_runtime.c Wed Apr 22 09:49:11 2026 +0200
@@ -14,8 +14,9 @@
/* 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.) */
+%(plc_memory_storage_block)s struct iec60870_binding {
@@ -43,14 +44,15 @@
+/* PLC-mapped diagnostics only (intrinsic mode uses static storage above). */ static struct iec60870_binding iec60870_bindings[] = {
static const size_t iec60870_binding_count = IEC60870_NUM_BINDINGS_%(locstr)s;
static void iec_rd_inc(iec60870_srv_t *s) {