Ejemplo n.º 1
0
    def test_request(self):

        tests = [
            {
                'src': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD',
                'raw': b'\x01\x03\x00\x00\x00\x0A',
                'exp': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD'
            },
        ]

        obj = ModbusTransaction()

        for test in tests:
            # loop through all tests

            # print("test:{}".format(test))

            # this won't have the UID in first place - moved to [KEY_SRC_ID]
            msg = test['raw'][1:]

            result = obj.set_request(test['src'], ia_trans.IA_PROTOCOL_MBRTU)
            # print("obj pst:{}".format(obj.attrib))
            self.assertTrue(result)
            self.assertEqual(obj[obj.KEY_REQ_RAW], test['src'])
            self.assertEqual(obj[obj.KEY_REQ_PROTOCOL],
                             ia_trans.IA_PROTOCOL_MBRTU)
            self.assertNotIn(obj.KEY_SRC_ID, obj.attrib)
            self.assertEqual(obj[obj.KEY_REQ], msg)

            result = obj.get_request()
            self.assertEqual(result, test['exp'])

        return
Ejemplo n.º 2
0
    def __init__(self):

        # thread-safe event to halt execution
        self.running = Event()
        self.running.set()

        # various server settings
        self.host_ip = self.DEF_HOST_IP
        self.host_port = self.DEF_HOST_PORT
        self.host_protocol = self.DEF_HOST_PROTOCOL

        # used to abort an idle client connection
        self.idle_timeout = self.DEF_IDLE_TIMEOUT
        self.last_activity = time.time()

        self.serial_name = self.DEF_SERIAL_PORT
        self.serial_baud = self.DEF_SERIAL_BAUD
        self.serial_parity = self.DEF_SERIAL_PARITY
        self.serial_protocol = self.DEF_SERIAL_PROTOCOL
        self.ser = None

        self.logger = None

        # hold our server socket
        self.server = None
        self._rd_ready = None

        # this is our object to hold, parse, and transmute packets
        self.modbus = ModbusTransaction()

        return
    def test_code_ascii(self):

        # _estimate_length_request

        tests = [
            {
                'src': b':01010001000A66\r\n',
                'exp': 6
            },
        ]

        obj = ModbusTransaction()

        for test in tests:
            # loop through all tests

            if test['exp'] == ValueError:
                with self.assertRaises(ValueError):
                    obj._parse_rtu(test['src'])

                pass
            else:
                obj._parse_rtu(test['src'])
                self.assertEqual(result, test['exp'])
                self.assertEqual(obj['cooked_protocol'], obj.IA_PROTOCOL_MBRTU)

        return
Ejemplo n.º 4
0
    def test_all_request(self):

        tests = [
            {
                'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A',
                'raw': b'\x01\x03\x00\x00\x00\x0A',
                'asc': b':01030000000AF2\r\n',
                'rtu': b'\x01\x03\x00\x00\x00\x0A\xC5\xCD',
                'tcp': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
            },
        ]

        obj = ModbusTransaction()

        for test in tests:
            # loop through all tests

            # print("test:{}".format(test))

            result = obj.set_request(test['src'], ia_trans.IA_PROTOCOL_MBTCP)
            self.assertEqual(obj[obj.KEY_REQ_PROTOCOL],
                             ia_trans.IA_PROTOCOL_MBTCP)

            # default will be TCP
            result = obj.get_request()
            self.assertEqual(result, test['tcp'])

            # confirm we can pull back as any of the three
            result = obj.get_request(ia_trans.IA_PROTOCOL_MBASC)
            self.assertEqual(result, test['asc'])

            result = obj.get_request(ia_trans.IA_PROTOCOL_MBRTU)
            self.assertEqual(result, test['rtu'])

            result = obj.get_request(ia_trans.IA_PROTOCOL_MBTCP)
            self.assertEqual(result, test['tcp'])

        return
    def test_estimate_length_request(self):

        # _estimate_length_request

        tests = [
            {
                'src': b'\x01\x01\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x01\x00\x01\x00\x0A\x44\x2A',
                'exp': 6
            },
            {
                'src': b'\x01\x02\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x03\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x04\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x05\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x06\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x0F\x02\x00\x01',
                'exp': 5
            },
            {
                'src': b'\x01\x10\x02\x00\x01',
                'exp': 5
            },
            {
                'src': '\x01\x01\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x00\x02\x00\x01',
                'exp': ValueError
            },
        ]

        obj = ModbusTransaction()

        for test in tests:
            # loop through all tests

            if test['exp'] == ValueError:
                with self.assertRaises(ValueError):
                    obj._estimate_length_request(test['src'])

                pass
            else:
                result = obj._estimate_length_request(test['src'])
                self.assertEqual(result, test['exp'])

        return
    def test_parse_rtu(self):

        # _estimate_length_request

        tests = [
            {
                'src': b'\x01\x01\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x01\x00\x01\x00\x0A\x44\x2A',
                'exp': 6
            },
            {
                'src': b'\x01\x02\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x03\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x04\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x05\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x06\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x0F\x02\x00\x01',
                'exp': 5
            },
            {
                'src': b'\x01\x10\x02\x00\x01',
                'exp': 5
            },
            {
                'src': '\x01\x01\x00\x01\x00\x0A',
                'exp': 6
            },
            {
                'src': b'\x01\x00\x02\x00\x01',
                'exp': ValueError
            },
        ]

        obj = ModbusTransaction()

        for test in tests:
            # loop through all tests

            if test['exp'] == ValueError:
                with self.assertRaises(ValueError):
                    obj._parse_rtu(test['src'])

                pass
            else:
                obj._parse_rtu(test['src'])
                # self.assertEqual(result, test['exp'])
                self.assertEqual(obj['cooked_protocol'], obj.IA_PROTOCOL_MBRTU)

        return
Ejemplo n.º 7
0
class ModbusBridge(object):

    # note in router, 'localhost' literally means only internal
    DEF_HOST_IP = ''
    DEF_HOST_PORT = 8512
    DEF_HOST_PROTOCOL = IA_PROTOCOL_MBTCP
    DEF_IDLE_TIMEOUT = 300

    DEF_SERIAL_PORT = '/dev/ttyS1'
    DEF_SERIAL_BAUD = 9600
    DEF_SERIAL_PARITY = 'N'
    DEF_SERIAL_PROTOCOL = IA_PROTOCOL_MBRTU

    def __init__(self):

        # thread-safe event to halt execution
        self.running = Event()
        self.running.set()

        # various server settings
        self.host_ip = self.DEF_HOST_IP
        self.host_port = self.DEF_HOST_PORT
        self.host_protocol = self.DEF_HOST_PROTOCOL

        # used to abort an idle client connection
        self.idle_timeout = self.DEF_IDLE_TIMEOUT
        self.last_activity = time.time()

        self.serial_name = self.DEF_SERIAL_PORT
        self.serial_baud = self.DEF_SERIAL_BAUD
        self.serial_parity = self.DEF_SERIAL_PARITY
        self.serial_protocol = self.DEF_SERIAL_PROTOCOL
        self.ser = None

        self.logger = None

        # hold our server socket
        self.server = None
        self._rd_ready = None

        # this is our object to hold, parse, and transmute packets
        self.modbus = ModbusTransaction()

        return

    def run_loop(self):
        """
        The main outer loop - make sure server is listening

        :return:
        """

        result_code = 0

        try:
            while self.running.is_set():

                try:
                    # this opens the self.server
                    self.server = self.bind_server()

                except ConnectionError:
                    # we exit, because if we cannot secure the resource, the
                    # failure is very likely permanent. ideally would cause
                    # a reboot
                    self.running.clear()
                    result_code = -1
                    break

                # here, self.server should magically be valid and open
                self._rd_ready = [self.server]

                while self.running.is_set():
                    # loop forever
                    _rd, _wr, _x = select.select(self._rd_ready, [], [],
                                                 DEF_SELECT_TIMEOUT)

                    # handle the inputs/receives
                    for sock in _rd:
                        if sock == self.server:
                            # then is a new client
                            self.accept_client()

                        else:
                            # this is a client
                            data = sock.recv(1024)
                            if not data:
                                # then client socket closed/failed
                                self.remove_client(sock)

                            else:
                                try:
                                    result = self.handle_data(data)

                                except ConnectionError:
                                    # if Serial() open fails, ConnectionError
                                    # is thrown; no point remaining in loop
                                    self.running.clear()
                                    result_code = -2
                                    break

                                if result:
                                    sock.send(result)

                                else:
                                    # then hand-up!
                                    self.remove_client(sock)

                    # for now, ignore the _wr, and _x lists

                    if len(self._rd_ready) < 2:
                        # then no client, the DEF_SELECT_TIMEOUT happened
                        self.logger.debug("waiting")

                    elif self.idle_timeout is not None:
                        # then we have a client, so check the idle_timeout
                        delta = time.time() - self.last_activity
                        if delta > self.idle_timeout:
                            self.logger.warning("Idle Timeout!")
                            for sock in self._rd_ready:
                                if sock != self.server:
                                    self.remove_client(sock)

                        else:
                            self.logger.debug("idle:{} sec".format(
                                round(delta, 2)))

                    # loop up

        finally:
            # do some clean up
            # self.logger.debug("Do Clean-Up")
            if self.server is not None:
                self.logger.debug("TCP server is not None, try cleanup")
                try:
                    self.server.close()
                    time.sleep(1.0)
                    del self.server
                except:
                    self.logger.exception("server.close() failed")
                    raise

            if self.ser is not None:
                self.logger.debug("Serial port is not None, try cleanup")
                try:
                    self.ser.close()
                    time.sleep(1.0)
                    del self.ser
                except:
                    self.logger.exception("Serial.close() failed")
                    raise

        # force garbage collection
        gc.collect()

        if DEF_TIME_WAIT_DELAY:
            self.logger.debug(
                "Final TIME_WAIT delay:{} sec".format(DEF_TIME_WAIT_DELAY))
            time.sleep(DEF_TIME_WAIT_DELAY)

        # we're to stop running
        return result_code

    def bind_server(self):
        """
        Wrap the bind process
        :return:
        """
        # define the socket resource, including the type (stream == "TCP")
        bind_address = (self.host_ip, self.host_port)
        self.logger.debug("Preparing TCP socket {}".format(bind_address))
        self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

        # try to speed up reuse
        self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # attempt to actually lock resource, which may fail if unavailable
        try:
            self.server.bind(bind_address)
        except OSError as msg:
            self.logger.error("socket.bind() failed - {}".format(msg))
            self.server.close()
            self.server = None
            self.logger.error("TCP server socket closed")
            raise ConnectionError

        # only allow 1 client at a time
        self.server.listen(1)
        self.logger.info("Waiting on TCP {}, protocol:{}".format(
            bind_address, self.host_protocol))

        return self.server

    def accept_client(self):
        """
        have a nibble on the server line, reel in client

        :return:
        """
        client, address = self.server.accept()
        self.logger.info("Accepted connection from {}".format(address))

        # for cellular, ALWAYS enable TCP Keep Alive
        client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

        self._rd_ready.append(client)
        self.last_activity = time.time()
        return

    def remove_client(self, client):
        """
        have a nibble on the server line, reel in client

        :return:
        """
        self.logger.info("Client disconnected")

        client.close()

        if client in self._rd_ready:
            self._rd_ready.remove(client)

        # try to force faster memory clean-up
        gc.collect()
        return

    def handle_data(self, data):
        """

        :param bytes data:
        :return:
        """
        logger_buffer_dump(self.logger, "TCP-REQ", data)
        self.last_activity = time.time()

        response = None

        try:
            # parse the MB/TCP request
            self.modbus.set_request(data, self.host_protocol)
            # self.logger.debug("REQ:{}".format(self.modbus.attrib))

        except (ModbusBadForm, ModbusBadChecksum):
            # this could be a bad setting
            self.logger.warning("Bad Modbus Form")
            return None

        # retrieve as MB/RTU request
        modbus_rtu = self.modbus.get_request(self.serial_protocol)
        # self.logger.debug("SER:{}".format(modbus_rtu))

        # if Serial() open fails, we'll throw ConnectionError, which this
        # code assumes the CALLER of handle_data() handles
        response = self.send_serial(modbus_rtu)
        if response and len(response):
            # parse the MB/RTU in
            try:
                self.modbus.set_response(response, self.serial_protocol)
                response = self.modbus.get_response(self.host_protocol)

            except ModbusBadForm:
                # this could be a bad setting
                self.logger.warning("Bad Modbus Form")
                response = None

            except ModbusBadChecksum:
                # likely line noise or loose wire
                self.logger.warning("Bad Checksum")
                response = None

        else:
            # in truth, if client is:
            #  Modbus/TCP - should return exception 0x0B
            # Modbus/RTU - no response
            self.logger.debug("No response")
            response = None

        if response is None:
            # then re-form as the correct err response, which may be None
            response = self.modbus.get_no_response_error(self.host_protocol)

        logger_buffer_dump(self.logger, "TCP-RSP", response)
        # else was in error, hang-up
        return response

    def send_serial(self, data):
        """
        Send what is assumed a Modbus serial packet. If the serial port is
        NOT open (so self.ser == None), this this routine tries to open it.

        If the open fails, a ConnectError is raised, which is the SDK apps'
        signal to quit/abort running.

        :param bytes data: the Modbus serial message, which could be either
                           Modbus/RTU or ASCII
        :return: the response data, which might be b'' (empty/no response)
        """
        import logging

        if self.ser is None:
            self.logger.info("Open serial port:{}".format(self.serial_name))
            try:
                self.ser = serial.Serial(port=self.serial_name,
                                         baudrate=self.serial_baud,
                                         bytesize=8,
                                         parity=self.serial_parity,
                                         stopbits=1,
                                         timeout=1,
                                         xonxoff=0,
                                         rtscts=0)
                self.ser.setDTR(True)
                # self.ser.setRTS(True)

            except serial.SerialException:
                self.ser = None
                self.logger.exception("Open of serial port failed")
                raise ConnectionError("Open of serial port failed")

            self.logger.info("Serial Protocol:{}".format(self.serial_protocol))

        if self.logger.getEffectiveLevel() <= logging.DEBUG:
            if self.serial_protocol == IA_PROTOCOL_MBASC:
                # for ASCII, just print as string
                self.logger.debug("ASC-REQ:{}".format(data))
            else:  # for RTU, we want HEX form
                logger_buffer_dump(self.logger, "RTU-REQ", data)

        self.ser.write(data)

        # we have 1 second response timeout in the Serial() open
        time.sleep(0.25)
        response = self.ser.read(256)

        if self.logger.getEffectiveLevel() <= logging.DEBUG:
            if response is None or response == b'':
                self.logger.debug("SER-RSP:None/No response")
            elif self.serial_protocol == IA_PROTOCOL_MBASC:
                # for ASCII, just print as string
                self.logger.debug("ASC-RSP:{}".format(response))
            else:  # for RTU, we want HEX form
                logger_buffer_dump(self.logger, "RTU-RSP", response)

        return response
Ejemplo n.º 8
0
    def test_sequence_number(self):

        tests = [
            {
                'src': b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A',
                'seq': b'\xAB\xCD',
                'raw': b'\x01\x03\x00\x00\x00\x0A'
            },
        ]

        obj = ModbusTransaction()

        source = b'\xAB\xCD\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        expect = source
        obj.set_request(source, ia_trans.IA_PROTOCOL_MBTCP)
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new 2-byte sequence
        obj[obj.KEY_SRC_SEQ] = b'\xFF\xEE'
        expect = b'\xFF\xEE\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new 1-byte sequence - expect padding with 0x01
        obj[obj.KEY_SRC_SEQ] = b'\x44'
        expect = b'\x44\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new n byte sequence - expect padding with 0x01
        obj[obj.KEY_SRC_SEQ] = b''
        expect = b'\x01\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new 3 byte sequence - expect truncate to 2 bytes
        obj[obj.KEY_SRC_SEQ] = b'\x05\x06\x07'
        expect = b'\x05\x06\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new int sequence
        obj[obj.KEY_SRC_SEQ] = 0
        expect = b'\x00\x00\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        obj[obj.KEY_SRC_SEQ] = 0x01
        expect = b'\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new int sequence - saved as big-endian
        obj[obj.KEY_SRC_SEQ] = 0x0306
        expect = b'\x03\x06\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        # swap in a new int sequence - excess is truncated
        obj[obj.KEY_SRC_SEQ] = 0xFF0306
        expect = b'\x03\x06\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0A'
        result = obj.get_request()
        self.assertEqual(result, expect)

        return