lpcmanager

Fix so PLC code can use IEC60870 variables
iec60870
8 weeks ago, Dino Kosic
9d54aa733b96
Parents d8b12828e6aa
Children 9e7923ad7ae3
Fix so PLC code can use IEC60870 variables
--- a/.hgignore Thu Apr 16 12:35:36 2026 +0200
+++ b/.hgignore Wed Apr 22 09:49:11 2026 +0200
@@ -3,3 +3,5 @@
syntax: regexp
^.*\.pyc$
^.*\.swp$
+# Default ignored files
+.idea/workspace.xml
--- 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 @@
# -*- 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
+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"),
}
-LOCATION_TYPES = {
- "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")
+_IEC_TO_C_STORAGE = {
+ "BOOL": "IEC_BOOL",
+ "BYTE": "IEC_BYTE",
+ "INT": "IEC_INT",
+ "WORD": "IEC_UINT",
+ "REAL": "IEC_REAL",
+ "UDINT": "UDINT",
+}
+
+
+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 @@
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.
@@ -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 @@
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).
- """
+ """Return (rd_name, wr_name, conn_name, mode) with mode \"plc\" or \"intrinsic\"."""
loc_sources = []
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
entries = []
for offset in range(ioa, ioa + count):
@@ -852,14 +750,18 @@
bindings_rows = []
srv_rows = []
diag_static_lines = []
+ plc_memory_storage_lines = []
+ loc_vars_lines = []
+ loc_vars_seen = set()
def add_extern(iec_type, name):
- key = (iec_type, name)
+ storage = _matiec_located_storage_cname(name)
+ key = ("stor", 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})
+ "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,
- 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
@@ -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
+ else:
+ # 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)
+
srv_rows.append(
"""
{ .loc_label = "%(loc)s",
@@ -948,9 +839,9 @@
.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),
+ .rd_ctr = %(rd_ref)s,
+ .wr_ctr = %(wr_ref)s,
+ .conn_bool = %(cn_ref)s,
.slave = NULL,
.init_st = 0
},""" % {
@@ -969,11 +860,45 @@
"t1": t1,
"t2": t2,
"t3": t3,
- "rd": rd_name,
- "wr": wr_name,
- "cn": conn_name,
+ "rd_ref": rd_ref,
+ "wr_ref": wr_ref,
+ "cn_ref": cn_ref,
})
+ 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:
+ continue
+ tail = int(loc[-1])
+ if tail not in (0, 1, 2):
+ continue
+ pfx = tuple(loc[:-1])
+ if len(pfx) < len(srv_tree_loc):
+ continue
+ if pfx[-len(srv_tree_loc):] != srv_tree_loc:
+ continue
+ nm = str(iecvar["NAME"]).strip()
+ if nm in loc_vars_seen:
+ continue
+ iet = iecvar.get("IEC_TYPE")
+ if tail == 0 and iet == "UDINT":
+ loc_vars_lines.append(
+ "UDINT *%s = &iec60870_diag_rd_%d;\n" % (
+ nm, server_id))
+ loc_vars_seen.add(nm)
+ elif tail == 1 and iet == "UDINT":
+ loc_vars_lines.append(
+ "UDINT *%s = &iec60870_diag_wr_%d;\n" % (
+ nm, server_id))
+ loc_vars_seen.add(nm)
+ elif tail == 2 and iet in ("BOOL", "IEC_BOOL"):
+ loc_vars_lines.append(
+ "IEC_BOOL *%s = &iec60870_diag_conn_%d;\n" % (
+ nm, server_id))
+ loc_vars_seen.add(nm)
+
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():
loc = iecvar["LOC"]
- if len(loc) < 4:
+ if len(loc) < 3:
continue
ioa_v = int(loc[-1])
if ioa_v < ioa0 or ioa_v >= ioa0 + npoints:
continue
- nm = str(iecvar["NAME"])
+ nm = str(iecvar["NAME"]).strip()
iet = iecvar["IEC_TYPE"]
- add_extern(iet, nm)
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:
+ loc_vars_lines.append(
+ "%(ct)s *%(nm)s = &%(sn)s;\n" % {
+ "ct": c_stor, "nm": nm, "sn": stor_name})
+ loc_vars_seen.add(nm)
bindings_rows.append(
- " { %(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 }," % {
"sid": server_id,
"tid": tid,
"ioa": ioa_v,
"isc": is_cmd,
"bk": bk,
- "nm": nm,
+ "sn": stor_name,
})
num_srv = len(servers)
@@ -1017,12 +953,22 @@
diag_static_block = "".join(diag_static_lines)
if diag_static_block:
diag_static_block = (
- "/* Intrinsic diagnostic storage — not mapped to IEC %%MD/%%MX */\n"
+ "/* Intrinsic diagnostic counters / connection flag */\n"
+ diag_static_block)
+ plc_memory_storage_block = "".join(plc_memory_storage_lines)
+ loc_vars_block = "".join(loc_vars_lines)
+ if loc_vars_block:
+ loc_vars_block = (
+ "/* Located variables -> extension-owned CS104 buffers */\n"
+ + loc_vars_block)
+
tpl = {
"locstr": loc_prefix,
"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,
"srv_def": srv_def,
--- 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.) */
%(diag_static_block)s
+%(plc_memory_storage_block)s
+%(loc_vars_block)s
struct iec60870_binding {
int server_index;
int type_id;
@@ -43,14 +44,15 @@
int init_st;
} iec60870_srv_t;
+/* PLC-mapped diagnostics only (intrinsic mode uses static storage above). */
+%(extern_block)s
+
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) {