--- a/LPCExtension.py Tue Mar 31 11:06:21 2026 +0200
+++ b/LPCExtension.py Wed May 06 16:27:02 2026 +0200
@@ -48,6 +48,7 @@
('lpchmi', _('Smarteh HMI'), _('Create customized HMI'), 'lpchmi.LPCHMI'),
('bacnet', _('Bacnet support'), _('Map located variables over Bacnet'), 'LPCBACnet.RootClass'),
('modbus', _('Modbus'), _('Map located variables over Modbus'), 'LPCModbus.RootClass'),
+ ('iec60870', _('IEC 60870-5-104'), _('Map located variables over IEC 60870-5-104'), 'LPCIEC60870.RootClass'), ('mqtt', _('MQTT client'), _('Map MQTT topics as located variables'), 'LPCMQTT.MQTTClient'),
('LPCBus', _('LPC bus'), _('Support for Smarteh modules'), 'LPCBus.LPCBus'),
('CanOpen', _('CANOpen'), _('Support for CANopen'), 'LPCCanFestival.LPCCanOpen')]
@@ -167,7 +168,7 @@
old_ThirdPartyPath = paths.ThirdPartyPath
def ThirdPartyPath(name, *args):
- if name in ["BACnet", "Modbus", "CanFestival-3"]:
+ if name in ["BACnet", "Modbus", "CanFestival-3", "IEC60870"]: res = old_ThirdPartyPath(name)
res = os.path.join(res, arch)
elif name == "paho.mqtt.c":
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iec60870/__init__.py Wed May 06 16:27:02 2026 +0200
@@ -0,0 +1,3 @@
+from __future__ import absolute_import --- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iec60870/iec60870.py Wed May 06 16:27:02 2026 +0200
@@ -0,0 +1,1294 @@
+from __future__ import absolute_import +from ConfigTreeNode import ConfigTreeNode +from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_MEMORY +import util.paths as paths +from iec60870.iec60870_utils import iec_iec_type_to_bind_kind +# (type_id, IEC_type, datasize, direction, size_code, description) +# direction: "I" monitor / process info to master, "Q" command / control from master +# size_code: memory tree prefix — X bit, B byte, W word, D dword (see list.txt) +# Time-tagged ASDUs use the same IEC cell mapping as the corresponding NA_1 type; +# CP24/CP56 time tags are carried only in the protocol, not in the PLC binding. + # Monitoring — process information (1–21, 30–40) + "M_SP_NA_1 - Single-point information": (1, "BOOL", 1, "I", "X", + "Single-point information"), + "M_SP_TA_1 - Single-point information with time tag": (2, "BOOL", 1, "I", "X", + "Single-point information with time tag"), + "M_DP_NA_1 - Double-point information": (3, "BYTE", 8, "I", "B", + "Double-point information"), + "M_DP_TA_1 - Double-point information with time tag": (4, "BYTE", 8, "I", "B", + "Double-point information with time tag"), + "M_ST_NA_1 - Step position information": (5, "INT", 16, "I", "W", + "Step position information"), + "M_ST_TA_1 - Step position information with time tag": (6, "INT", 16, "I", "W", + "Step position information with time tag"), + "M_BO_NA_1 - Bitstring of 32 bits": (7, "UDINT", 32, "I", "D", + "Bitstring of 32 bits"), + "M_BO_TA_1 - Bitstring of 32 bits with time tag": (8, "UDINT", 32, "I", "D", + "Bitstring of 32 bits with time tag"), + "M_ME_NA_1 - Measured value, normalized": (9, "WORD", 16, "I", "W", + "Measured value, normalized"), + "M_ME_TA_1 - Measured value, normalized with time tag": (10, "WORD", 16, "I", "W", + "Measured value, normalized with time tag"), + "M_ME_NB_1 - Measured value, scaled": (11, "INT", 16, "I", "W", + "Measured value, scaled"), + "M_ME_TB_1 - Measured value, scaled with time tag": (12, "INT", 16, "I", "W", + "Measured value, scaled with time tag"), + "M_ME_NC_1 - Measured value, short floating point": (13, "REAL", 32, "I", "D", + "Measured value, short floating point"), + "M_ME_TC_1 - Measured value, short floating point with time tag": ( + 14, "REAL", 32, "I", "D", + "Measured value, short floating point with time tag"), + "M_IT_NA_1 - Integrated totals": (15, "UDINT", 32, "I", "D", + "M_IT_TA_1 - Integrated totals with time tag": (16, "UDINT", 32, "I", "D", + "Integrated totals with time tag"), + "M_EP_TA_1 - Event of protection equipment with time tag": ( + 17, "UDINT", 32, "I", "D", + "Event of protection equipment with time tag"), + "M_EP_TB_1 - Packed start events of protection equipment with time tag": ( + 18, "UDINT", 32, "I", "D", + "Packed start events of protection equipment with time tag"), + "M_EP_TC_1 - Packed output circuit info of protection equipment with time tag": ( + 19, "UDINT", 32, "I", "D", + "Packed output circuit info of protection equipment with time tag"), + "M_PS_NA_1 - Packed single-point info with status change detection": ( + 20, "UDINT", 32, "I", "D", + "Packed single-point info with status change detection"), + "M_ME_ND_1 - Measured value, normalized without quality descriptor": ( + 21, "WORD", 16, "I", "W", + "Measured value, normalized without quality descriptor"), + "M_SP_TB_1 - Single-point information with time tag CP56Time2a": ( + 30, "BOOL", 1, "I", "X", + "Single-point information with time tag CP56Time2a"), + "M_DP_TB_1 - Double-point information with time tag CP56Time2a": ( + 31, "BYTE", 8, "I", "B", + "Double-point information with time tag CP56Time2a"), + "M_ST_TB_1 - Step position information with time tag CP56Time2a": ( + 32, "INT", 16, "I", "W", + "Step position information with time tag CP56Time2a"), + "M_BO_TB_1 - Bitstring of 32 bits with time tag CP56Time2a": ( + 33, "UDINT", 32, "I", "D", + "Bitstring of 32 bits with time tag CP56Time2a"), + "M_ME_TD_1 - Measured value, normalized with time tag CP56Time2a": ( + 34, "WORD", 16, "I", "W", + "Measured value, normalized with time tag CP56Time2a"), + "M_ME_TE_1 - Measured value, scaled with time tag CP56Time2a": ( + 35, "INT", 16, "I", "W", + "Measured value, scaled with time tag CP56Time2a"), + "M_ME_TF_1 - Measured value, short floating point with time tag CP56Time2a": ( + 36, "REAL", 32, "I", "D", + "Measured value, short floating point with time tag CP56Time2a"), + "M_IT_TB_1 - Integrated totals with time tag CP56Time2a": ( + 37, "UDINT", 32, "I", "D", + "Integrated totals with time tag CP56Time2a"), + "M_EP_TD_1 - Event of protection equipment with time tag CP56Time2a": ( + 38, "UDINT", 32, "I", "D", + "Event of protection equipment with time tag CP56Time2a"), + "M_EP_TE_1 - Packed start events of protection equipment with time tag CP56Time2a": ( + 39, "UDINT", 32, "I", "D", + "Packed start events with time tag CP56Time2a"), + "M_EP_TF_1 - Packed output circuit info of protection equipment with time tag CP56Time2a": ( + 40, "UDINT", 32, "I", "D", + "Packed output circuit info with time tag CP56Time2a"), + # Control — commands (45–51, 58–64) + "C_SC_NA_1 - Single command": (45, "BOOL", 1, "Q", "X", "Single command"), + "C_DC_NA_1 - Double command": (46, "BYTE", 8, "Q", "B", "Double command"), + "C_RC_NA_1 - Regulating step command": (47, "BYTE", 8, "Q", "B", + "Regulating step command"), + "C_SE_NA_1 - Set-point command, normalized value": (48, "WORD", 16, "Q", "W", + "Set-point command, normalized value"), + "C_SE_NB_1 - Set-point command, scaled value": (49, "INT", 16, "Q", "W", + "Set-point command, scaled value"), + "C_SE_NC_1 - Set-point command, short floating point value": ( + 50, "REAL", 32, "Q", "D", + "Set-point command, short floating point value"), + "C_BO_NA_1 - Bitstring of 32 bits": (51, "UDINT", 32, "Q", "D", + "Bitstring of 32 bits"), + "C_SC_TA_1 - Single command with time tag CP56Time2a": ( + 58, "BOOL", 1, "Q", "X", + "Single command with time tag CP56Time2a"), + "C_DC_TA_1 - Double command with time tag CP56Time2a": ( + 59, "BYTE", 8, "Q", "B", + "Double command with time tag CP56Time2a"), + "C_RC_TA_1 - Regulating step command with time tag CP56Time2a": ( + 60, "BYTE", 8, "Q", "B", + "Regulating step command with time tag CP56Time2a"), + "C_SE_TA_1 - Set-point command, normalized with time tag CP56Time2a": ( + 61, "WORD", 16, "Q", "W", + "Set-point command, normalized with time tag CP56Time2a"), + "C_SE_TB_1 - Set-point command, scaled with time tag CP56Time2a": ( + 62, "INT", 16, "Q", "W", + "Set-point command, scaled with time tag CP56Time2a"), + "C_SE_TC_1 - Set-point command, short floating point with time tag CP56Time2a": ( + 63, "REAL", 32, "Q", "D", + "Set-point command, short floating point with time tag CP56Time2a"), + "C_BO_TA_1 - Bitstring of 32 bits with time tag CP56Time2a": ( + 64, "UDINT", 32, "Q", "D", + "Bitstring of 32 bits with time tag CP56Time2a"), + "M_EI_NA_1 - End of initialization (monitoring)": (70, "BOOL", 1, "I", "X", + "End of initialization (monitoring)"), + "C_IC_NA_1 - Interrogation command": (100, "BOOL", 1, "Q", "X", + "Interrogation command"), + "C_CI_NA_1 - Counter interrogation command": (101, "UDINT", 32, "Q", "D", + "Counter interrogation command"), + "C_RD_NA_1 - Read command": (102, "BOOL", 1, "Q", "X", "Read command"), + "C_CS_NA_1 - Clock synchronization command": (103, "UDINT", 32, "Q", "D", + "Clock synchronization command"), + "C_TS_NA_1 - Test command": (104, "BOOL", 1, "Q", "X", "Test command"), + "C_RP_NA_1 - Reset process command": (105, "BOOL", 1, "Q", "X", + "Reset process command"), + "C_CD_NA_1 - Delay acquisition command": (106, "UDINT", 32, "Q", "D", + "Delay acquisition command"), + "C_TS_TA_1 - Test command with time tag CP56Time2a": ( + 107, "BOOL", 1, "Q", "X", + "Test command with time tag CP56Time2a"), + "P_ME_NA_1 - Parameter of measured value, normalized": ( + 110, "WORD", 16, "Q", "W", + "Parameter of measured value, normalized"), + "P_ME_NB_1 - Parameter of measured value, scaled": ( + 111, "INT", 16, "Q", "W", + "Parameter of measured value, scaled"), + "P_ME_NC_1 - Parameter of measured value, short floating point": ( + 112, "REAL", 32, "Q", "D", + "Parameter of measured value, short floating point"), + "P_AC_NA_1 - Parameter activation": (113, "BYTE", 8, "Q", "B", + "Parameter activation"), + # File transfer (PLC mapping is a minimal cell per IOA; payloads may need many IOAs) + "F_FR_NA_1 - File ready": (120, "UDINT", 32, "I", "D", "File ready"), + "F_SR_NA_1 - Section ready": (121, "UDINT", 32, "I", "D", "Section ready"), + "F_SC_NA_1 - Call directory, select file, call file, call section": ( + 122, "BYTE", 8, "Q", "B", + "Call directory, select file, call file, call section"), + "F_LS_NA_1 - Last section, last segment": (123, "UDINT", 32, "I", "D", + "Last section, last segment"), + "F_AF_NA_1 - Ack file, ack section": (124, "BYTE", 8, "I", "B", + "Ack file, ack section"), + "F_SG_NA_1 - Segment": (125, "UDINT", 32, "I", "D", "Segment"), + "F_DR_NA_1 - Directory": (126, "UDINT", 32, "I", "D", "Directory"), + "F_SC_NB_1 - QueryLog (request archive file)": (127, "BYTE", 8, "Q", "B", + "QueryLog (request archive file)"), +# XSD fragment for the shared IEC 60870-5-104 connection parameters +# (used in both server and client node XSD definitions) +_IEC60870_CONN_PARAMS_XSD = """\ + <xsd:attribute name="APCI_k" use="optional" default="12"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="32767"/> + <xsd:attribute name="APCI_w" use="optional" default="8"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="32767"/> + <xsd:attribute name="Timeout_t0" use="optional" default="10"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="255"/> + <xsd:attribute name="Timeout_t1" use="optional" default="15"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="255"/> + <xsd:attribute name="Timeout_t2" use="optional" default="10"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="255"/> + <xsd:attribute name="Timeout_t3" use="optional" default="20"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="255"/> + <xsd:attribute name="Use_TLS" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="CA_Size" use="optional" default="2"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="2"/> + <xsd:attribute name="IOA_Size" use="optional" default="3"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="3"/> + <xsd:attribute name="COT_Has_OA" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="OA_Value" use="optional" default="10"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="255"/> + <xsd:attribute name="Use_Local_Timezone" type="xsd:boolean" use="optional" default="true"/> +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 _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 + 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 +# 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 _matiec_located_storage_cname(iec_name): + """C identifier matiec uses with __INIT_LOCATED: extern TYPE *__SYM. + LOCATED_VARIABLES.h NAME is often already '__MD4_0_1' — do not add another + n = str(iec_name).strip() +def _c_addr_expr_symbol(addr_expr): + """Turn '&(__MD4_0_1)' into '__MD4_0_1' for comparisons.""" + s = str(addr_expr).strip() + if s.startswith("&(") and s.endswith(")"): +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 _resolve_server_diagnostic_names(srv, locations, project_locations): + """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: + 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" +def _normalize_loc_tuple(loc): + """LOC from parsers may be list or tuple; normalize for comparisons.""" + return tuple(int(x) for x in loc) + except (TypeError, ValueError): +def _iec60870_loc_is_server_intrinsic_diag_slot(srv_loc, loc_tuple): + True when loc_tuple is the IEC60870 server's own diagnostic tier: + %%MD…0 / %%MD…1 / %%MX…2 directly under srv (length len(srv_loc)+1). + ConfigTreeNode.GetLocations() uses prefix matching on LOC tuples, so a + datapoint at e.g. …4.0.0 incorrectly receives %%MD4.0.0 alongside %%MX4.0.0.0. + Datapoint IOAs live one segment deeper (…4.0.0.<ioa>), so they never match. + if not srv_loc or not loc_tuple: + srv_loc = tuple(srv_loc) + loc_tuple = tuple(loc_tuple) + if len(loc_tuple) != len(srv_loc) + 1: + if loc_tuple[:-1] != srv_loc: + return loc_tuple[-1] in (0, 1, 2) +def _iec_type_is_udint(iet): + return str(iet).upper().replace(" ", "") == "UDINT" +def _iec_type_is_bool(iet): + return str(iet).upper().replace(" ", "") in ("BOOL", "IEC_BOOL") +def _intrinsic_diag_matiec_storage_symbols(srv, locations=None, project_locs=None): + Map diagnostics (%MD…0/1, %MX…2 under this server) to matiec storage names (__…). + Scans PLC located-var lists first — srv.GetLocations() can miss rows when + LOC is a list and ConfigTreeNode compares with a tuple prefix. + srv_tree_loc = tuple(srv.GetCurrentLocation()) + rd_stor = wr_stor = cn_stor = None + for src in (locations, project_locs, srv.GetLocations()): + loc = _normalize_loc_tuple(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 tuple(pfx[-len(srv_tree_loc):]) != srv_tree_loc: + nm = str(iecvar.get("NAME", "")).strip() + stor = _matiec_located_storage_cname(nm) + iet = iecvar.get("IEC_TYPE") + if tail == 0 and _iec_type_is_udint(iet): + rd_stor = rd_stor or stor + elif tail == 1 and _iec_type_is_udint(iet): + wr_stor = wr_stor or stor + elif tail == 2 and _iec_type_is_bool(iet): + cn_stor = cn_stor or stor + return rd_stor, wr_stor, cn_stor +class _DataPointPlug(object): + XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?> + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <xsd:element name="IEC60870DataPoint"> + <xsd:attribute name="ASDU_Type" type="xsd:string" use="optional" default="M_SP_NA_1 - Single-point information"/> + <xsd:attribute name="IOA" use="optional" default="0"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="16777215"/> + <xsd:attribute name="Nr_of_Points" use="optional" default="1"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="65535"/> + def GetParamsAttributes(self, path=None): + infos = ConfigTreeNode.GetParamsAttributes(self, path=path) + if element["name"] == "IEC60870DataPoint": + for child in element["children"]: + if child["name"] == "ASDU_Type": + _list = sorted(iec60870_asdu_types.keys()) + def GetVariableLocationTree(self): + current_location = self.GetCurrentLocation() + name = self.BaseParams.getName() + ioa = self.GetParamsAttributes()[0]["children"][1]["value"] + count = self.GetParamsAttributes()[0]["children"][2]["value"] + asdu_type_str = self.GetParamsAttributes()[0]["children"][0]["value"] + type_id, datatype, datasize, direction, size_code, desc = \ + iec60870_asdu_types[asdu_type_str] + # Extension-owned %M memory (Modbus-style): no BSP %IX/%QX required. + loc_type = LOCATION_VAR_MEMORY + for offset in range(ioa, ioa + count): + "name": desc + " IOA " + str(offset), + "var_name": "IEC60870_" + str(type_id) + "_" + str(offset), + "location": size_code + ".".join( + [str(i) for i in current_location]) + "." + str(offset), + "type": LOCATION_CONFNODE, + [str(i) for i in current_location]) + ".x", + def CTNGenerate_C(self, buildpath, locations): +# C O N T R O L L E D S T A T I O N (Server) +class _IEC60870ServerPlug(object): + XSD = ("""<?xml version="1.0" encoding="ISO-8859-1" ?> + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <xsd:element name="IEC60870ServerNode"> + <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="Local_IP_Address" type="xsd:string" use="optional" default="#ANY#"/> + <xsd:attribute name="Local_Port_Number" type="xsd:string" use="optional" default="2404"/> + <xsd:attribute name="Common_Address" use="optional" default="1"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="65534"/> + + _IEC60870_CONN_PARAMS_XSD + + CTNChildrenTypes = [("IEC60870DataPoint", _DataPointPlug, "Data Point")] + PlugType = "IEC60870Server" + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.IEC60870ServerNode.setConfiguration_Name( + "IEC60870 Server " + loc_str) + def GetNodeCount(self): + def GetConfigName(self): + return self.IEC60870ServerNode.getConfiguration_Name() + def GetIPServerPortNumbers(self): + port = self.IEC60870ServerNode.getLocal_Port_Number() + addr = self.IEC60870ServerNode.getLocal_IP_Address() + return [(self.GetCurrentLocation(), addr, port)] + def GetVariableLocationTree(self): + current_location = self.GetCurrentLocation() + name = self.BaseParams.getName() + "name": "Read Request Counter", + "type": LOCATION_VAR_MEMORY, + "var_name": "rd_request_counter", + "location": "D" + ".".join( + [str(i) for i in current_location]) + ".0", + "description": "IEC60870 read request counter", + "name": "Write Request Counter", + "type": LOCATION_VAR_MEMORY, + "var_name": "wr_request_counter", + "location": "D" + ".".join( + [str(i) for i in current_location]) + ".1", + "description": "IEC60870 write request counter", + "name": "Connection Active Flag", + "type": LOCATION_VAR_MEMORY, + "var_name": "conn_active_flag", + "location": "X" + ".".join( + [str(i) for i in current_location]) + ".2", + "description": "IEC60870 connection active flag", + for child in self.IECSortedChildren(): + entries.append(child.GetVariableLocationTree()) + "type": LOCATION_CONFNODE, + [str(i) for i in current_location]) + ".x", + def CTNGenerate_C(self, buildpath, locations): +# C O N T R O L L I N G S T A T I O N (Client) +# TEMPORARY: client node hidden from IDE — commented out in RootClass.CTNChildrenTypes. +# Class kept here so existing projects / configs that reference IEC60870Client still load. +class _IEC60870ClientPlug(object): + XSD = ("""<?xml version="1.0" encoding="ISO-8859-1" ?> + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <xsd:element name="IEC60870ClientNode"> + <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="Remote_IP_Address" type="xsd:string" use="optional" default="localhost"/> + <xsd:attribute name="Remote_Port_Number" type="xsd:string" use="optional" default="2404"/> + <xsd:attribute name="Common_Address" use="optional" default="1"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="65534"/> + <xsd:attribute name="Polling_Interval_ms" use="optional" default="1000"> + <xsd:restriction base="xsd:unsignedLong"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="2147483647"/> + + _IEC60870_CONN_PARAMS_XSD + + CTNChildrenTypes = [("IEC60870DataPoint", _DataPointPlug, "Data Point")] + PlugType = "IEC60870Client" + loc_str = ".".join(map(str, self.GetCurrentLocation())) + self.IEC60870ClientNode.setConfiguration_Name( + "IEC60870 Client " + loc_str) + def GetNodeCount(self): + def GetConfigName(self): + return self.IEC60870ClientNode.getConfiguration_Name() + def GetVariableLocationTree(self): + current_location = self.GetCurrentLocation() + name = self.BaseParams.getName() + "name": "Connection Status", + "type": LOCATION_VAR_MEMORY, + "var_name": "conn_status", + "location": "B" + ".".join( + [str(i) for i in current_location]) + ".0", + "description": "Connection status (0=disconnected, 1=connected, " + "2=connecting, 3=error)", + "name": "Interrogation Trigger", + "type": LOCATION_VAR_MEMORY, + "var_name": "interrogation_trigger", + "location": "X" + ".".join( + [str(i) for i in current_location]) + ".1", + "description": "Trigger general interrogation", + for child in self.IECSortedChildren(): + entries.append(child.GetVariableLocationTree()) + "type": LOCATION_CONFNODE, + [str(i) for i in current_location]) + ".x", + def CTNGenerate_C(self, buildpath, locations): +def _lt_to_str(loctuple): + return '.'.join(map(str, loctuple)) +class RootClass(object): + XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?> + <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"> + <xsd:element name="IEC60870Root"> + <xsd:attribute name="MaxRemoteClients" use="optional" default="10"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="65535"/> + ("IEC60870Server", _IEC60870ServerPlug, "IEC 60870-5-104 Server"), + # ("IEC60870Client", _IEC60870ClientPlug, "IEC 60870-5-104 Client"), + def GetNodeCount(self): + max_remote_clients = self.GetParamsAttributes()[0]["children"][0]["value"] + total = (max_remote_clients, 0) + for child in self.IECSortedChildren(): + x1 + x2 for x1, x2 in zip(total, child.GetNodeCount())) + def GetIPServerPortNumbers(self): + for child in self.IECSortedChildren(): + if child.CTNType == "IEC60870Server": + port_numbers.extend(child.GetIPServerPortNumbers()) + def GetConfigNames(self): + for child in self.IECSortedChildren(): + (child.GetCurrentLocation(), child.GetConfigName())) + def CTNGenerate_C(self, buildpath, locations): + for CTNInstance in self.GetCTRoot().IterChildren(): + if CTNInstance.CTNType == "iec60870": + node_config_names.extend(CTNInstance.GetConfigNames()) + for i in range(0, len(node_config_names) - 1): + for j in range(i + 1, len(node_config_names)): + if node_config_names[i][1] == node_config_names[j][1]: + "Error: IEC60870 plugin nodes %%{a1}.x and %%{a2}.x " + "use the same Configuration_Name \"{a3}\".\n" + a1=_lt_to_str(node_config_names[i][0]), + a2=_lt_to_str(node_config_names[j][0]), + a3=node_config_names[j][1]) + self.FatalError(error_message) + for CTNInstance in self.GetCTRoot().IterChildren(): + if CTNInstance.CTNType == "iec60870": + ip_ports.extend(CTNInstance.GetIPServerPortNumbers()) + for loc1, addr1, port1 in ip_ports[:-1]: + for loc2, addr2, port2 in ip_ports[i:]: + if (port1 == port2) and ( + or (addr1 in ("", "*", "#ANY#")) + or (addr2 in ("", "*", "#ANY#")) + "Error: IEC60870 plugin nodes %%{a1}.x and %%{a2}.x " + "use same port number \"{a3}\" on the same " + "(or overlapping) network interfaces " + "\"{a4}\" and \"{a5}\".\n" + a1=_lt_to_str(loc1), a2=_lt_to_str(loc2), + a3=port1, a4=addr1, a5=addr2) + self.FatalError(error_message) + 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()) + diag_intrinsic_global_lines = [] + diag_matiec_weak_seen = set() + diag_weak_backing_by_ptrsym = {} + plc_memory_storage_lines = [] + weak_loc_backing_id = [0] + def add_matiec_weak_located_ptr(pointee_c_type, ptr_symbol): + matiec __INIT_LOCATED(TYPE, __SYM, ...) uses extern TYPE *__SYM (pointer). + Emit static backing + TYPE *__SYM IEC60870_WEAK = &backing; + Returns backing identifier for constant (&backing) static initializers. + key = ("weakptr", ptr_symbol) + if key in diag_matiec_weak_seen: + return diag_weak_backing_by_ptrsym.get(ptr_symbol) + diag_matiec_weak_seen.add(key) + weak_loc_backing_id[0] += 1 + bk = "iec60870_weakbk_%d" % weak_loc_backing_id[0] + zinit = _iec_c_storage_zero_init(pointee_c_type) + diag_intrinsic_global_lines.append( + "static %(pt)s %(bk)s = %(z)s;\n" + "%(pt)s *%(ps)s IEC60870_WEAK = &%(bk)s;\n" % { + diag_weak_backing_by_ptrsym[ptr_symbol] = bk + def add_matiec_weak_located_ptr_for_plc_var(iec_decl_type, plc_base_name): + ptr_sym = _matiec_located_storage_cname(plc_base_name) + pt = "BOOL" if iec_decl_type == "BOOL" else iec_decl_type + return add_matiec_weak_located_ptr(pt, ptr_sym) + 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) + rd_stat = "iec60870_diag_rd_%d" % server_id + wr_stat = "iec60870_diag_wr_%d" % server_id + cn_stat = "iec60870_diag_conn_%d" % server_id + if diag_mode == "intrinsic": + rd_plc, wr_plc, cn_plc = _intrinsic_diag_matiec_storage_symbols( + srv, locations, project_locs) + add_matiec_weak_located_ptr("UDINT", rd_plc) if rd_plc else None) + add_matiec_weak_located_ptr("UDINT", wr_plc) if wr_plc else None) + add_matiec_weak_located_ptr("BOOL", cn_plc) if cn_plc else None) + diag_static_lines.append("static UDINT %s = 0U;\n" % rd_stat) + diag_static_lines.append("static UDINT %s = 0U;\n" % wr_stat) + diag_static_lines.append( + "static IEC_BOOL %s = (IEC_BOOL)0;\n" % cn_stat) + rd_ref_sym = rd_plc if rd_plc else rd_stat + wr_ref_sym = wr_plc if wr_plc else wr_stat + cn_ref_sym = cn_plc if cn_plc else cn_stat + rd_ref = "&(%s)" % rd_bk if rd_bk else "&(%s)" % rd_ref_sym + wr_ref = "&(%s)" % wr_bk if wr_bk else "&(%s)" % wr_ref_sym + cn_ref = "&(%s)" % cn_bk if cn_bk else "&(%s)" % cn_ref_sym + rd_bk = add_matiec_weak_located_ptr_for_plc_var("UDINT", rd_name) + wr_bk = add_matiec_weak_located_ptr_for_plc_var("UDINT", wr_name) + cn_bk = add_matiec_weak_located_ptr_for_plc_var("BOOL", conn_name) + rd_ref = "&(%s)" % rd_bk + wr_ref = "&(%s)" % wr_bk + cn_ref = "&(%s)" % cn_bk + srv_diag_scalars = set() + if diag_mode == "intrinsic": + for sym in (rd_plc, wr_plc, cn_plc): + srv_diag_scalars.add(sym) + for plc_nm in (rd_name, wr_name, conn_name): + srv_diag_scalars.add(_matiec_located_storage_cname(plc_nm)) + 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()))) + { .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_ref)s, + "cotoa": 1 if cot_has_oa else 0, + 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 + srv_loc_tuple = tuple(srv.GetCurrentLocation()) + for iecvar in dp.GetLocations(): + loc_t = _normalize_loc_tuple(loc) + if _iec60870_loc_is_server_intrinsic_diag_slot( + if ioa_v < ioa0 or ioa_v >= ioa0 + npoints: + nm = str(iecvar["NAME"]).strip() + iet = iecvar["IEC_TYPE"] + bk = iec_iec_type_to_bind_kind(iet) + bind_idx = len(bindings_rows) + c_stor = _iec_c_storage_type(iet) + if nm in srv_diag_scalars: + weak_bk = diag_weak_backing_by_ptrsym.get(nm) + _("IEC60870 internal: diagnostic %(sym)s has " + "no generated backing.\n") % {"sym": nm}) + " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bkind)d, " + "(void *)&%(weak_bk)s }," % { + 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 *)&%(sn)s }," % { + " { -1, 0, 0, 0, 0, NULL }, /* placeholder */") + num_bind = len(bindings_rows) + bindings_body = "\n".join(bindings_rows) + "static iec60870_srv_t iec60870_srv[IEC60870_NUM_SERVERS_%s] = {%s\n};" + % (loc_prefix, "".join(srv_rows))) + diag_intrinsic_global_block = "".join(diag_intrinsic_global_lines) + if diag_intrinsic_global_block: + diag_intrinsic_global_block = ( + "/* IEC60870 diagnostics: weak located pointers (matiec extern TYPE *__SYM) */\n" + + diag_intrinsic_global_block) + diag_static_block = "".join(diag_static_lines) + "/* 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_intrinsic_global_block": diag_intrinsic_global_block, + "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, + "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' + IEC60870Path + '"') + LDFLAGS.append(' "' + os.path.join(IEC60870Path, "liblib60870.a") + '"') + LDFLAGS.append(' "-lpthread"') + cflags = ' -I"' + IEC60870Path + '"' \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iec60870/iec60870_runtime.c Wed May 06 16:27:02 2026 +0200
@@ -0,0 +1,667 @@
+/* Generated IEC 60870-5-104 (CS104) server runtime — instance %(locstr)s */ +#include "iec_types_all.h" +#include "iec60870_common.h" +#include "cs104_slave.h" +#include "cs101_information_objects.h" +#include "IEC104_%(locstr)s.h" +#if defined(__GNUC__) || defined(__clang__) +#define IEC60870_WEAK __attribute__((weak)) +/* Referenced from __init_* before definition; silences -Wimplicit-function-declaration */ +int __cleanup_%(locstr)s(void); +%(diag_intrinsic_global_block)s +%(plc_memory_storage_block)s +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; +/* CP24/CP56 helpers for time-tagged ASDUs (wall-clock; PLC does not drive protocol timestamps). + * Buffers are caller-provided so concurrent iec_make_monitor_io() calls do not share static state. */ +static CP56Time2a iec_wall_cp56_buf(struct sCP56Time2a *buf) { + CP56Time2a t = (CP56Time2a)buf; + CP56Time2a_createFromMsTimestamp(t, (uint64_t)time(NULL) * 1000ULL); +static CP24Time2a iec_zero_cp24_buf(struct sCP24Time2a *buf) { + memset(buf, 0, sizeof(*buf)); + return (CP24Time2a)buf; +static CP16Time2a iec_zero_cp16_buf(struct sCP16Time2a *buf) { + memset(buf, 0, sizeof(*buf)); + return (CP16Time2a)buf; +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; + struct sCP56Time2a cp56_wall; + struct sCP24Time2a cp24_z; + struct sCP16Time2a cp16_z; + struct sBinaryCounterReading bcr_st; + BinaryCounterReading bcr; + QualityDescriptorP qdp; + struct sStatusAndStatusChangeDetection scd_st; + StatusAndStatusChangeDetection scd; + return (InformationObject)SinglePointInformation_create( + (SinglePointInformation)mem, ioa, iec_get_bool_var(b->iec_var), q); + return (InformationObject)SinglePointWithCP24Time2a_create( + (SinglePointWithCP24Time2a)mem, ioa, iec_get_bool_var(b->iec_var), q, + iec_zero_cp24_buf(&cp24_z)); + return (InformationObject)SinglePointWithCP56Time2a_create( + (SinglePointWithCP56Time2a)mem, ioa, iec_get_bool_var(b->iec_var), q, + iec_wall_cp56_buf(&cp56_wall)); + DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3); + return (InformationObject)DoublePointInformation_create( + (DoublePointInformation)mem, ioa, dv, q); + DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3); + return (InformationObject)DoublePointWithCP24Time2a_create( + (DoublePointWithCP24Time2a)mem, ioa, dv, q, iec_zero_cp24_buf(&cp24_z)); + DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3); + return (InformationObject)DoublePointWithCP56Time2a_create( + (DoublePointWithCP56Time2a)mem, ioa, dv, q, iec_wall_cp56_buf(&cp56_wall)); + return (InformationObject)StepPositionInformation_create( + (StepPositionInformation)mem, ioa, (int)*(IEC_INT *)b->iec_var, false, q); + return (InformationObject)StepPositionWithCP24Time2a_create( + (StepPositionWithCP24Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, false, q, + iec_zero_cp24_buf(&cp24_z)); + return (InformationObject)StepPositionWithCP56Time2a_create( + (StepPositionWithCP56Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, false, q, + iec_wall_cp56_buf(&cp56_wall)); + return (InformationObject)BitString32_createEx( + (BitString32)mem, ioa, (uint32_t)*(UDINT *)b->iec_var, q); + return (InformationObject)Bitstring32WithCP24Time2a_createEx( + (Bitstring32WithCP24Time2a)mem, ioa, (uint32_t)*(UDINT *)b->iec_var, q, + iec_zero_cp24_buf(&cp24_z)); + return (InformationObject)Bitstring32WithCP56Time2a_createEx( + (Bitstring32WithCP56Time2a)mem, ioa, (uint32_t)*(UDINT *)b->iec_var, q, + iec_wall_cp56_buf(&cp56_wall)); + float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalized_create( + (MeasuredValueNormalized)mem, ioa, nv, q); + float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalizedWithCP24Time2a_create( + (MeasuredValueNormalizedWithCP24Time2a)mem, ioa, nv, q, iec_zero_cp24_buf(&cp24_z)); + float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalizedWithCP56Time2a_create( + (MeasuredValueNormalizedWithCP56Time2a)mem, ioa, nv, q, iec_wall_cp56_buf(&cp56_wall)); + float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalizedWithoutQuality_create( + (MeasuredValueNormalizedWithoutQuality)mem, ioa, nv); + return (InformationObject)MeasuredValueScaled_create( + (MeasuredValueScaled)mem, ioa, (int)*(IEC_INT *)b->iec_var, q); + return (InformationObject)MeasuredValueScaledWithCP24Time2a_create( + (MeasuredValueScaledWithCP24Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, q, + iec_zero_cp24_buf(&cp24_z)); + return (InformationObject)MeasuredValueScaledWithCP56Time2a_create( + (MeasuredValueScaledWithCP56Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, q, + iec_wall_cp56_buf(&cp56_wall)); + return (InformationObject)MeasuredValueShort_create( + (MeasuredValueShort)mem, ioa, *(IEC_REAL *)b->iec_var, q); + return (InformationObject)MeasuredValueShortWithCP24Time2a_create( + (MeasuredValueShortWithCP24Time2a)mem, ioa, *(IEC_REAL *)b->iec_var, q, + iec_zero_cp24_buf(&cp24_z)); + return (InformationObject)MeasuredValueShortWithCP56Time2a_create( + (MeasuredValueShortWithCP56Time2a)mem, ioa, *(IEC_REAL *)b->iec_var, q, + iec_wall_cp56_buf(&cp56_wall)); + bcr = BinaryCounterReading_create((BinaryCounterReading)&bcr_st, + (int32_t)(*(UDINT *)b->iec_var), 0, false, false, false); + return (InformationObject)IntegratedTotals_create((IntegratedTotals)mem, ioa, bcr); + bcr = BinaryCounterReading_create((BinaryCounterReading)&bcr_st, + (int32_t)(*(UDINT *)b->iec_var), 0, false, false, false); + return (InformationObject)IntegratedTotalsWithCP24Time2a_create( + (IntegratedTotalsWithCP24Time2a)mem, ioa, bcr, iec_zero_cp24_buf(&cp24_z)); + bcr = BinaryCounterReading_create((BinaryCounterReading)&bcr_st, + (int32_t)(*(UDINT *)b->iec_var), 0, false, false, false); + return (InformationObject)IntegratedTotalsWithCP56Time2a_create( + (IntegratedTotalsWithCP56Time2a)mem, ioa, bcr, iec_wall_cp56_buf(&cp56_wall)); + pack_u32 = (uint32_t)*(UDINT *)b->iec_var; + se = (SingleEvent)&se_byte; + SingleEvent_setEventState(se, (EventState)(pack_u32 & 3u)); + SingleEvent_setQDP(se, (QualityDescriptorP)((pack_u32 >> 2) & 0xffu)); + return (InformationObject)EventOfProtectionEquipment_create( + (EventOfProtectionEquipment)mem, ioa, se, iec_zero_cp16_buf(&cp16_z), iec_zero_cp24_buf(&cp24_z)); + pack_u32 = (uint32_t)*(UDINT *)b->iec_var; + ste = (StartEvent)(pack_u32 & 0xffu); + qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu); + return (InformationObject)PackedStartEventsOfProtectionEquipment_create( + (PackedStartEventsOfProtectionEquipment)mem, ioa, ste, qdp, iec_zero_cp16_buf(&cp16_z), + iec_zero_cp24_buf(&cp24_z)); + pack_u32 = (uint32_t)*(UDINT *)b->iec_var; + oci = (OutputCircuitInfo)(pack_u32 & 0xffu); + qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu); + return (InformationObject)PackedOutputCircuitInfo_create( + (PackedOutputCircuitInfo)mem, ioa, oci, qdp, iec_zero_cp16_buf(&cp16_z), iec_zero_cp24_buf(&cp24_z)); + pack_u32 = (uint32_t)*(UDINT *)b->iec_var; + se = (SingleEvent)&se_byte; + SingleEvent_setEventState(se, (EventState)(pack_u32 & 3u)); + SingleEvent_setQDP(se, (QualityDescriptorP)((pack_u32 >> 2) & 0xffu)); + return (InformationObject)EventOfProtectionEquipmentWithCP56Time2a_create( + (EventOfProtectionEquipmentWithCP56Time2a)mem, ioa, se, iec_zero_cp16_buf(&cp16_z), + iec_wall_cp56_buf(&cp56_wall)); + pack_u32 = (uint32_t)*(UDINT *)b->iec_var; + ste = (StartEvent)(pack_u32 & 0xffu); + qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu); + return (InformationObject)PackedStartEventsOfProtectionEquipmentWithCP56Time2a_create( + (PackedStartEventsOfProtectionEquipmentWithCP56Time2a)mem, ioa, ste, qdp, + iec_zero_cp16_buf(&cp16_z), iec_wall_cp56_buf(&cp56_wall)); + pack_u32 = (uint32_t)*(UDINT *)b->iec_var; + oci = (OutputCircuitInfo)(pack_u32 & 0xffu); + qdp = (QualityDescriptorP)((pack_u32 >> 8) & 0xffu); + return (InformationObject)PackedOutputCircuitInfoWithCP56Time2a_create( + (PackedOutputCircuitInfoWithCP56Time2a)mem, ioa, oci, qdp, iec_zero_cp16_buf(&cp16_z), + iec_wall_cp56_buf(&cp56_wall)); + memset(&scd_st, 0, sizeof(scd_st)); + scd = (StatusAndStatusChangeDetection)&scd_st; + StatusAndStatusChangeDetection_setSTn(scd, (uint16_t)(*(UDINT *)b->iec_var & 0xffffu)); + return (InformationObject)PackedSinglePointWithSCD_create( + (PackedSinglePointWithSCD)mem, ioa, scd, q); + return (InformationObject)EndOfInitialization_create( + (EndOfInitialization)mem, (uint8_t)(iec_get_bool_var(b->iec_var) ? 1 : 0)); + /* File transfer indications — PLC holds scalar summaries per IOA (length / qualifiers). */ + return (InformationObject)FileReady_create( + (FileReady)mem, ioa, 0, (uint32_t)*(UDINT *)b->iec_var, true); + return (InformationObject)SectionReady_create( + (SectionReady)mem, ioa, 0, 0, (uint32_t)*(UDINT *)b->iec_var, false); + uint32_t v = (uint32_t)*(UDINT *)b->iec_var; + /* Packed: NOF (bits 0–15), NOS (16–23), LSQ (24–31); CHS via extension if needed */ + return (InformationObject)FileLastSegmentOrSection_create( + (FileLastSegmentOrSection)mem, ioa, + (uint16_t)(v & 0xffffu), + (uint8_t)((v >> 16) & 0xffu), + (uint8_t)((v >> 24) & 0xffu), + return (InformationObject)FileACK_create( + (FileACK)mem, ioa, 0, 0, (uint8_t)(*(IEC_BYTE *)b->iec_var & 0xffu)); + return (InformationObject)FileSegment_create( + (FileSegment)mem, ioa, 0, 0, &seg_dummy, 0); + return (InformationObject)FileDirectory_create( + (FileDirectory)mem, ioa, 0, (uint32_t)*(UDINT *)b->iec_var, 0, iec_wall_cp56_buf(&cp56_wall)); +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_tid_handled_as_command(TypeID tid) { + if (t >= C_SC_NA_1 && t <= C_BO_NA_1) + if (t >= C_SC_TA_1 && t <= C_BO_TA_1) + if (t >= C_IC_NA_1 && t <= C_TS_TA_1) + if (t >= P_ME_NA_1 && t <= P_AC_NA_1) + if (t == F_SC_NA_1 || t == F_SC_NB_1) +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; + fv = SetpointCommandNormalized_getValue(sn); + 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); + Bitstring32Command bc = (Bitstring32Command)io; + *(UDINT *)match->iec_var = (UDINT)Bitstring32Command_getValue(bc); + iec_set_bool_var(match->iec_var, SingleCommand_getState((SingleCommand)io)); + DoubleCommandWithCP56Time2a dc = (DoubleCommandWithCP56Time2a)io; + *(IEC_BYTE *)match->iec_var = (IEC_BYTE)(DoubleCommandWithCP56Time2a_getState(dc) & 0xff); + StepCommandWithCP56Time2a sc = (StepCommandWithCP56Time2a)io; + *(IEC_BYTE *)match->iec_var = (IEC_BYTE)((int)StepCommandWithCP56Time2a_getState(sc) & 0xff); + SetpointCommandNormalizedWithCP56Time2a sn = (SetpointCommandNormalizedWithCP56Time2a)io; + fv = SetpointCommandNormalizedWithCP56Time2a_getValue(sn); + scv = NormalizedValue_toScaled(fv); + *(IEC_UINT *)match->iec_var = (uint16_t)scv; + SetpointCommandScaledWithCP56Time2a ss = (SetpointCommandScaledWithCP56Time2a)io; + *(IEC_INT *)match->iec_var = (int16_t)SetpointCommandScaledWithCP56Time2a_getValue(ss); + SetpointCommandShortWithCP56Time2a sf = (SetpointCommandShortWithCP56Time2a)io; + *(IEC_REAL *)match->iec_var = SetpointCommandShortWithCP56Time2a_getValue(sf); + Bitstring32CommandWithCP56Time2a bc = (Bitstring32CommandWithCP56Time2a)io; + *(UDINT *)match->iec_var = (UDINT)Bitstring32CommandWithCP56Time2a_getValue(bc); + InterrogationCommand ic = (InterrogationCommand)io; + iec_set_bool_var(match->iec_var, InterrogationCommand_getQOI(ic) != 0); + CounterInterrogationCommand cic = (CounterInterrogationCommand)io; + *(UDINT *)match->iec_var = (UDINT)CounterInterrogationCommand_getQCC(cic); + iec_set_bool_var(match->iec_var, true); + ClockSynchronizationCommand cs = (ClockSynchronizationCommand)io; + *(UDINT *)match->iec_var = + (UDINT)(CP56Time2a_toMsTimestamp(ClockSynchronizationCommand_getTime(cs)) & 0xffffffffu); + iec_set_bool_var(match->iec_var, TestCommand_isValid((TestCommand)io)); + *(IEC_BYTE *)match->iec_var = + (IEC_BYTE)(ResetProcessCommand_getQRP((ResetProcessCommand)io) & 0xff); + DelayAcquisitionCommand da = (DelayAcquisitionCommand)io; + *(UDINT *)match->iec_var = + (UDINT)CP16Time2a_getEplapsedTimeInMs(DelayAcquisitionCommand_getDelay(da)); + *(IEC_UINT *)match->iec_var = + (IEC_UINT)TestCommandWithCP56Time2a_getCounter((TestCommandWithCP56Time2a)io); + ParameterNormalizedValue pn = (ParameterNormalizedValue)io; + fv = ParameterNormalizedValue_getValue(pn); + scv = NormalizedValue_toScaled(fv); + *(IEC_UINT *)match->iec_var = (uint16_t)scv; + ParameterScaledValue ps = (ParameterScaledValue)io; + *(IEC_INT *)match->iec_var = (int16_t)ParameterScaledValue_getValue(ps); + ParameterFloatValue pf = (ParameterFloatValue)io; + *(IEC_REAL *)match->iec_var = ParameterFloatValue_getValue(pf); + *(IEC_BYTE *)match->iec_var = + (IEC_BYTE)(ParameterActivation_getQuality((ParameterActivation)io) & 0xff); + *(IEC_BYTE *)match->iec_var = + (IEC_BYTE)(FileCallOrSelect_getSCQ((FileCallOrSelect)io) & 0xff); + *(IEC_UINT *)match->iec_var = (IEC_UINT)QueryLog_getNOF((QueryLog)io); + 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 (iec_tid_handled_as_command(tid)) + 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); + /* Do not call CS104_Slave_getOpenConnections here — it re-enters the slave from inside + * lib60870 connection callbacks and can deadlock with multiple clients. conn_bool is + * refreshed every PLC cycle in __retrieve_*(). */ +int __init_%(locstr)s(int argc, char **argv) { + for (si = 0; si < IEC60870_NUM_SERVERS_%(locstr)s; si++) { + iec60870_srv_t *s = &iec60870_srv[si]; + 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); + /* Each master TCP session is an independent redundancy group. The default SINGLE_REDUNDANCY_GROUP + * shares one app-layer context across all peers; closing one connection can break others + * (e.g. two GiMonitor clients). */ + CS104_Slave_setServerMode(s->slave, CS104_MODE_CONNECTION_IS_REDUNDANCY_GROUP); + 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); + __cleanup_%(locstr)s(); +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) { +int __cleanup_%(locstr)s(void) { + 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); \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/iec60870/iec60870_runtime.h Wed May 06 16:27:02 2026 +0200
@@ -0,0 +1,10 @@
+/* Template expanded by IEC60870 RootClass.CTNGenerate_C — instance %(locstr)s */ +#ifndef IEC60870_RUNTIME_%(locstr)s_H +#define IEC60870_RUNTIME_%(locstr)s_H +#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 Wed May 06 16:27:02 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)