diff --git a/minicps/protocols.py b/minicps/protocols.py index 7b5ea00..946f58e 100644 --- a/minicps/protocols.py +++ b/minicps/protocols.py @@ -24,10 +24,11 @@ import shlex import subprocess +from multiprocessing import Process + import cpppo import pymodbus - # Protocol {{{1 class Protocol(object): @@ -128,7 +129,6 @@ def _send_multiple(self, what, values, address): # }}} # EnipProtocol {{{1 - # TODO: support vectorial tags def, read and write # int def: SCADA=INT[3] # int read: SCADA[0-3] @@ -137,12 +137,9 @@ def _send_multiple(self, what, values, address): # string read: TEXT # string write: 'TEXT[0]=(SSTRING)"Hello world"' class EnipProtocol(Protocol): - """EnipProtocol manager. - name: enip - - EnipProtocol manages python cpppo library, Look at the original + EnipProtocol manages python enip library, Look at the original documentation for more information. Tags are passed as a tuple of tuples, if the tuple contains only 1 tag @@ -151,30 +148,23 @@ class EnipProtocol(Protocol): eg: tag = (('SENSOR1'), ) - Supported modes: - - 0: client only - - 1: tcp enip server - - Supported tag data types: + Supported tag datatypes: - SINT (8-bit) - INT (16-bit) - DINT (32-bit) - REAL (32-bit float) - BOOL (8-bit, bit #0) - - SSTRING[10] (simple string of 10 chars) + - SSTRING[10] (simple string of 10 chars) # TODO: Not supported yet. """ # server ports _TCP_PORT = ':44818' - _UDP_PORT = ':2222' + # _UDP_PORT = ':2222' # not supported def __init__(self, protocol): super(EnipProtocol, self).__init__(protocol) - self._client_cmd = sys.executable + ' -m cpppo.server.enip.client ' - - # NOTE: set up logging if sys.platform.startswith('linux'): self._client_log = 'logs/enip_client ' else: @@ -189,127 +179,38 @@ def __init__(self, protocol): else: raise OSError - print 'DEBUG EnipProtocol server addr: ', self._server['address'] + # print 'DEBUG EnipProtocol server addr: ', self._server['address'] if self._server['address'].find(':') == -1: - print 'DEBUG: concatenating server address with default port.' + # print 'DEBUG: concatenating server address with default port.' self._server['address'] += EnipProtocol._TCP_PORT elif not self._server['address'].endswith(EnipProtocol._TCP_PORT): - print 'WARNING: not using std enip %s TCP port' % \ - EnipProtocol._TCP_PORT - - self._server_cmd = sys.executable + ' -m cpppo.server.enip ' + print 'WARNING: not using std enip %s TCP port' % EnipProtocol._TCP_PORT self._server_subprocess = EnipProtocol._start_server( address=self._server['address'], tags=self._server['tags']) # TODO: udp enip server - elif self._mode == 2: - - # NOTE: set up logging - if sys.platform.startswith('linux'): - self._server_log = 'logs/enip_udp_server ' - else: - raise OSError - - print 'DEBUG EnipProtocol server addr: ', self._server['address'] - if self._server['address'].find(':') == -1: - print 'DEBUG: concatenating server address with default port.' - self._server['address'] += EnipProtocol._UDP_PORT - - elif not self._server['address'].endswith(EnipProtocol._UDP_PORT): - print 'WARNING: not using std enip %s UDP port' % \ - EnipProtocol._UDP_PORT - # TODO: add --udp flag - self._server_cmd = sys.executable + ' -m cpppo.server.enip ' - if sys.platform.startswith('linux'): - self._server_log = 'logs/enip_udp_server ' - else: - raise OSError - - # TODO: start UDP enip server - - @classmethod - def _tuple_to_cpppo_tag(cls, what, value=None, serializer=':'): - """Returns a cpppo string to read/write a server. - - Can be used both to generate cpppo scalar read query, like - SENSOR1:1, and scalar write query, like ACTUATOR1=1. - - Value correctness is up the client and it is blindly - converted to string and appended to the cpppo client query. - """ - - tag_string = '' - tag_string += str(what[0]) - - if len(what) > 1: - for field in what[1:]: - tag_string += EnipProtocol._SERIALIZER - tag_string += str(field) - if value is not None: - if type(value) is str: - # TODO: add support for SSTRING tags - # ''' enip_client -a 192.168.1.20 'README:2[0]=(SSTRING)"string"' ''' - pass - tag_string += '=' - tag_string += str(value) - # print 'DEBUG _tuple_to_cpppo_tag tag_string: ', tag_string - - return tag_string + elif self._mode == 2: pass @classmethod - def _tuple_to_cpppo_tags(cls, tags, serializer=':'): - """Returns a cpppo tags string to init a server. - - cpppo API: SENSOR1=INT SENSOR2=REAL ACTUATOR1=INT + def _nested_tuples_to_enip_tags(cls, tags): + """ Tuple to input format for server script init + :tags: ((SENSOR1, BOOL), (ACTUATOR1, 1, SINT), (TEMP2, REAL)) + :return: a string of the tuples (name and type separated by serializer) separated by white space + E.g. 'sensor1@BOOL actuator1:1@SINT temp2@REAL' """ - - tags_string = '' + tag_list = [] for tag in tags: - tags_string += str(tag[0]) - for field in tag[1:-1]: - tags_string += serializer - # print 'DEBUG _tuple_to_cpppo_tags field: ', field - tags_string += str(field) - - tags_string += '=' - tags_string += str(tag[-1]) - tags_string += ' ' - print 'DEBUG enip server tags_string: ', tags_string - - return tags_string + tag = [str(x) for x in tag] + tag_list.append("{0}@{1}".format(':'.join(tag[:-1]), tag[-1])) + return ' '.join(tag_list) - @classmethod - def _start_server(cls, address, tags): - """Start a cpppo enip server. - - The command used to start the server is generated by - ``_start_server_cmd``. - - Notice that the client has to manage the new process, - eg:kill it after use. - - :address: to serve - :tags: to serve - """ - - try: - cmd = EnipProtocol._start_server_cmd(address, tags) - server = subprocess.Popen(cmd, shell=False) - - return server - - except Exception as error: - print 'ERROR enip _start_server: ', error - - # TODO: how to start a UDP cpppo server? - # TODO: parametric PRINT_STDOUT and others @classmethod def _start_server_cmd(cls, address='localhost:44818', tags=(('SENSOR1', 'INT'), ('ACTUATOR1', 'INT'))): - """Build a subprocess.Popen cmd string for cpppo server. + """Build a Popen cmd string for enip server. Tags can be any tuple of tuples. Each tuple has to contain a set of string-convertible fields, the last one has to be a string containing @@ -318,124 +219,150 @@ def _start_server_cmd(cls, address='localhost:44818', Consistency between enip server key-values and state key-values has to be guaranteed by the client. - :address: to serve :tags: to serve - - :returns: list of strings generated with shlex.split, - passable to subprocess.Popen object + :returns: cmd string passable to Popen object """ - CMD = sys.executable + ' -m cpppo.server.enip ' - PRINT_STDOUT = '--print ' - HTTP = '--web %s:80 ' % address[0:address.find(':')] - # print 'DEBUG: enip _start_server_cmd HTTP: ', HTTP - ADDRESS = '--address ' + address + ' ' - TAGS = EnipProtocol._tuple_to_cpppo_tags(tags) + if address.find(":") != -1: + address = address.split(":")[0] - if sys.platform.startswith('linux'): - SHELL = '/bin/bash -c ' - LOG = '--log logs/protocols_tests_enip_server ' - else: + ADDRESS = '-i ' + address + ' ' + TAGS = '-t ' + cls._nested_tuples_to_enip_tags(tags) + + ENV = "python3" + CMD = " -m enipserver.main " + + if not sys.platform.startswith('linux'): raise OSError cmd = shlex.split( + ENV + CMD + - PRINT_STDOUT + - LOG + ADDRESS + TAGS ) - print 'DEBUG enip _start_server cmd: ', cmd + # print 'DEBUG enip _start_server cmd: ', cmd return cmd + @classmethod + def _start_server(cls, address, tags): + """Start a enip server. + + Notice that the client has to manage the new process, + eg:kill it after use. + + :address: to serve + :tags: to serve + """ + try: + cmd = cls._start_server_cmd(address, tags) + cls.server = subprocess.Popen(cmd, shell=False) + return cls.server + + except Exception as error: + print 'ERROR enip _start_server: ', error + @classmethod def _stop_server(cls, server): """Stop an enip server. - :server: subprocess.Popen object + :server: Popen object """ - try: server.kill() except Exception as error: print 'ERROR stop enip server: ', error - def _send(self, what, value, address='localhost:44818', **kwargs): - """Send (write) a value to another host. + def _send(self, what, value, address='localhost', **kwargs): + """Send (serve) a value. It is a blocking operation the parent process will wait till the child cpppo process returns. - :what: tuple addressing what + :what: tag :value: sent - :address: ip[:port] + :address: ip """ - - tag_string = '' - tag_string = EnipProtocol._tuple_to_cpppo_tag(what, value) - # print 'DEBUG enip _send tag_string: ', tag_string + def infer_tag_type(val): + if type(val) is float: _typ = "REAL" + elif type(val) is int: _typ = "INT" + elif type(val) is str: _typ = "STRING" + elif type(val) is bool: _typ = "BOOL" + else: _typ = "unsupported" + return _typ + + tag = ':'.join([str(x) for x in what]) + typ = infer_tag_type(value) + + ENV = "python " #sys.executable + CMD = "{0}pyenip/single_write.py ".format(self._minicps_path) + ADDRESS = "-i {0} ".format(address) + TAG = "-t {0} ".format(tag) + VAL = "-v '{}' ".format(str(value)) + TYP = "--type {}".format(typ) cmd = shlex.split( - self._client_cmd + - '--log ' + self._client_log + - '--address ' + address + - ' ' + tag_string + ENV + + CMD + + ADDRESS + + TAG + + VAL + + TYP ) - # print 'DEBUG enip _send cmd shlex list: ', cmd + # print 'DEBUG enip _start_server cmd: ', cmd - # TODO: pipe stdout and return the sent value try: - client = subprocess.Popen(cmd, shell=False) - client.wait() + client = subprocess.Popen(cmd, shell=False, + stdout=subprocess.PIPE) + + # client.communicate is blocking + raw_out = client.communicate() + return raw_out[0] except Exception as error: print 'ERROR enip _send: ', error - def _receive(self, what, address='localhost:44818', **kwargs): - """Receive (read) a value from another host. + def _receive(self, what, address='localhost'): + + """Receive a (requested) value. It is a blocking operation the parent process will wait till the child cpppo process returns. - :what: to ask for + :what: tag :address: to receive from - :returns: tag value as a `str` + :returns: tuple of (value, datatype) """ + tag_name = ':'.join([str(x) for x in what]) - tag_string = '' - tag_string = EnipProtocol._tuple_to_cpppo_tag(what) + ENV = "python " #sys.executable + CMD = "{0}pyenip/single_read.py ".format(self._minicps_path) + ADDRESS = "-i {0} ".format(address) + TAG = "-t {0} ".format(tag_name) cmd = shlex.split( - self._client_cmd + - '--log ' + self._client_log + - '--address ' + address + - ' ' + tag_string + ENV + + CMD + + ADDRESS + + TAG ) - # print 'DEBUG enip _receive cmd shlex list: ', cmd + # print 'DEBUG enip _start_server cmd: ', cmd try: client = subprocess.Popen(cmd, shell=False, - stdout=subprocess.PIPE) + stdout=subprocess.PIPE) # client.communicate is blocking raw_out = client.communicate() # print 'DEBUG enip _receive raw_out: ', raw_out - - # value is stored as first tuple element - # between a pair of square brackets - raw_string = raw_out[0] - out = raw_string[(raw_string.find('[') + 1):raw_string.find(']')] - - return out + return raw_out[0] except Exception as error: print 'ERROR enip _receive: ', error - # }}} - # ModbusProtocol {{{1 class ModbusProtocol(Protocol): @@ -490,7 +417,7 @@ def __init__(self, protocol): print 'DEBUG ModbusProtocol server addr: ', self._server['address'] if self._server['address'].find(':') == -1: - print 'DEBUG: concatenating server address with default port.' + # print 'DEBUG: concatenating server address with default port.' self._server['address'] += ModbusProtocol._TCP_PORT elif not self._server['address'].endswith(ModbusProtocol._TCP_PORT): @@ -581,7 +508,7 @@ def _start_server_cmd(cls, cmd_path, address='localhost:502', MODE + DI + CO + IR + HR ) - print 'DEBUG modbus _start_server cmd: ', cmd + # print 'DEBUG modbus _start_server cmd: ', cmd return cmd diff --git a/minicps/pyenip/single_read.py b/minicps/pyenip/single_read.py new file mode 100755 index 0000000..865bed8 --- /dev/null +++ b/minicps/pyenip/single_read.py @@ -0,0 +1,37 @@ +#!/usr/bin/python2 +import argparse +import sys +from pycomm.ab_comm.clx import Driver as ClxDriver + +def read_tag(address, tag_name): + plc = ClxDriver() + if plc.open(address): + tagg = plc.read_tag(tag_name) + plc.close() + return (tagg) + else: + return "false" + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + + parser.add_argument('-i', type=str, dest='ip', required=True, + help='request ip') + + parser.add_argument('-t', '--tag', type=str, dest='tag', required=True, + help='request tag with type') + + args = parser.parse_args() + + # split tags if possible to retrieve name + tag_name = args.tag.split("@")[0] + + # retrieve the ip and ignore the port + address = args.ip.split(":")[0] + + res = read_tag(address, tag_name) + + val = "err" if (res is None or res=="false") else res[0] + sys.stdout.write("%s" % val) diff --git a/minicps/pyenip/single_write.py b/minicps/pyenip/single_write.py new file mode 100755 index 0000000..fb74df9 --- /dev/null +++ b/minicps/pyenip/single_write.py @@ -0,0 +1,58 @@ +#!/usr/bin/python2 + +import sys +import argparse +from pycomm.ab_comm.clx import Driver as ClxDriver + +def convert_value_to_proper_type(tag_type, val): + if tag_type == "INT": value = int(val) + elif tag_type == "REAL": value = float(val) + elif tag_type == "BOOL": + if val == "False" or val == 0: value = False + else: value = True + else: value = str(val) + return value + +def write_tag(tag_name, value, tag_type): + plc = ClxDriver() + if plc.open(address): + temp = plc.write_tag(tag_name, value, tag_type) + plc.close() + return temp + else: + return "false" + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + + parser.add_argument('-i', type=str, dest='ip', required=True, + help='request ip') + + parser.add_argument('-t', '--tag', type=str, dest='tag', required=True, + help='request tag with type. format: NAME[:ID][:ID]...') + + parser.add_argument('-v', '--val', dest='val', required=True, + help='value to be written.') + + parser.add_argument('--type', dest='typ', required=True, + help='[INT][STRING][REAL]') + + args = parser.parse_args() + + tag_name = args.tag + tag_type = args.typ + value = convert_value_to_proper_type(tag_type, args.val) + + # retrieve the ip and ignore the port + address = args.ip.split(":")[0] + + if tag_type not in ["INT", "STRING", "REAL", "BOOL"]: + print("single_write.py: error: tag type is invalid.") + print("usage: single_write.py -h for help") + raise ValueError("single_write.py: error: tag type is invalid. Only INT, STRING, BOOL, and REAL is supported.") + + res = write_tag(tag_name, value, tag_type) + + val = "err" if (res is None or res=="false") else res + sys.stdout.write("%s" % res) diff --git a/tests/protocols_tests.py b/tests/protocols_tests.py index 119a32e..a896303 100644 --- a/tests/protocols_tests.py +++ b/tests/protocols_tests.py @@ -11,8 +11,8 @@ import shlex import time -import pymodbus -import cpppo +#import pymodbus +#import cpppo from minicps.protocols import Protocol, EnipProtocol, ModbusProtocol @@ -26,14 +26,17 @@ class TestProtocol(): # }}} -# TestEnipProtocol {{{1 +# # TestEnipProtocol {{{1 class TestEnipProtocol(): # NOTE: second tuple element is the process id TAGS = ( ('SENSOR1', 1, 'INT'), ('SENSOR1', 2, 'INT'), - ('ACTUATOR1', 'INT')) + ('ACTUATOR1', 'INT'), + ('FLAG101', 'STRING'), + ('FLAG201', 2, 'STRING')) + SERVER = { 'address': 'localhost:44818', 'tags': TAGS @@ -61,6 +64,7 @@ def test_server_start_stop(self): try: server = EnipProtocol._start_server(ADDRESS, TAGS) + time.sleep(1) EnipProtocol._stop_server(server) except Exception as error: @@ -83,6 +87,7 @@ def test_init_server(self): try: server = EnipProtocol( protocol=TestEnipProtocol.CLIENT_SERVER_PROTOCOL) + time.sleep(1) eq_(server._name, 'enip') server._stop_server(server._server_subprocess) del server @@ -93,9 +98,10 @@ def test_init_server(self): def test_server_multikey(self): ADDRESS = 'localhost:44818' # TEST port - TAGS = (('SENSOR1', 1, 'INT'), ('ACTUATOR1', 'INT')) + TAGS = (('SENSOR1', 1, 'INT'), ('ACTUATOR1', 'INT'), ('FLAG2', 2, 'STRING')) try: server = EnipProtocol._start_server(ADDRESS, TAGS) + time.sleep(1) EnipProtocol._stop_server(server) except Exception as error: print 'ERROR test_server_multikey: ', error @@ -107,23 +113,32 @@ def test_send_multikey(self): protocol=TestEnipProtocol.CLIENT_PROTOCOL) ADDRESS = 'localhost:44818' # TEST port - TAGS = (('SENSOR1', 1, 'INT'), ('ACTUATOR1', 'INT')) + TAGS = (('SENSOR1', 1, 'INT'), ('ACTUATOR1', 1, 'INT'), ('FLAG101', 2, 'STRING')) try: server = EnipProtocol._start_server(ADDRESS, TAGS) - + time.sleep(1) # wait for the server to actually start so client can connect # write a multikey what = ('SENSOR1', 1) for value in range(5): - enip._send(what, value, ADDRESS) + eq_(enip._send(what, value, ADDRESS), str(True)) - # write a single key - what = ('ACTUATOR1',) + # write a multi key + what = ('ACTUATOR1', 1) for value in range(5): - enip._send(what, value, ADDRESS) + eq_(enip._send(what, 1, ADDRESS), str(True)) - EnipProtocol._stop_server(server) + # write a multi key + what = ('FLAG101', 2) + for value in range(5): + eq_(enip._send(what, chr(127-value)*6, ADDRESS), str(True)) + + # write a multi key + what = ('HMI_TEST101',) + for value in range(5): + eq_(enip._send(what, 1, ADDRESS), str(False)) + EnipProtocol._stop_server(server) except Exception as error: EnipProtocol._stop_server(server) print 'ERROR test_send_multikey: ', error @@ -135,20 +150,30 @@ def test_receive_multikey(self): protocol=TestEnipProtocol.CLIENT_PROTOCOL) ADDRESS = 'localhost:44818' # TEST port - TAGS = (('SENSOR1', 1, 'INT'), ('ACTUATOR1', 'INT')) + TAGS = (('SENSOR1', 1, 'INT'), ('ACTUATOR1', 1, 'INT'), ('FLAG101', 2, 'STRING')) try: server = EnipProtocol._start_server(ADDRESS, TAGS) + time.sleep(1) # wait for the server to actually start so client can connect # read a multikey what = ('SENSOR1', 1) address = 'localhost:44818' - enip._receive(what, ADDRESS) + eq_(enip._receive(what, ADDRESS), "0") - # read a single key - what = ('ACTUATOR1',) + # read a multi key + what = ('ACTUATOR1',1) address = 'localhost:44818' - enip._receive(what, ADDRESS) + eq_(enip._receive(what, ADDRESS), "0") + + # Read a single key - uninitialized tag + what = ('HMI_TEST101',) + address = 'localhost:44818' + eq_(enip._receive(what, ADDRESS), "err") + + # Read a multi key + what = ('FLAG101', 2) + eq_(enip._receive(what, ADDRESS), 'ENIPSERVER') EnipProtocol._stop_server(server) @@ -167,23 +192,52 @@ def test_client_server(self): enip = EnipProtocol( protocol=TestEnipProtocol.CLIENT_SERVER_PROTOCOL) + time.sleep(1) # wait for the server to actually start so client can connect + # read a multikey what = ('SENSOR1', 1) - enip._receive(what, ADDRESS) + eq_(enip._receive(what, ADDRESS), '0') # read a single key what = ('ACTUATOR1',) - enip._receive(what, ADDRESS) + eq_(enip._receive(what, ADDRESS), '0') + + # read a single key - missing tag + what = ('HMI_TEST101',) + eq_(enip._receive(what, ADDRESS), "err") + + # read a single key + what = ('FLAG101',) + eq_(enip._receive(what, ADDRESS),'ENIPSERVER') + + # read a single key + what = ('FLAG201', 2) + eq_(enip._receive(what, ADDRESS),'ENIPSERVER') # write a multikey what = ('SENSOR1', 1) for value in range(5): - enip._send(what, value, ADDRESS) + eq_(enip._send(what, value, ADDRESS), str(True)) # write a single key what = ('ACTUATOR1',) for value in range(5): - enip._send(what, value, ADDRESS) + eq_(enip._send(what, value, ADDRESS), str(True)) + + # write a single key - uninitialized tag - error shouldn't occur + what = ('HMI_TEST101') + for value in range(5): + eq_(enip._send(what, value, ADDRESS), str(False)) + + # write a single key + what = ('FLAG101',) + for value in range(5): + eq_(enip._send(what, str(127-value)*8, ADDRESS), str(True)) + + # write a multi key + what = ('FLAG201', 2) + for value in range(5): + eq_(enip._send(what, chr(127-value)*8, ADDRESS), str(True)) EnipProtocol._stop_server(enip._server_subprocess) @@ -200,7 +254,7 @@ def test_server_udp(self): # }}} -# TestModbusProtocol {{{1 +# # TestModbusProtocol {{{1 class TestModbusProtocol(): # NOTE: current API specifies only the number of tags @@ -479,4 +533,4 @@ def test_client_server_count(self): ModbusProtocol._stop_server(server) print 'ERROR test_client_server_count: ', error assert False -# }}} +# # }}}