lpcmanager

Parents 656720f97e4d
Children 2ac146c1b16c
Update IEC60870 files to accommodate multiple common addresses on same server instance
--- a/iec60870/iec60870.py Tue May 26 13:50:51 2026 +0200
+++ b/iec60870/iec60870.py Tue Jun 02 09:02:31 2026 +0200
@@ -183,9 +183,8 @@
"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: TCP/APCI link-layer parameters (one CS104 listener per server instance)
+_IEC60870_APCI_PARAMS_XSD = """\
<xsd:attribute name="APCI_k" use="optional" default="12">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
@@ -235,6 +234,10 @@
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Use_TLS" type="xsd:boolean" use="optional" default="false"/>
+"""
+
+# XSD: application-layer ASDU encoding per common address (station unit)
+_IEC60870_APP_LAYER_PARAMS_XSD = """\
<xsd:attribute name="CA_Size" use="optional" default="2">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
@@ -263,6 +266,9 @@
<xsd:attribute name="Use_Local_Timezone" type="xsd:boolean" use="optional" default="true"/>
"""
+# Client node still uses the combined fragment (hidden from IDE)
+_IEC60870_CONN_PARAMS_XSD = _IEC60870_APCI_PARAMS_XSD + _IEC60870_APP_LAYER_PARAMS_XSD
+
def _srv_attr_map(server_plug):
for element in server_plug.GetParamsAttributes():
@@ -271,6 +277,24 @@
return {}
+def _ca_attr_map(ca_plug):
+ for element in ca_plug.GetParamsAttributes():
+ if element["name"] == "IEC60870CommonAddressNode":
+ return {c["name"]: c["value"] for c in element["children"]}
+ return {}
+
+
+def _app_layer_signature(cmap):
+ """Tuple used to ensure one CS104 slave uses consistent ASDU encoding."""
+ cot_has_oa = str(cmap.get("COT_Has_OA", True)).lower() in ("true", "1", "yes")
+ return (
+ int(cmap.get("CA_Size", 2)),
+ int(cmap.get("IOA_Size", 3)),
+ 1 if cot_has_oa else 0,
+ int(cmap.get("OA_Value", 10)),
+ )
+
+
def _c_escape_str(s):
return s.replace("\\", "\\\\").replace("\"", "\\\"")
@@ -554,8 +578,8 @@
%%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.
+ datapoint at e.g. …4.0.0.0 incorrectly receives %%MD4.0.0 alongside %%MX4.0.0.0.0.
+ Datapoint IOAs live two segments deeper (…4.0.0.<ioa>), so they never match.
"""
if not srv_loc or not loc_tuple:
return False
@@ -670,6 +694,10 @@
ioa = self.GetParamsAttributes()[0]["children"][1]["value"]
count = self.GetParamsAttributes()[0]["children"][2]["value"]
asdu_type_str = self.GetParamsAttributes()[0]["children"][0]["value"]
+ common_label = ""
+ parent = getattr(self, "CTNParent", None)
+ if parent is not None and getattr(parent, "PlugType", None) == "IEC60870CommonAddress":
+ common_label = str(parent.IEC60870CommonAddressNode.getCommon_Address())
type_id, datatype, datasize, direction, size_code, desc = \
iec60870_asdu_types[asdu_type_str]
@@ -678,12 +706,16 @@
entries = []
for offset in range(ioa, ioa + count):
+ ioa_name = desc + " IOA " + str(offset)
+ if common_label:
+ ioa_name = "CA " + common_label + " — " + ioa_name
entries.append({
- "name": desc + " IOA " + str(offset),
+ "name": ioa_name,
"type": loc_type,
"size": datasize,
"IEC_type": datatype,
- "var_name": "IEC60870_" + str(type_id) + "_" + str(offset),
+ "var_name": "IEC60870_" + str(type_id) + (
+ ("_CA" + common_label) if common_label else "") + "_" + str(offset),
"location": size_code + ".".join(
[str(i) for i in current_location]) + "." + str(offset),
"description": desc,
@@ -700,6 +732,67 @@
#
+# C O M M O N A D D R E S S
+#
+
+class _CommonAddressPlug(object):
+ XSD = ("""<?xml version="1.0" encoding="ISO-8859-1" ?>
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <xsd:element name="IEC60870CommonAddressNode">
+ <xsd:complexType>
+ <xsd:attribute name="Configuration_Name" type="xsd:string" use="optional" default=""/>
+ <xsd:attribute name="Common_Address" use="optional" default="1">
+ <xsd:simpleType>
+ <xsd:restriction base="xsd:integer">
+ <xsd:minInclusive value="1"/>
+ <xsd:maxInclusive value="65534"/>
+ </xsd:restriction>
+ </xsd:simpleType>
+ </xsd:attribute>
+"""
+ + _IEC60870_APP_LAYER_PARAMS_XSD +
+ """
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ """)
+
+ CTNChildrenTypes = [("IEC60870DataPoint", _DataPointPlug, "Data Point")]
+ PlugType = "IEC60870CommonAddress"
+
+ def __init__(self):
+ loc_str = ".".join(map(str, self.GetCurrentLocation()))
+ common = self.IEC60870CommonAddressNode.getCommon_Address()
+ self.IEC60870CommonAddressNode.setConfiguration_Name(
+ "Common Address %s (%s)" % (common, loc_str))
+
+ def GetNodeCount(self):
+ return (0, 0)
+
+ def GetConfigName(self):
+ return self.IEC60870CommonAddressNode.getConfiguration_Name()
+
+ def GetVariableLocationTree(self):
+ current_location = self.GetCurrentLocation()
+ name = self.BaseParams.getName()
+ common = self.IEC60870CommonAddressNode.getCommon_Address()
+
+ entries = []
+ for child in self.IECSortedChildren():
+ entries.append(child.GetVariableLocationTree())
+
+ return {"name": name,
+ "type": LOCATION_CONFNODE,
+ "location": ".".join(
+ [str(i) for i in current_location]) + ".x",
+ "description": "Common Address %s" % common,
+ "children": entries}
+
+ def CTNGenerate_C(self, buildpath, locations):
+ return [], "", False
+
+
+#
# C O N T R O L L E D S T A T I O N (Server)
#
@@ -711,23 +804,16 @@
<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:simpleType>
- <xsd:restriction base="xsd:integer">
- <xsd:minInclusive value="1"/>
- <xsd:maxInclusive value="65534"/>
- </xsd:restriction>
- </xsd:simpleType>
- </xsd:attribute>
"""
- + _IEC60870_CONN_PARAMS_XSD +
+ + _IEC60870_APCI_PARAMS_XSD +
"""
</xsd:complexType>
</xsd:element>
</xsd:schema>
""")
- CTNChildrenTypes = [("IEC60870DataPoint", _DataPointPlug, "Data Point")]
+ CTNChildrenTypes = [
+ ("IEC60870CommonAddress", _CommonAddressPlug, "Common Address")]
PlugType = "IEC60870Server"
def __init__(self):
@@ -1000,6 +1086,7 @@
bindings_rows = []
srv_rows = []
+ ca_rows = []
diag_static_lines = []
diag_intrinsic_global_lines = []
diag_matiec_weak_seen = set()
@@ -1047,8 +1134,42 @@
if len(loose_locs) > len(project_locs or []):
project_locs = loose_locs
+ ca_index = 0
+
for server_id, srv in enumerate(servers):
smap = _srv_attr_map(srv)
+ ca_nodes = srv.IECSortedChildren()
+ legacy_dps = [
+ c for c in ca_nodes if getattr(c, "CTNType", None) == "IEC60870DataPoint"]
+ if legacy_dps:
+ self.FatalError(
+ _("Error: IEC60870 server %%{a1}.x has data points attached "
+ "directly to the server. Add one or more Common Address "
+ "nodes and move data points underneath them.\n").format(
+ a1=_lt_to_str(srv.GetCurrentLocation())))
+ seen_common = set()
+ app_layer_sigs = set()
+ for ca_node in ca_nodes:
+ cmap = _ca_attr_map(ca_node)
+ common = int(cmap.get("Common_Address", 1))
+ if common in seen_common:
+ self.FatalError(
+ _("Error: IEC60870 server %%{a1}.x has duplicate "
+ "Common_Address {a2} on nodes %%{a3}.x and another "
+ "Common Address child.\n").format(
+ a1=_lt_to_str(srv.GetCurrentLocation()),
+ a2=common,
+ a3=_lt_to_str(ca_node.GetCurrentLocation())))
+ seen_common.add(common)
+ app_layer_sigs.add(_app_layer_signature(cmap))
+ if len(app_layer_sigs) > 1:
+ self.FatalError(
+ _("Error: IEC60870 server %%{a1}.x: all Common Address "
+ "children must use the same CA_Size, IOA_Size, "
+ "COT_Has_OA, and OA_Value (one CS104 listener shares "
+ "application-layer encoding).\n").format(
+ a1=_lt_to_str(srv.GetCurrentLocation())))
+
rd_name, wr_name, conn_name, diag_mode = _resolve_server_diagnostic_names(
srv, locations, project_locs)
@@ -1102,12 +1223,6 @@
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 (
- "true", "1", "yes")
- oa = int(smap.get("OA_Value", 10))
apci_k = int(smap["APCI_k"])
apci_w = int(smap["APCI_w"])
t0 = int(smap["Timeout_t0"])
@@ -1115,13 +1230,25 @@
t2 = int(smap["Timeout_t2"])
t3 = int(smap["Timeout_t3"])
+ if ca_nodes:
+ first_cmap = _ca_attr_map(ca_nodes[0])
+ srv_ca_sz = int(first_cmap.get("CA_Size", 2))
+ srv_ioa_sz = int(first_cmap.get("IOA_Size", 3))
+ srv_cot_has_oa = str(first_cmap.get("COT_Has_OA", True)).lower() in (
+ "true", "1", "yes")
+ srv_oa = int(first_cmap.get("OA_Value", 10))
+ else:
+ srv_ca_sz = 2
+ srv_ioa_sz = 3
+ srv_cot_has_oa = True
+ srv_oa = 10
+
loc_label = _c_escape_str(
".".join(map(str, srv.GetCurrentLocation())))
srv_rows.append(
"""
{ .loc_label = "%(loc)s",
- .common_address = %(co)d,
.ip_str = "%(ipc)s",
.port = %(port)d,
.max_open = %(maxc)d,
@@ -1138,14 +1265,13 @@
.init_st = 0
},""" % {
"loc": loc_label,
- "co": common,
"ipc": ip_c,
"port": port,
"maxc": max_remote,
- "casz": ca_sz,
- "ioasz": ioa_sz,
- "cotoa": 1 if cot_has_oa else 0,
- "oa": oa,
+ "casz": srv_ca_sz,
+ "ioasz": srv_ioa_sz,
+ "cotoa": 1 if srv_cot_has_oa else 0,
+ "oa": srv_oa,
"apk": apci_k,
"apw": apci_w,
"t0": t0,
@@ -1157,71 +1283,107 @@
"cn_ref": cn_ref,
})
- 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 = iecvar["LOC"]
- loc_t = _normalize_loc_tuple(loc)
- if not loc_t:
- continue
- if _iec60870_loc_is_server_intrinsic_diag_slot(
- srv_loc_tuple, loc_t):
- continue
- if len(loc_t) < 3:
- continue
- ioa_v = int(loc_t[-1])
- if ioa_v < ioa0 or ioa_v >= ioa0 + npoints:
- continue
- 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)
- if not weak_bk:
- self.FatalError(
- _("IEC60870 internal: diagnostic %(sym)s has "
- "no generated backing.\n") % {"sym": nm})
- loc_vars_seen.add(nm)
+ for ca_node in ca_nodes:
+ cmap = _ca_attr_map(ca_node)
+ common = int(cmap.get("Common_Address", 1))
+ ca_sz = int(cmap.get("CA_Size", 2))
+ ioa_sz = int(cmap.get("IOA_Size", 3))
+ cot_has_oa = str(cmap.get("COT_Has_OA", True)).lower() in (
+ "true", "1", "yes")
+ oa = int(cmap.get("OA_Value", 10))
+ ca_loc_label = _c_escape_str(
+ ".".join(map(str, ca_node.GetCurrentLocation())))
+
+ ca_rows.append(
+ """
+ { .server_index = %(sid)d,
+ .loc_label = "%(loc)s",
+ .common_address = %(co)d,
+ .ca_sz = %(casz)d,
+ .ioa_sz = %(ioasz)d,
+ .cot_two_byte = %(cotoa)d,
+ .oa = %(oa)d
+ },""" % {
+ "sid": server_id,
+ "loc": ca_loc_label,
+ "co": common,
+ "casz": ca_sz,
+ "ioasz": ioa_sz,
+ "cotoa": 1 if cot_has_oa else 0,
+ "oa": oa,
+ })
+ this_ca_index = ca_index
+ ca_index += 1
+
+ for dp in ca_node.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 = iecvar["LOC"]
+ loc_t = _normalize_loc_tuple(loc)
+ if not loc_t:
+ continue
+ if _iec60870_loc_is_server_intrinsic_diag_slot(
+ srv_loc_tuple, loc_t):
+ continue
+ if len(loc_t) < 4:
+ continue
+ ioa_v = int(loc_t[-1])
+ if ioa_v < ioa0 or ioa_v >= ioa0 + npoints:
+ continue
+ 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)
+ if not weak_bk:
+ self.FatalError(
+ _("IEC60870 internal: diagnostic %(sym)s has "
+ "no generated backing.\n") % {"sym": nm})
+ loc_vars_seen.add(nm)
+ bindings_rows.append(
+ " { %(caid)d, %(tid)d, %(ioa)d, %(isc)d, %(bkind)d, "
+ "(void *)&%(weak_bk)s }," % {
+ "caid": this_ca_index,
+ "tid": tid,
+ "ioa": ioa_v,
+ "isc": is_cmd,
+ "bkind": bk,
+ "weak_bk": weak_bk,
+ })
+ continue
+ stor_name = "iec60870_plcmem_%s_%d" % (loc_prefix, bind_idx)
+ zinit = _iec_c_storage_zero_init(c_stor)
+ plc_memory_storage_lines.append(
+ "static %(ct)s %(sn)s = %(z)s;\n" % {
+ "ct": c_stor, "sn": stor_name, "z": zinit})
+ if nm not in loc_vars_seen:
+ loc_vars_lines.append(
+ "%(ct)s *%(nm)s = &%(sn)s;\n" % {
+ "ct": c_stor, "nm": nm, "sn": stor_name})
+ loc_vars_seen.add(nm)
bindings_rows.append(
- " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bkind)d, "
- "(void *)&%(weak_bk)s }," % {
- "sid": server_id,
+ " { %(caid)d, %(tid)d, %(ioa)d, %(isc)d, %(bk)d, "
+ "(void *)&%(sn)s }," % {
+ "caid": this_ca_index,
"tid": tid,
"ioa": ioa_v,
"isc": is_cmd,
- "bkind": bk,
- "weak_bk": weak_bk,
+ "bk": bk,
+ "sn": stor_name,
})
- continue
- stor_name = "iec60870_plcmem_%s_%d" % (loc_prefix, bind_idx)
- zinit = _iec_c_storage_zero_init(c_stor)
- plc_memory_storage_lines.append(
- "static %(ct)s %(sn)s = %(z)s;\n" % {
- "ct": c_stor, "sn": stor_name, "z": zinit})
- if nm not in loc_vars_seen:
- loc_vars_lines.append(
- "%(ct)s *%(nm)s = &%(sn)s;\n" % {
- "ct": c_stor, "nm": nm, "sn": stor_name})
- loc_vars_seen.add(nm)
- bindings_rows.append(
- " { %(sid)d, %(tid)d, %(ioa)d, %(isc)d, %(bk)d, "
- "(void *)&%(sn)s }," % {
- "sid": server_id,
- "tid": tid,
- "ioa": ioa_v,
- "isc": is_cmd,
- "bk": bk,
- "sn": stor_name,
- })
num_srv = len(servers)
+ num_ca = ca_index
+ if not ca_rows:
+ ca_rows.append(
+ " { 0, \"\", 1, 2, 3, 1, 10 }, /* placeholder */")
if not bindings_rows:
bindings_rows.append(
" { -1, 0, 0, 0, 0, NULL }, /* placeholder */")
@@ -1232,6 +1394,9 @@
srv_def = (
"static iec60870_srv_t iec60870_srv[IEC60870_NUM_SERVERS_%s] = {%s\n};"
% (loc_prefix, "".join(srv_rows)))
+ ca_def = (
+ "static iec60870_ca_t iec60870_ca[IEC60870_NUM_CA_%s] = {%s\n};"
+ % (loc_prefix, "".join(ca_rows)))
diag_intrinsic_global_block = "".join(diag_intrinsic_global_lines)
if diag_intrinsic_global_block:
@@ -1262,7 +1427,9 @@
"extern_block": extern_block,
"bindings_rows": bindings_body,
"srv_def": srv_def,
+ "ca_def": ca_def,
"num_servers": str(num_srv),
+ "num_ca": str(num_ca),
"num_bindings": str(num_bind),
"max_remote_clients": str(max_remote),
}
--- a/iec60870/iec60870_runtime.c Tue May 26 13:50:51 2026 +0200
+++ b/iec60870/iec60870_runtime.c Tue Jun 02 09:02:31 2026 +0200
@@ -35,7 +35,7 @@
%(plc_memory_storage_block)s
%(loc_vars_block)s
struct iec60870_binding {
- int server_index;
+ int ca_index;
int type_id;
int ioa;
int is_command;
@@ -44,8 +44,17 @@
};
typedef struct {
+ int server_index;
const char *loc_label;
int common_address;
+ int ca_sz;
+ int ioa_sz;
+ int cot_two_byte;
+ int oa;
+} iec60870_ca_t;
+
+typedef struct {
+ const char *loc_label;
char ip_str[128];
int port;
int max_open;
@@ -94,8 +103,16 @@
static const size_t iec60870_binding_count = IEC60870_NUM_BINDINGS_%(locstr)s;
+%(ca_def)s
+
%(srv_def)s
+static iec60870_srv_t *iec60870_srv_for_ca(int ca_index) {
+ if (ca_index < 0 || ca_index >= IEC60870_NUM_CA_%(locstr)s)
+ return NULL;
+ return &iec60870_srv[iec60870_ca[ca_index].server_index];
+}
+
static void iec_rd_inc(iec60870_srv_t *s) {
if (s->rd_ctr)
*s->rd_ctr += 1U;
@@ -310,7 +327,7 @@
iec_wall_ref_maybe_refresh();
for (i = 0; i < iec60870_binding_count; i++) {
- if (iec60870_bindings[i].server_index < 0 || !iec60870_bindings[i].iec_var)
+ if (iec60870_bindings[i].ca_index < 0 || !iec60870_bindings[i].iec_var)
continue;
iec_binding_last_mono_ms[i] = now_mono;
iec_binding_shadow_store(i, &iec60870_bindings[i]);
@@ -323,7 +340,7 @@
for (i = 0; i < iec60870_binding_count; i++) {
struct iec60870_binding *b = &iec60870_bindings[i];
- if (b->server_index < 0 || !b->iec_var)
+ if (b->ca_index < 0 || !b->iec_var)
continue;
if (iec_binding_shadow_differs(i, b))
iec_binding_touch(i);
@@ -356,8 +373,7 @@
struct sCP24Time2a cp24_z;
struct sCP16Time2a cp16_z;
size_t bind_idx = (size_t)(b - iec60870_bindings);
- iec60870_srv_t *srv =
- (b->server_index >= 0) ? &iec60870_srv[b->server_index] : NULL;
+ iec60870_srv_t *srv = iec60870_srv_for_ca(b->ca_index);
uint64_t tag_ms = iec_binding_tag_ms(srv, bind_idx);
struct sBinaryCounterReading bcr_st;
BinaryCounterReading bcr;
@@ -580,16 +596,17 @@
}
}
-static bool iec_send_monitor(IMasterConnection conn, iec60870_srv_t *srv, struct iec60870_binding *b) {
+static bool iec_send_monitor(IMasterConnection conn, iec60870_ca_t *ca, struct iec60870_binding *b) {
+ iec60870_srv_t *srv = iec60870_srv_for_ca(b->ca_index);
CS101_AppLayerParameters al = CS104_Slave_getAppLayerParameters(srv->slave);
- int oa = srv->cot_two_byte ? srv->oa : 0;
+ int oa = ca->cot_two_byte ? ca->oa : 0;
InformationObject io = iec_make_monitor_io(b->type_id, b->ioa, b);
if (!io)
return false;
CS101_ASDU asdu = CS101_ASDU_create(al, false, CS101_COT_INTERROGATED_BY_STATION, oa,
- srv->common_address, false, false);
+ ca->common_address, false, false);
if (!asdu) {
InformationObject_destroy(io);
return false;
@@ -609,21 +626,25 @@
}
static bool iec_interrogation_handler(void *parameter, IMasterConnection connection, CS101_ASDU asdu, uint8_t qoi) {
- (void)asdu;
iec60870_srv_t *srv = (iec60870_srv_t *)parameter;
- size_t idx = (size_t)(srv - iec60870_srv);
+ size_t srv_idx = (size_t)(srv - iec60870_srv);
+ int asdu_ca = CS101_ASDU_getCA(asdu);
size_t i;
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)
+ iec60870_ca_t *ca;
+ if (b->ca_index < 0)
continue;
- if ((size_t)b->server_index != idx)
+ ca = &iec60870_ca[b->ca_index];
+ if ((size_t)ca->server_index != srv_idx)
+ continue;
+ if (ca->common_address != asdu_ca)
continue;
if (b->is_command)
continue;
- iec_send_monitor(connection, srv, b);
+ iec_send_monitor(connection, ca, b);
}
}
return true;
@@ -648,6 +669,7 @@
TypeID tid = CS101_ASDU_getTypeID(asdu);
InformationObject io = CS101_ASDU_getElement(asdu, 0);
size_t srv_idx = (size_t)(srv - iec60870_srv);
+ int asdu_ca = CS101_ASDU_getCA(asdu);
size_t i;
struct iec60870_binding *match = NULL;
float fv;
@@ -658,9 +680,13 @@
{
int ioa = InformationObject_getObjectAddress(io);
for (i = 0; i < iec60870_binding_count; i++) {
- if (iec60870_bindings[i].server_index < 0)
+ iec60870_ca_t *ca;
+ if (iec60870_bindings[i].ca_index < 0)
continue;
- if ((size_t)iec60870_bindings[i].server_index != srv_idx)
+ ca = &iec60870_ca[iec60870_bindings[i].ca_index];
+ if ((size_t)ca->server_index != srv_idx)
+ continue;
+ if (ca->common_address != asdu_ca)
continue;
if (!iec60870_bindings[i].is_command)
continue;
@@ -891,7 +917,11 @@
s->time_synced = false;
s->cs_plc_var = NULL;
for (bi = 0; bi < iec60870_binding_count; bi++) {
- if (iec60870_bindings[bi].server_index != si)
+ iec60870_ca_t *ca;
+ if (iec60870_bindings[bi].ca_index < 0)
+ continue;
+ ca = &iec60870_ca[iec60870_bindings[bi].ca_index];
+ if ((size_t)ca->server_index != (size_t)si)
continue;
if (!iec60870_bindings[bi].is_command)
continue;
--- a/iec60870/iec60870_runtime.h Tue May 26 13:50:51 2026 +0200
+++ b/iec60870/iec60870_runtime.h Tue Jun 02 09:02:31 2026 +0200
@@ -4,6 +4,7 @@
#define IEC60870_RUNTIME_%(locstr)s_H
#define IEC60870_NUM_SERVERS_%(locstr)s %(num_servers)s
+#define IEC60870_NUM_CA_%(locstr)s %(num_ca)s
#define IEC60870_NUM_BINDINGS_%(locstr)s %(num_bindings)s
#define IEC60870_MAX_REMOTE_CLIENTS_%(locstr)s %(max_remote_clients)s