--- a/iec60870/iec60870.py Wed Apr 22 09:49:11 2026 +0200
+++ b/iec60870/iec60870.py Mon May 04 07:42:48 2026 +0200
@@ -14,21 +14,172 @@
# (type_id, IEC_type, datasize, direction, size_code, description)
-# direction: "I" for monitor (input), "Q" for control (output), "M" for memory
-# size_code: "X" for bit, "B" for byte, "W" for word, "D" for dword
+# 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. - "M_SP_NA_1 - Single point": (1, "BOOL", 1, "I", "X", "Single Point Information"),
- "M_DP_NA_1 - Double point": (3, "BYTE", 8, "I", "B", "Double Point Information"),
- "M_ST_NA_1 - Step position": (5, "INT", 16, "I", "W", "Step Position Information"),
- "M_ME_NA_1 - Measured normalized": (9, "WORD", 16, "I", "W", "Measured Value Normalized"),
- "M_ME_NB_1 - Measured scaled": (11, "INT", 16, "I", "W", "Measured Value Scaled"),
- "M_ME_NC_1 - Measured float": (13, "REAL", 32, "I", "D", "Measured Value Short Float"),
- "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 - Step command": (47, "BYTE", 8, "Q", "B", "Regulating Step Command"),
- "C_SE_NA_1 - Setpoint normalized": (48, "WORD", 16, "Q", "W", "Set Point Normalized"),
- "C_SE_NB_1 - Setpoint scaled": (49, "INT", 16, "Q", "W", "Set Point Scaled"),
- "C_SE_NC_1 - Setpoint float": (50, "REAL", 32, "Q", "D", "Set Point Short Float"),
+ # 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
@@ -236,8 +387,22 @@
def _matiec_located_storage_cname(iec_name):
- """Matiec storage symbol for located var NAME is __##NAME."""
- return "__" + str(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):
@@ -372,6 +537,91 @@
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 @@ -381,7 +631,7 @@
<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"/>
+ <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">
@@ -432,7 +682,7 @@
- "var_name": "IEC104_" + str(type_id) + "_" + str(offset),
+ "var_name": "IEC60870_" + str(type_id) + "_" + str(offset), "location": size_code + ".".join(
[str(i) for i in current_location]) + "." + str(offset),
@@ -505,7 +755,7 @@
"type": LOCATION_VAR_MEMORY,
- "var_name": "var_name",
+ "var_name": "rd_request_counter", "location": "D" + ".".join(
[str(i) for i in current_location]) + ".0",
"description": "IEC60870 read request counter",
@@ -515,7 +765,7 @@
"type": LOCATION_VAR_MEMORY,
- "var_name": "var_name",
+ "var_name": "wr_request_counter", "location": "D" + ".".join(
[str(i) for i in current_location]) + ".1",
"description": "IEC60870 write request counter",
@@ -525,7 +775,7 @@
"type": LOCATION_VAR_MEMORY,
- "var_name": "var_name",
+ "var_name": "conn_active_flag", "location": "X" + ".".join(
[str(i) for i in current_location]) + ".2",
"description": "IEC60870 connection active flag",
@@ -547,6 +797,8 @@
# 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" ?>
@@ -604,7 +856,7 @@
"type": LOCATION_VAR_MEMORY,
- "var_name": "var_name",
+ "var_name": "conn_status", "location": "B" + ".".join(
[str(i) for i in current_location]) + ".0",
"description": "Connection status (0=disconnected, 1=connected, "
@@ -615,7 +867,7 @@
"type": LOCATION_VAR_MEMORY,
- "var_name": "var_name",
+ "var_name": "interrogation_trigger", "location": "X" + ".".join(
[str(i) for i in current_location]) + ".1",
"description": "Trigger general interrogation",
@@ -661,7 +913,7 @@
("IEC60870Server", _IEC60870ServerPlug, "IEC 60870-5-104 Server"),
- ("IEC60870Client", _IEC60870ClientPlug, "IEC 60870-5-104 Client"),
+ # ("IEC60870Client", _IEC60870ClientPlug, "IEC 60870-5-104 Client"), @@ -745,23 +997,46 @@
"CS104 runtime. Disable Use_TLS on server \"%s\".\n") %
+ diag_intrinsic_global_lines = [] + diag_matiec_weak_seen = set() + diag_weak_backing_by_ptrsym = {} plc_memory_storage_lines = []
- def add_extern(iec_type, name):
- storage = _matiec_located_storage_cname(name)
- key = ("stor", iec_type, name)
- "extern %(t)s %(s)s;" % {"t": iec_type, "s": storage})
+ 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"])
@@ -776,22 +1051,48 @@
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_name = "iec60870_diag_rd_%d" % server_id
- wr_name = "iec60870_diag_wr_%d" % server_id
- conn_name = "iec60870_diag_conn_%d" % server_id
- diag_static_lines.append(
- "static UDINT %(rd)s = 0U;\n"
- "static UDINT %(wr)s = 0U;\n"
- "static IEC_BOOL %(cn)s = (IEC_BOOL)0;\n" % {
+ rd_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 - add_extern("UDINT", rd_name)
- add_extern("UDINT", wr_name)
- add_extern("BOOL", conn_name)
+ 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#"):
@@ -816,16 +1117,6 @@
loc_label = _c_escape_str(
".".join(map(str, srv.GetCurrentLocation())))
- if diag_mode == "intrinsic":
- rd_ref = "&(%s)" % rd_name
- wr_ref = "&(%s)" % wr_name
- cn_ref = "&(%s)" % conn_name
- # PLC diagnostics: use address of matiec storage (__##name).
- rd_ref = "&(%s)" % _matiec_located_storage_cname(rd_name)
- wr_ref = "&(%s)" % _matiec_located_storage_cname(wr_name)
- cn_ref = "&(%s)" % _matiec_located_storage_cname(conn_name)
{ .loc_label = "%(loc)s",
@@ -865,51 +1156,24 @@
- srv_tree_loc = tuple(srv.GetCurrentLocation())
- if diag_mode == "intrinsic":
- for iecvar in srv.GetLocations():
- loc = iecvar.get("LOC")
- if not loc or len(loc) < 3:
- if tail not in (0, 1, 2):
- if len(pfx) < len(srv_tree_loc):
- if pfx[-len(srv_tree_loc):] != srv_tree_loc:
- nm = str(iecvar["NAME"]).strip()
- if nm in loc_vars_seen:
- iet = iecvar.get("IEC_TYPE")
- if tail == 0 and iet == "UDINT":
- "UDINT *%s = &iec60870_diag_rd_%d;\n" % (
- elif tail == 1 and iet == "UDINT":
- "UDINT *%s = &iec60870_diag_wr_%d;\n" % (
- elif tail == 2 and iet in ("BOOL", "IEC_BOOL"):
- "IEC_BOOL *%s = &iec60870_diag_conn_%d;\n" % (
for dp in srv.IECSortedChildren():
asdu_str = _data_point_ct(dp, 0)
ioa0 = int(_data_point_ct(dp, 1))
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()
@@ -917,6 +1181,24 @@
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(
@@ -943,13 +1225,19 @@
" { -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_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)
@@ -965,6 +1253,7 @@
+ "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,
@@ -1002,4 +1291,4 @@
\ No newline at end of file
--- a/iec60870/iec60870_runtime.c Wed Apr 22 09:49:11 2026 +0200
+++ b/iec60870/iec60870_runtime.c Mon May 04 07:42:48 2026 +0200
@@ -4,6 +4,7 @@
#include "iec_types_all.h"
#include "iec60870_common.h"
@@ -11,9 +12,18 @@
#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
@@ -44,7 +54,6 @@
-/* PLC-mapped diagnostics only (intrinsic mode uses static storage above). */
static struct iec60870_binding iec60870_bindings[] = {
@@ -73,44 +82,242 @@
*(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). */ +static struct sCP56Time2a iec_wall_cp56_storage; +static struct sCP24Time2a iec_zero_cp24_storage; +static struct sCP16Time2a iec_zero_cp16_storage; +static CP56Time2a iec_wall_cp56(void) { + CP56Time2a t = (CP56Time2a)&iec_wall_cp56_storage; + CP56Time2a_createFromMsTimestamp(t, (uint64_t)time(NULL) * 1000ULL); +static CP24Time2a iec_zero_cp24(void) { + memset(&iec_zero_cp24_storage, 0, sizeof(iec_zero_cp24_storage)); + return (CP24Time2a)&iec_zero_cp24_storage; +static CP16Time2a iec_zero_cp16(void) { + memset(&iec_zero_cp16_storage, 0, sizeof(iec_zero_cp16_storage)); + return (CP16Time2a)&iec_zero_cp16_storage; 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 sBinaryCounterReading bcr_st; + BinaryCounterReading bcr; + QualityDescriptorP qdp; + struct sStatusAndStatusChangeDetection scd_st; + StatusAndStatusChangeDetection scd; - QualityDescriptor q = IEC60870_QUALITY_GOOD;
- bool val = iec_get_bool_var(b->iec_var);
return (InformationObject)SinglePointInformation_create(
- (SinglePointInformation)mem, ioa, val, q);
+ (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, + return (InformationObject)SinglePointWithCP56Time2a_create( + (SinglePointWithCP56Time2a)mem, ioa, iec_get_bool_var(b->iec_var), 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;
+ DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3); + return (InformationObject)DoublePointWithCP24Time2a_create( + (DoublePointWithCP24Time2a)mem, ioa, dv, q, iec_zero_cp24()); + DoublePointValue dv = (DoublePointValue)(*(IEC_BYTE *)b->iec_var & (IEC_BYTE)3); + return (InformationObject)DoublePointWithCP56Time2a_create( + (DoublePointWithCP56Time2a)mem, ioa, dv, q, iec_wall_cp56()); return (InformationObject)StepPositionInformation_create(
- (StepPositionInformation)mem, ioa, sv, false, q);
+ (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, + return (InformationObject)StepPositionWithCP56Time2a_create( + (StepPositionWithCP56Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, false, q, + 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, + return (InformationObject)Bitstring32WithCP56Time2a_createEx( + (Bitstring32WithCP56Time2a)mem, ioa, (uint32_t)*(UDINT *)b->iec_var, q, - float nv = NormalizedValue_fromScaled(
- (int)(int16_t)*(IEC_UINT *)b->iec_var);
+ 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 nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalizedWithCP24Time2a_create( + (MeasuredValueNormalizedWithCP24Time2a)mem, ioa, nv, q, iec_zero_cp24()); + float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalizedWithCP56Time2a_create( + (MeasuredValueNormalizedWithCP56Time2a)mem, ioa, nv, q, iec_wall_cp56()); + float nv = NormalizedValue_fromScaled((int)(int16_t)*(IEC_UINT *)b->iec_var); + return (InformationObject)MeasuredValueNormalizedWithoutQuality_create( + (MeasuredValueNormalizedWithoutQuality)mem, ioa, nv);
- float fv = *(IEC_REAL *)b->iec_var;
+ 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, + return (InformationObject)MeasuredValueScaledWithCP56Time2a_create( + (MeasuredValueScaledWithCP56Time2a)mem, ioa, (int)*(IEC_INT *)b->iec_var, q, return (InformationObject)MeasuredValueShort_create(
- (MeasuredValueShort)mem, ioa, fv, q);
+ (MeasuredValueShort)mem, ioa, *(IEC_REAL *)b->iec_var, q); + return (InformationObject)MeasuredValueShortWithCP24Time2a_create( + (MeasuredValueShortWithCP24Time2a)mem, ioa, *(IEC_REAL *)b->iec_var, q, + return (InformationObject)MeasuredValueShortWithCP56Time2a_create( + (MeasuredValueShortWithCP56Time2a)mem, ioa, *(IEC_REAL *)b->iec_var, q, + 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()); + 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()); + 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(), iec_zero_cp24()); + 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(), + 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(), iec_zero_cp24()); + 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(), + 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(), iec_wall_cp56()); + 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(), + 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()); @@ -166,12 +373,29 @@
+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;
@@ -211,11 +435,9 @@
SetpointCommandNormalized sn = (SetpointCommandNormalized)io;
- float fv = SetpointCommandNormalized_getValue(sn);
- int scv = NormalizedValue_toScaled(fv);
- *(IEC_UINT *)match->iec_var = (uint16_t)scv;
+ fv = SetpointCommandNormalized_getValue(sn); + scv = NormalizedValue_toScaled(fv); + *(IEC_UINT *)match->iec_var = (uint16_t)scv; @@ -228,6 +450,115 @@
*(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); @@ -241,7 +572,7 @@
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)
+ if (iec_tid_handled_as_command(tid)) return iec_handle_command(srv, connection, asdu);
@@ -328,4 +659,4 @@
\ No newline at end of file