--- 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 @@
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
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 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(). + while i < len(locstr) and not locstr[i].isdigit(): + 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.""" + want = tuple(target_tuple) + for locdic in iterable_of_locdicts: + loc = locdic.get("LOC") + return str(locdic["NAME"]) +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) + for locdic in iterable_of_locdicts: + loc = locdic.get("LOC") + by_loc[tuple(loc)] = locdic + 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): + if P[-len(srv_loc):] != srv_loc: + if not (d0 and d1 and d2): + 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 + """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. +_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"(?:,\s*(?P<SIZE>[XBWDL])\s*)?" + r",\s*(?P<LOC>[\d,\s]+)\)\s*" +def _parse_located_variables_h_loose(filepath): + Same structure as ProjectController.GetLocations() resdicts, with + whitespace-tolerant LOC lists. + if not filepath or not os.path.isfile(filepath): + with open(filepath, "r") as f: + if "__LOCATED_VAR" not in line: + if line.startswith("//") or line.startswith("/*"): + m = _LOOSE_LOCATED_LINE.search(line) + resdict = m.groupdict() + loc_raw = resdict["LOC"] + loc_raw = loc_raw.replace(" ", "").replace("\t", "") + resdict["LOC"] = tuple(map(int, loc_raw.split(","))) + if not resdict.get("SIZE"): + locations.append(resdict) +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). + 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 [] + 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", "")) + for src in loc_sources: + rd = _find_name_for_loc_tuple(t0, src) + wr = _find_name_for_loc_tuple(t1, src) + cn = _find_name_for_loc_tuple(t2, src) + return rd, wr, cn, "plc" + srv_loc = tuple(srv.GetCurrentLocation()) + for src in loc_sources: + loc = locdic.get("LOC") + if len(loc) != len(srv_loc) + 1: + if loc[:-1] != srv_loc: + name = str(locdic["NAME"]) + return rd, wr, cn, "plc" + for src in loc_sources: + rd, wr, cn = _resolve_diagnostics_by_suffix_tail(srv_loc, merged) + return rd, wr, cn, "plc" + fallback = srv.GetLocations() + rd, wr, cn = _resolve_diagnostics_by_suffix_tail(srv_loc, fallback) + return rd, wr, cn, "plc" + for locdic in fallback: + loc = locdic.get("LOC") + if len(loc) != len(srv_loc) + 1: + if loc[:-1] != srv_loc: + name = str(locdic["NAME"]) + return rd, wr, cn, "plc" + return None, None, None, "intrinsic" @@ -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"] + IEC60870Path = paths.ThirdPartyPath("IEC60870") + tls = _srv_attr_map(ch).get("Use_TLS", False) + if str(tls).lower() in ("true", "1", "yes"): + _("IEC60870: TLS is not supported by the generated " + "CS104 runtime. Disable Use_TLS on server \"%s\".\n") % + ch.BaseParams.getName()) + def add_extern(iec_type, name): + "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, + 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 + 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" % { + 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 = _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 ( + 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_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')
+ { .loc_label = "%(loc)s", + .common_address = %(co)d, + .cot_two_byte = %(cotoa)d, + .apci_k = %(apk)d, .apci_w = %(apw)d, .t0 = %(t0)d, .t1 = %(t1)d, + .t2 = %(t2)d, .t3 = %(t3)d, + .conn_bool = &(%(cn)s), + "cotoa": 1 if cot_has_oa else 0, - iec60870_main = open(c_filename).read() % loc_dict
- f = open(gen_iec60870_c_path, 'w')
+ 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(): + if ioa_v < ioa0 or ioa_v >= ioa0 + npoints: + nm = str(iecvar["NAME"]) + iet = iecvar["IEC_TYPE"] + bk = iec_iec_type_to_bind_kind(iet) + " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bk)d," + " (void *)&(%(nm)s) }," % { + " { -1, 0, 0, 0, 0, NULL }, /* placeholder */") + num_bind = len(bindings_rows) + extern_block = "\n".join(extern_lines) + bindings_body = "\n".join(bindings_rows) + "static iec60870_srv_t iec60870_srv[IEC60870_NUM_SERVERS_%s] = {%s\n};" + % (loc_prefix, "".join(srv_rows))) + diag_static_block = "".join(diag_static_lines) + "/* Intrinsic diagnostic storage — not mapped to IEC %%MD/%%MX */\n" + "diag_static_block": diag_static_block, + "extern_block": extern_block, + "bindings_rows": bindings_body, + "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: + with open(c_src, "r") as f: + c_text = f.read() % tpl + with open(gen_c, "w") as fc: - LDFLAGS.append(" \"-L" + iec60870_path + "\"")
- 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 + '"' --- 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 "iec_types_all.h" +#include "iec60870_common.h" +#include "cs101_information_objects.h" +#include "IEC104_%(locstr)s.h"
-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.) */ +struct iec60870_binding { + int apci_k, apci_w, t0, t1, t2, t3; +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) { +static void iec_wr_inc(iec60870_srv_t *s) { +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()); + QualityDescriptor q = IEC60870_QUALITY_GOOD; + bool val = iec_get_bool_var(b->iec_var); + return (InformationObject)SinglePointInformation_create( + (SinglePointInformation)mem, ioa, val, q); + DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3); + return (InformationObject)DoublePointInformation_create( + (DoublePointInformation)mem, ioa, dv, q); + int sv = (int)*(IEC_INT *)b->iec_var; + return (InformationObject)StepPositionInformation_create( + (StepPositionInformation)mem, ioa, sv, false, q); + float nv = NormalizedValue_fromScaled( + (int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalized_create( + (MeasuredValueNormalized)mem, ioa, nv, q); + int sv = (int)*(IEC_INT *)b->iec_var; + return (InformationObject)MeasuredValueScaled_create( + (MeasuredValueScaled)mem, ioa, sv, q); + float fv = *(IEC_REAL *)b->iec_var; + return (InformationObject)MeasuredValueShort_create( + (MeasuredValueShort)mem, ioa, fv, q); +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); + CS101_ASDU asdu = CS101_ASDU_create(al, false, CS101_COT_INTERROGATED_BY_STATION, oa, + srv->common_address, false, false); + InformationObject_destroy(io); + if (!CS101_ASDU_addInformationObject(asdu, io)) { + InformationObject_destroy(io); + CS101_ASDU_destroy(asdu); + if (!IMasterConnection_sendASDU(conn, asdu)) { + CS101_ASDU_destroy(asdu); + CS101_ASDU_destroy(asdu); +static bool iec_interrogation_handler(void *parameter, IMasterConnection connection, CS101_ASDU asdu, uint8_t qoi) { + iec60870_srv_t *srv = (iec60870_srv_t *)parameter; + size_t idx = (size_t)(srv - iec60870_srv); + 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) + if ((size_t)b->server_index != idx) + iec_send_monitor(connection, srv, b); +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); + struct iec60870_binding *match = NULL; + int ioa = InformationObject_getObjectAddress(io); + for (i = 0; i < iec60870_binding_count; i++) { + if (iec60870_bindings[i].server_index < 0) + if ((size_t)iec60870_bindings[i].server_index != srv_idx) + if (!iec60870_bindings[i].is_command) + if (iec60870_bindings[i].ioa == ioa && iec60870_bindings[i].type_id == (int)tid) { + match = &iec60870_bindings[i]; + SingleCommand sc = (SingleCommand)io; + iec_set_bool_var(match->iec_var, SingleCommand_getState(sc)); + DoubleCommand dc = (DoubleCommand)io; + *(IEC_BYTE *)match->iec_var = (IEC_BYTE)(DoubleCommand_getState(dc) & 0xff); + StepCommand sc = (StepCommand)io; + *(IEC_BYTE *)match->iec_var = (IEC_BYTE)((int)StepCommand_getState(sc) & 0xff); + SetpointCommandNormalized sn = (SetpointCommandNormalized)io; + float fv = SetpointCommandNormalized_getValue(sn); + int scv = NormalizedValue_toScaled(fv); + *(IEC_UINT *)match->iec_var = (uint16_t)scv; + SetpointCommandScaled ss = (SetpointCommandScaled)io; + *(IEC_INT *)match->iec_var = (int16_t)SetpointCommandScaled_getValue(ss); + SetpointCommandShort sf = (SetpointCommandShort)io; + *(IEC_REAL *)match->iec_var = SetpointCommandShort_getValue(sf); + IMasterConnection_sendACT_CON(connection, asdu, false); +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); +static void iec_connection_event(void *parameter, IMasterConnection connection, CS104_PeerConnectionEvent event) { + iec60870_srv_t *srv = (iec60870_srv_t *)parameter; + 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");
- // 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); + fprintf(stderr, "IEC60870 %%s: CS104_Slave_create failed\\n", s->loc_label); + 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); + 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); - // 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);
+ __cleanup_%(locstr)s(); - // TODO: initialize resources, threads, state
+void __retrieve_%(locstr)s(void) { + 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
+ for (si = 0; si < IEC60870_NUM_SERVERS_%(locstr)s; si++) { + iec60870_srv_t *s = &iec60870_srv[si]; + CS104_Slave_stop(s->slave); + CS104_Slave_destroy(s->slave); --- 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 */
+#ifndef IEC60870_RUNTIME_%(locstr)s_H +#define IEC60870_RUNTIME_%(locstr)s_H
\ 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 @@
+"""Helpers for IEC 60870-5-104 Beremiz plugin (CS104 server runtime generation).""" +from __future__ import absolute_import + from six.moves import xrange +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: + for ch in element["children"]: + if ch["name"] == attr_name: + 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).""" + return m.get(iec_type, 0)