Skip to content
This repository has been archived by the owner on Mar 15, 2021. It is now read-only.

CIP EthernetIP protocol implementation #187

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions liota/device_comms/cip_ethernet_ip_device_comms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# -*- coding: utf-8 -*-
Copy link
Contributor

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

Copy link
Author

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?

# ----------------------------------------------------------------------------#
# 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):
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra line

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

105 changes: 105 additions & 0 deletions liota/lib/transports/cip_ethernet_ip.py
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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no credentials support?

Copy link
Author

Choose a reason for hiding this comment

The 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reason for assertion error?

Copy link
Author

Choose a reason for hiding this comment

The 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()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra lines

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok


22 changes: 22 additions & 0 deletions packages/examples/cipethernetip/README.md
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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra lines

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok




93 changes: 93 additions & 0 deletions packages/examples/cipethernetip/cip_socket_graphite.py
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


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add comments over here to explain the flow?

Copy link
Author

Choose a reason for hiding this comment

The 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):
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

https://github.com/vmware/liota/tree/master/examples

Copy link
Author

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove extra lines

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

7 changes: 7 additions & 0 deletions packages/sampleProp.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ LivingRoomTemperatureTopic = "home/living-room/temperature"
LivingRoomHumidityTopic = "home/living-room/humidity"
LivingRoomLightTopic = "home/living-room/light"



#### [CIP EthernetIP Parameters] ####

CipEtherNetIp = "127.0.0.1"
Tag = "Scada"
Index = "0"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading