-
Notifications
You must be signed in to change notification settings - Fork 120
CIP EthernetIP protocol implementation #187
base: master
Are you sure you want to change the base?
Changes from 5 commits
a631d43
4cae282
84c86ba
c6f5f33
2c891e9
ff75f4e
5abdb82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------# | ||
# Copyright © 2015-2016 VMware, Inc. All Rights Reserved. # | ||
# # | ||
# Licensed under the BSD 2-Clause License (the “License”); you may not use # | ||
# this file except in compliance with the License. # | ||
# # | ||
# The BSD 2-Clause License # | ||
# # | ||
# Redistribution and use in source and binary forms, with or without # | ||
# modification, are permitted provided that the following conditions are met:# | ||
# # | ||
# - Redistributions of source code must retain the above copyright notice, # | ||
# this list of conditions and the following disclaimer. # | ||
# # | ||
# - Redistributions in binary form must reproduce the above copyright # | ||
# notice, this list of conditions and the following disclaimer in the # | ||
# documentation and/or other materials provided with the distribution. # | ||
# # | ||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"# | ||
|
||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # | ||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # | ||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # | ||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # | ||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # | ||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # | ||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # | ||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # | ||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # | ||
# THE POSSIBILITY OF SUCH DAMAGE. # | ||
# ----------------------------------------------------------------------------# | ||
|
||
import logging | ||
|
||
from liota.device_comms.device_comms import DeviceComms | ||
from liota.lib.transports.cip_ethernet_ip import CipEthernetIp | ||
import random | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class CipEtherNetIpDeviceComms(DeviceComms): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename to CipDeviceComms |
||
""" | ||
DeviceComms for EtherNet/IP protocol | ||
""" | ||
|
||
def __init__( | ||
self, | ||
host, | ||
port=None, | ||
timeout=None, | ||
dialect=None, | ||
profiler=None, | ||
udp=False, | ||
broadcast=False, | ||
source_address=None): | ||
|
||
""" | ||
:param host: CIP EtherNet/IP IP | ||
:param port: CIP EtherNet/IP Port | ||
:param timeout: Connection timeout | ||
:param dialect: An EtherNet/IP CIP dialect, if not logix.Logix | ||
:param profiler: If using a Python profiler, provide it to disable around I/O code | ||
:param udp: Establishes a UDP/IP socket to use for request (eg. List Identity) | ||
:param broadcast: Avoids connecting UDP/IP sockets; may receive many replies | ||
:param source_address: Bind to a specific local interface (Default: 0.0.0.0:0) | ||
|
||
""" | ||
|
||
self.host = host | ||
self.port = port | ||
self.timeout = timeout | ||
self.dialect = dialect | ||
self.profiler = profiler | ||
self.udp = udp | ||
self.broadcast = broadcast | ||
self.source_address = source_address | ||
|
||
if host is None: | ||
raise TypeError("Host can't be None") | ||
|
||
self._connect() | ||
|
||
def _connect(self): | ||
self.client = CipEthernetIp( | ||
self.host, | ||
self.port, | ||
self.timeout, | ||
self.dialect, | ||
self.profiler, | ||
self.udp, | ||
self.broadcast, | ||
self.source_address) | ||
self.client.connect() | ||
|
||
def _disconnect(self): | ||
self.client.disconnect() | ||
|
||
def send(self, tag, elements, data, tag_type): | ||
if data is None: | ||
raise TypeError("Data can't be none") | ||
else: | ||
self.client.send(tag, elements, data, tag_type) | ||
|
||
def receive(self, tag, index): | ||
data = self.client.receive(tag, index) | ||
return data | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extra line There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------# | ||
# Copyright © 2015-2016 VMware, Inc. All Rights Reserved. # | ||
# # | ||
# Licensed under the BSD 2-Clause License (the “License”); you may not use # | ||
# this file except in compliance with the License. # | ||
# # | ||
# The BSD 2-Clause License # | ||
# # | ||
# Redistribution and use in source and binary forms, with or without # | ||
# modification, are permitted provided that the following conditions are met:# | ||
# # | ||
# - Redistributions of source code must retain the above copyright notice, # | ||
# this list of conditions and the following disclaimer. # | ||
# # | ||
# - Redistributions in binary form must reproduce the above copyright # | ||
# notice, this list of conditions and the following disclaimer in the # | ||
# documentation and/or other materials provided with the distribution. # | ||
# # | ||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"# | ||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # | ||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # | ||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # | ||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # | ||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # | ||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # | ||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # | ||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # | ||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # | ||
# THE POSSIBILITY OF SUCH DAMAGE. # | ||
# ----------------------------------------------------------------------------# | ||
|
||
import logging | ||
import os | ||
import sys | ||
import time | ||
import cpppo | ||
from random import randint | ||
from cpppo.server.enip import client | ||
from cpppo.server.enip.getattr import attribute_operations | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class CipEthernetIp: | ||
''' | ||
EtherNet Industrial Protocol implementation for LIOTA. It uses python-cpppo internally. | ||
''' | ||
|
||
def __init__(self, host, port=None, timeout=None, dialect=None, profiler=None, udp=False, broadcast=False, source_address=None): | ||
""" | ||
:param host: CIP EtherNet/IP IP | ||
:param port: CIP EtherNet/IP Port | ||
:param timeout: Connection timeout | ||
:param dialect: An EtherNet/IP CIP dialect, if not logix.Logix | ||
:param profiler: If using a Python profiler, provide it to disable around I/O code | ||
:param udp: Establishes a UDP/IP socket to use for request (eg. List Identity) | ||
:param broadcast: Avoids connecting UDP/IP sockets; may receive many replies | ||
:param source_address: Bind to a specific local interface (Default: 0.0.0.0:0) | ||
""" | ||
|
||
self.host = host | ||
self.port = port | ||
self.timeout = timeout | ||
self.dialect = dialect | ||
self.profiler = profiler | ||
self.udp = udp | ||
self.broadcast = broadcast | ||
self.source_address = source_address | ||
|
||
def connect(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there no credentials support? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, there is no credentials support |
||
with client.connector(host=self.host) as self.conn: | ||
log.info("Connected to Server") | ||
|
||
def send(self, tag, elements, data, tag_type): | ||
self.tag = tag | ||
self.elements = elements | ||
self.data = data | ||
self.tag_type = tag_type | ||
|
||
try: | ||
req = self.conn.write(self.tag, elements=self.elements, data=self.data, | ||
tag_type=self.tag_type) | ||
except AssertionError as exc: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reason for assertion error? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In cpppo library, which is being used here, the write() function calls another function "unconnected_send()" - where there is an assert statement https://github.com/pjkundert/cpppo/blob/master/server/enip/client.py#L768 and that is the reason for the assertion error. |
||
log.info("Response timed out!!") | ||
except socket.error as exc: | ||
log.exception("Couldn't send command: %s" % (exc)) | ||
|
||
def receive(self, tag, index): | ||
with self.conn: | ||
Tag = tag+'['+index+']' | ||
try: | ||
request_ = self.conn.read(Tag) | ||
assert self.conn.readable(timeout=1.0), "Failed to receive reply" | ||
response = next(self.conn) | ||
data = response['enip']['CIP']['send_data']['CPF']['item'][1]['unconnected_send']['request']['read_frag']['data'][0] | ||
except AssertionError as error: | ||
log.exception("Failed to receive reply") | ||
return data if data else None | ||
|
||
def disconnect(self): | ||
if self.conn is not None: | ||
self.conn.close() | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extra lines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Using CIP EtherNet Industrial Protocol as Transport in LIOTA | ||
|
||
LIOTA offers CIP EtherNet Industrial protocol as transport at Device end via [cip_ethernet_ip_device_comms](https://github.com/NithyaElango/liota/blob/CipEthernetIP/liota/device_comms/cip_ethernet_ip_device_comms.py) | ||
|
||
## Starting the server | ||
|
||
To get started with the CIP protocol, a server must be started in any linux machine. Install cpppo in that machine using | ||
"sudo pip install cpppo", then to start the server, "python -m cpppo.server.enip -v Scada=DINT[1]" | ||
|
||
|
||
## Using cip_ethernet_ip_device_comms | ||
|
||
Initially run the writer program which keeps writing data to the server, check this [simulator](https://github.com/NithyaElango/liota/blob/CipEthernetIP/tests/cipethernetip/cip_ethernet_ip_simulator.py) for the code. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This piece of code should be checked-in examples folder under CIP. There shouldn't be the reference to any developer branch as we are not sure if the code will exist forever. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This simulator code resides under liota/tests, should I move into liota/examples/CIP folder? |
||
Modify the variable "HOST" in this [simulator](https://github.com/NithyaElango/liota/blob/CipEthernetIP/tests/cipethernetip/cip_ethernet_ip_simulator.py) if the server is running in a different machine. If not, the server,simulator and liota program are running in the same machine, then nothing needs to be changed. | ||
|
||
Then, EtherNet/IP related parameters required in `send()` and `receive()` like tags, datatype etc., will be mentioned while starting the server. Please refer this [example](https://github.com/NithyaElango/liota/blob/CipEthernetIP/packages/examples/cipethernetip/cip_socket_graphite.py) which reads the data from the server and send it to DCC. | ||
|
||
In SampleProp.conf, the CipEtherNetIp should be changed to the IP of the server, if the server is running in a different machine.Alos, the Tag should be changed if anyother tag is mentioned while starting the server other than Scada. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extra lines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# -*- coding: utf-8 -*- | ||
# ----------------------------------------------------------------------------# | ||
# Copyright © 2015-2016 VMware, Inc. All Rights Reserved. # | ||
# # | ||
# Licensed under the BSD 2-Clause License (the “License”); you may not use # | ||
# this file except in compliance with the License. # | ||
# # | ||
# The BSD 2-Clause License # | ||
# # | ||
# Redistribution and use in source and binary forms, with or without # | ||
# modification, are permitted provided that the following conditions are met:# | ||
# # | ||
# - Redistributions of source code must retain the above copyright notice, # | ||
# this list of conditions and the following disclaimer. # | ||
# # | ||
# - Redistributions in binary form must reproduce the above copyright # | ||
# notice, this list of conditions and the following disclaimer in the # | ||
# documentation and/or other materials provided with the distribution. # | ||
# # | ||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"# | ||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # | ||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # | ||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # | ||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # | ||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # | ||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # | ||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # | ||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # | ||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF # | ||
# THE POSSIBILITY OF SUCH DAMAGE. # | ||
# ----------------------------------------------------------------------------# | ||
|
||
from liota.core.package_manager import LiotaPackage | ||
from liota.dcc_comms.socket_comms import SocketDccComms | ||
from liota.dccs.graphite import Graphite | ||
from liota.entities.metrics.metric import Metric | ||
from liota.entities.edge_systems.dell5k_edge_system import Dell5KEdgeSystem | ||
from liota.lib.utilities.utility import read_user_config | ||
from liota.device_comms.cip_ethernet_ip_device_comms import CipEtherNetIpDeviceComms | ||
from liota.entities.devices.simulated_device import SimulatedDevice | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you add comments over here to explain the flow? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
dependencies = ["graphite"] | ||
|
||
|
||
def read_value(conn, tag, index): | ||
value = conn.receive(tag, index) | ||
return value | ||
|
||
|
||
class PackageClass(LiotaPackage): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should also be a normal Liota example with the package example on the usage of this protocol as we haven't moved away or neither decided to discard the current existing examples. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You want me to write an liota example program other than a liota package for the usage of this protocol? And it should be under liota/examples right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wrote an example program for the same usage and placed it under liota/examples. |
||
|
||
def run(self, registry): | ||
|
||
# Acquire resources from registry | ||
graphite = registry.get("graphite") | ||
|
||
config_path = registry.get("package_conf") | ||
|
||
self.config = read_user_config(config_path + '/sampleProp.conf') | ||
|
||
self.cip_ethernet_ip_conn = CipEtherNetIpDeviceComms(host=self.config['CipEtherNetIp']) | ||
|
||
self.tag = self.config['Tag'] | ||
self.index = self.config['Index'] | ||
|
||
|
||
cip_ethernet_device = SimulatedDevice(self.config['DeviceName'], "Test") | ||
reg_cip_ethernet_device = graphite.register(cip_ethernet_device) | ||
|
||
self.metrics = [] | ||
|
||
cip_ethernet_device_metric_name = "CIP.ethernetIP" | ||
|
||
cip_ethernet_device_metric = Metric( | ||
name=cip_ethernet_device_metric_name, | ||
unit=None, | ||
interval=5, | ||
sampling_function=lambda: read_value(self.cip_ethernet_ip_conn, self.tag, self.index) | ||
) | ||
|
||
reg_cip_ethernet_device_metric = graphite.register(cip_ethernet_device_metric) | ||
graphite.create_relationship( | ||
reg_cip_ethernet_device, | ||
reg_cip_ethernet_device_metric) | ||
reg_cip_ethernet_device_metric.start_collecting() | ||
self.metrics.append(reg_cip_ethernet_device_metric) | ||
|
||
def clean_up(self): | ||
for metric in self.metrics: | ||
metric.stop_collecting() | ||
self.cip_ethernet_ip_conn._disconnect() | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extra lines There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ paho-mqtt==1.3.1 | |
pint==0.7.2 | ||
websocket-client==0.37.0 | ||
uptime==3.0.1 | ||
cpppo==3.9.7 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename cip_device_comms.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CIP could be implemented over four different networks.
I have done it over ethernet, so It is known as EtherNet/IP, and thus me and Piyush named it as cip_ethernet_ip.
Is it necessary to rename?