lpcmanager

iec60870: add draft C template files and linking with crossbuilt IEC60870 library.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import absolute_import
import os
from ConfigTreeNode import ConfigTreeNode
from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_INPUT, \
LOCATION_VAR_OUTPUT, LOCATION_VAR_MEMORY
import util.paths as paths
iec60870_path = paths.ThirdPartyPath("IEC60870")
# (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
iec60870_asdu_types = {
"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"),
}
LOCATION_TYPES = {
"I": LOCATION_VAR_INPUT,
"Q": LOCATION_VAR_OUTPUT,
"M": LOCATION_VAR_MEMORY,
}
# XSD fragment for the shared IEC 60870-5-104 connection parameters
# (used in both server and client node XSD definitions)
_IEC60870_CONN_PARAMS_XSD = """\
<xsd:attribute name="APCI_k" use="optional" default="12">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="32767"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="APCI_w" use="optional" default="8">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="32767"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Timeout_t0" use="optional" default="10">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Timeout_t1" use="optional" default="15">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Timeout_t2" use="optional" default="10">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Timeout_t3" use="optional" default="20">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Use_TLS" type="xsd:boolean" use="optional" default="false"/>
<xsd:attribute name="CA_Size" use="optional" default="2">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="2"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="IOA_Size" use="optional" default="3">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="3"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="COT_Has_OA" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="OA_Value" use="optional" default="10">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Use_Local_Timezone" type="xsd:boolean" use="optional" default="true"/>
"""
#
# D A T A P O I N T
#
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:complexType>
<xsd:attribute name="ASDU_Type" type="xsd:string" use="optional" default="M_SP_NA_1 - Single point"/>
<xsd:attribute name="IOA" use="optional" default="0">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="16777215"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Nr_of_Points" use="optional" default="1">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="65535"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""
def GetParamsAttributes(self, path=None):
infos = ConfigTreeNode.GetParamsAttributes(self, path=path)
for element in infos:
if element["name"] == "IEC60870DataPoint":
for child in element["children"]:
if child["name"] == "ASDU_Type":
_list = sorted(iec60870_asdu_types.keys())
child["type"] = _list
return infos
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]
loc_type = LOCATION_TYPES[direction]
entries = []
for offset in range(ioa, ioa + count):
entries.append({
"name": desc + " IOA " + str(offset),
"type": loc_type,
"size": datasize,
"IEC_type": datatype,
"var_name": "IEC104_" + str(type_id) + "_" + str(offset),
"location": size_code + ".".join(
[str(i) for i in current_location]) + "." + str(offset),
"description": desc,
"children": []})
return {"name": name,
"type": LOCATION_CONFNODE,
"location": ".".join(
[str(i) for i in current_location]) + ".x",
"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)
#
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:complexType>
<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 +
"""
</xsd:complexType>
</xsd:element>
</xsd:schema>
""")
CTNChildrenTypes = [("IEC60870DataPoint", _DataPointPlug, "Data Point")]
PlugType = "IEC60870Server"
def __init__(self):
loc_str = ".".join(map(str, self.GetCurrentLocation()))
self.IEC60870ServerNode.setConfiguration_Name(
"IEC60870 Server " + loc_str)
def GetNodeCount(self):
return (1, 0)
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()
entries = []
entries.append({
"name": "Read Request Counter",
"type": LOCATION_VAR_MEMORY,
"size": 32,
"IEC_type": "UDINT",
"var_name": "var_name",
"location": "D" + ".".join(
[str(i) for i in current_location]) + ".0",
"description": "IEC60870 read request counter",
"children": []})
entries.append({
"name": "Write Request Counter",
"type": LOCATION_VAR_MEMORY,
"size": 32,
"IEC_type": "UDINT",
"var_name": "var_name",
"location": "D" + ".".join(
[str(i) for i in current_location]) + ".1",
"description": "IEC60870 write request counter",
"children": []})
entries.append({
"name": "Connection Active Flag",
"type": LOCATION_VAR_MEMORY,
"size": 1,
"IEC_type": "BOOL",
"var_name": "var_name",
"location": "X" + ".".join(
[str(i) for i in current_location]) + ".2",
"description": "IEC60870 connection active flag",
"children": []})
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",
"children": entries}
def CTNGenerate_C(self, buildpath, locations):
return [], "", False
#
# C O N T R O L L I N G S T A T I O N (Client)
#
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:complexType>
<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:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="65534"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="Polling_Interval_ms" use="optional" default="1000">
<xsd:simpleType>
<xsd:restriction base="xsd:unsignedLong">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="2147483647"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
"""
+ _IEC60870_CONN_PARAMS_XSD +
"""
</xsd:complexType>
</xsd:element>
</xsd:schema>
""")
CTNChildrenTypes = [("IEC60870DataPoint", _DataPointPlug, "Data Point")]
PlugType = "IEC60870Client"
def __init__(self):
loc_str = ".".join(map(str, self.GetCurrentLocation()))
self.IEC60870ClientNode.setConfiguration_Name(
"IEC60870 Client " + loc_str)
def GetNodeCount(self):
return (1, 0)
def GetConfigName(self):
return self.IEC60870ClientNode.getConfiguration_Name()
def GetVariableLocationTree(self):
current_location = self.GetCurrentLocation()
name = self.BaseParams.getName()
entries = []
entries.append({
"name": "Connection Status",
"type": LOCATION_VAR_MEMORY,
"size": 8,
"IEC_type": "BYTE",
"var_name": "var_name",
"location": "B" + ".".join(
[str(i) for i in current_location]) + ".0",
"description": "Connection status (0=disconnected, 1=connected, "
"2=connecting, 3=error)",
"children": []})
entries.append({
"name": "Interrogation Trigger",
"type": LOCATION_VAR_MEMORY,
"size": 1,
"IEC_type": "BOOL",
"var_name": "var_name",
"location": "X" + ".".join(
[str(i) for i in current_location]) + ".1",
"description": "Trigger general interrogation",
"children": []})
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",
"children": entries}
def CTNGenerate_C(self, buildpath, locations):
return [], "", False
#
# R O O T C L A S S
#
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:complexType>
<xsd:attribute name="MaxRemoteClients" use="optional" default="10">
<xsd:simpleType>
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="65535"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:attribute>
</xsd:complexType>
</xsd:element>
</xsd:schema>
"""
CTNChildrenTypes = [
("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():
total = tuple(
x1 + x2 for x1, x2 in zip(total, child.GetNodeCount()))
return total
def GetIPServerPortNumbers(self):
port_numbers = []
for child in self.IECSortedChildren():
if child.CTNType == "IEC60870Server":
port_numbers.extend(child.GetIPServerPortNumbers())
return port_numbers
def GetConfigNames(self):
names = []
for child in self.IECSortedChildren():
names.append(
(child.GetCurrentLocation(), child.GetConfigName()))
return names
def CTNGenerate_C(self, buildpath, locations):
node_config_names = []
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_message = _(
"Error: IEC60870 plugin nodes %%{a1}.x and %%{a2}.x "
"use the same Configuration_Name \"{a3}\".\n"
).format(
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)
ip_ports = []
for CTNInstance in self.GetCTRoot().IterChildren():
if CTNInstance.CTNType == "iec60870":
ip_ports.extend(CTNInstance.GetIPServerPortNumbers())
i = 0
for loc1, addr1, port1 in ip_ports[:-1]:
i = i + 1
for loc2, addr2, port2 in ip_ports[i:]:
if (port1 == port2) and (
(addr1 == addr2)
or (addr1 in ("", "*", "#ANY#"))
or (addr2 in ("", "*", "#ANY#"))
):
error_message = _(
"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"
).format(
a1=_lt_to_str(loc1), a2=_lt_to_str(loc2),
a3=port1, a4=addr1, a5=addr2)
self.FatalError(error_message)
# define a unique name for the generated C and h files
current_location = self.GetCurrentLocation()
prefix = "_".join(map(str, current_location))
gen_iec60870_c_path = os.path.join(buildpath, "iec60870_%s.c" % prefix)
gen_iec60870_h_path = os.path.join(buildpath, "iec60870_%s.h" % prefix)
c_filename = os.path.join(os.path.split(__file__)[0], "iec60870_runtime.c")
h_filename = os.path.join(os.path.split(__file__)[0], "iec60870_runtime.h")
# TODO: get located variables and merge them with template files
loc_vars = []
loc_dict = {"locstr": "_".join(map(str, self.GetCurrentLocation()))}
loc_dict["loc_vars"] = "\n".join(loc_vars)
# create output files using templates
iec60870_main = open(h_filename).read() % loc_dict
f = open(gen_iec60870_h_path, 'w')
f.write(iec60870_main)
f.close()
iec60870_main = open(c_filename).read() % loc_dict
f = open(gen_iec60870_c_path, 'w')
f.write(iec60870_main)
f.close()
LDFLAGS = []
LDFLAGS.append(" \"-L" + iec60870_path + "\"")
#TODO: fix lib name
LDFLAGS.append(" \"" + os.path.join(iec60870_path, "liblib60870.a") + "\"")
LDFLAGS.append(" \"-Wl,-rpath," + iec60870_path + "\"")
return [(gen_iec60870_c_path, ' -I"' + iec60870_path + '"')], LDFLAGS, False