def _sendPacket(self, message, callback): """ Sends packets on the bus with 3.5char delay between frames :param message: Message to be sent over the bus :return: """ @gen.coroutine def sleep(timeout): yield gen.sleep(timeout) try: waiting = self.stream.connection.in_waiting if waiting: result = self.stream.connection.read(waiting) LOGGER.info("Cleanup recv buffer before send: " + hexlify_packets(result)) except OSError as e: self.transaction.getTransaction( message.transaction_id).set_exception(ModbusIOException(e)) return start = time.time() if self.last_frame_end: waittime = self.last_frame_end + self.silent_interval - start if waittime > 0: LOGGER.debug("Waiting for 3.5 char before next send - %f ms", waittime) sleep(waittime) self.state = ModbusTransactionState.SENDING LOGGER.debug("send: " + hexlify_packets(message)) self.stream.write(message, callback)
def _transact(self, packet, response_length, full=False): """ Does a Write and Read transaction :param packet: packet to be sent :param response_length: Expected response length :param full: the target device was notorious for its no response. Dont waste time this time by partial querying :return: response """ last_exception = None try: self.client.connect() packet = self.client.framer.buildPacket(packet) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("SEND: " + hexlify_packets(packet)) size = self._send(packet) if size: _logger.debug("Changing transaction state from 'SENDING' " "to 'WAITING FOR REPLY'") self.client.state = ModbusTransactionState.WAITING_FOR_REPLY result = self._recv(response_length, full) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("RECV: " + hexlify_packets(result)) except (socket.error, ModbusIOException, InvalidMessageReceivedException) as msg: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) last_exception = msg result = b'' return result, last_exception
def _transact(self, packet, response_length, full=False): """ Does a Write and Read transaction :param packet: packet to be sent :param response_length: Expected response length :param full: the target device was notorious for its no response. Dont waste time this time by partial querying :return: response """ last_exception = None try: self.client.connect() packet = self.client.framer.buildPacket(packet) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("SEND: " + hexlify_packets(packet)) size = self._send(packet) if size: _logger.debug("Changing transaction state from 'SENDING' " "to 'WAITING FOR REPLY'") self.client.state = ModbusTransactionState.WAITING_FOR_REPLY result = self._recv(response_length, full) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("RECV: " + hexlify_packets(result)) except (socket.error, ModbusIOException, InvalidMessageReceivedException) as msg: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) last_exception = msg result = b'' return result, last_exception
def _dump(self, data, direction): fd = self._debugfd if self._debugfd else sys.stdout try: fd.write(hexlify_packets(data)) except Exception as e: self._logger.debug(hexlify_packets(data)) self._logger.exception(e)
def _send(self, request): """ Sends data on the underlying socket If receive buffer still holds some data then flush it. Sleep if last send finished less than 3.5 character times ago. :param request: The encoded request to send :return: The number of bytes written """ if not self.socket: raise ConnectionException(self.__str__()) if request: try: waitingbytes = self._in_waiting() if waitingbytes: result = self.socket.read(waitingbytes) if self.state == ModbusTransactionState.RETRYING: _logger.debug("Sending available data in recv " "buffer {}".format( hexlify_packets(result))) return result if _logger.isEnabledFor(logging.WARNING): _logger.warning("Cleanup recv buffer before " "send: " + hexlify_packets(result)) except NotImplementedError: pass if self.state != ModbusTransactionState.SENDING: _logger.debug("New Transaction state 'SENDING'") self.state = ModbusTransactionState.SENDING size = self.socket.write(request) return size return 0
def _dump(self, data, direction): fd = self._debugfd if self._debugfd else sys.stdout try: fd.write(hexlify_packets(data)) except Exception as e: self._logger.debug(hexlify_packets(data)) self._logger.exception(e)
def _transact(self, packet, response_length, full=False, broadcast=False): """ Does a Write and Read transaction :param packet: packet to be sent :param response_length: Expected response length :param full: the target device was notorious for its no response. Dont waste time this time by partial querying :return: response """ last_exception = None try: self.client.connect() packet = self.client.framer.buildPacket(packet) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("SEND: " + hexlify_packets(packet)) size = self._send(packet) if isinstance( size, bytes ) and self.client.state == ModbusTransactionState.RETRYING: _logger.debug("Changing transaction state from " "'RETRYING' to 'PROCESSING REPLY'") self.client.state = ModbusTransactionState.PROCESSING_REPLY return size, None if broadcast: if size: _logger.debug("Changing transaction state from 'SENDING' " "to 'TRANSACTION_COMPLETE'") self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE return b'', None if size: _logger.debug("Changing transaction state from 'SENDING' " "to 'WAITING FOR REPLY'") self.client.state = ModbusTransactionState.WAITING_FOR_REPLY if hasattr(self.client, "handle_local_echo" ) and self.client.handle_local_echo is True: local_echo_packet = self._recv(size, full) if local_echo_packet != packet: return b'', "Wrong local echo" result = self._recv(response_length, full) # result2 = self._recv(response_length, full) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("RECV: " + hexlify_packets(result)) except (socket.error, ModbusIOException, InvalidMessageReceivedException) as msg: if self.reset_socket: self.client.close() _logger.debug("Transaction failed. (%s) " % msg) last_exception = msg result = b'' return result, last_exception
def getRawFrame(self): """ Returns the complete buffer """ _logger.debug("Getting Raw Frame - " "{}".format(hexlify_packets(self._buffer))) return self._buffer
def _send(self, request): """ Sends data on the underlying socket If receive buffer still holds some data then flush it. Sleep if last send finished less than 3.5 character times ago. :param request: The encoded request to send :return: The number of bytes written """ if not self.socket: raise ConnectionException(self.__str__()) if request: try: waitingbytes = self._in_waiting() if waitingbytes: result = self.socket.read(waitingbytes) if _logger.isEnabledFor(logging.WARNING): _logger.warning("Cleanup recv buffer before " "send: " + hexlify_packets(result)) except NotImplementedError: pass size = self.socket.write(request) return size return 0
def execute(self, request=None): """ Executes a transaction :param request: Request to be written on to the bus :return: """ request.transaction_id = self.transaction.getNextTID() def callback(*args): LOGGER.debug("in callback - {}".format(request.transaction_id)) while True: waiting = self.stream.connection.in_waiting if waiting: data = self.stream.connection.read(waiting) LOGGER.debug("recv: " + hexlify_packets(data)) unit = self.framer.decode_data(data).get("uid", 0) self.framer.processIncomingPacket( data, self._handle_response, unit, tid=request.transaction_id) break packet = self.framer.buildPacket(request) LOGGER.debug("send: " + hexlify_packets(packet)) self.stream.write(packet, callback=callback) f = self._build_response(request.transaction_id) return f
def _on_receive(fd, events): LOGGER.debug("_on_receive: %s, %s", fd, events) try: waiting = self.stream.connection.in_waiting if waiting: LOGGER.debug("waiting = %d", waiting) data = self.stream.connection.read(waiting) LOGGER.debug( "recv: " + hexlify_packets(data)) except OSError as ex: _clear_timer() self.close() self.transaction.getTransaction(request.transaction_id).set_exception(ModbusIOException(ex)) return self.framer.addToFrame(data) # check if we have regular frame or modbus exception fcode = self.framer.decode_data(self.framer.getRawFrame()).get("fcode", 0) if fcode and ( (fcode > 0x80 and len(self.framer.getRawFrame()) == exception_response_length) or (len(self.framer.getRawFrame()) == expected_response_length) ): _clear_timer() self.io_loop.remove_handler(fd) self.state = ModbusTransactionState.IDLE self.framer.processIncomingPacket( b'', # already sent via addToFrame() self._handle_response, 0, # don't care for `single=True` single=True, tid=request.transaction_id )
def handle(self, event): reset_frame = False try: units = self.context.slaves() if isinstance(event, QuicEvent): # addr is populated when talking over UDP data = event.data else: print("event is none") return if not isinstance(units, (list, tuple)): units = [units] # if broadcast is enabled make sure to # process requests to address 0 if self.broadcast_enable: # pragma: no cover if 0 not in units: units.append(0) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) single = self.context.single self.framer.processIncomingPacket( data=data, callback=lambda x: self.execute(x, event), unit=units, single=single) finally: if reset_frame: self.framer.resetFrame() reset_frame = False
def getRawFrame(self): """ Returns the complete buffer """ _logger.debug("Getting Raw Frame - " "{}".format(hexlify_packets(self._buffer))) return self._buffer
def handle(self): """ Callback when we receive any data """ reset_frame = False while self.running: try: data, self.socket = self.request if not data: self.running = False data = b'' if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) # if not self.server.control.ListenOnly: units = self.server.context.slaves() single = self.server.context.single self.framer.processIncomingPacket(data, self.execute, units, single=single) except socket.timeout: pass except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False reset_frame = True except Exception as msg: _logger.error(msg) self.running = False reset_frame = True finally: # Reset data after processing self.request = (None, self.socket) if reset_frame: self.framer.resetFrame() reset_frame = False
def handle(self): """ Callback when we receive any data """ reset_frame = False while self.running: try: data, self.socket = self.request if not data: self.running = False data = b'' if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) # if not self.server.control.ListenOnly: units = self.server.context.slaves() single = self.server.context.single self.framer.processIncomingPacket(data, self.execute, units, single=single) except socket.timeout: pass except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False reset_frame = True except Exception as msg: _logger.error(msg) self.running = False reset_frame = True finally: # Reset data after processing self.request = (None, self.socket) if reset_frame: self.framer.resetFrame() reset_frame = False
def _dataReceived(self, data): ''' Get response, check for valid message, decode result :param data: The data returned from the server ''' _logger.debug("recv: " + hexlify_packets(data)) unit = self.framer.decode_data(data).get("unit", 0) self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
def handle(self): """Callback when we receive any data, until self.running becomes False. Blocks indefinitely awaiting data. If shutdown is required, then the global socket.settimeout(<seconds>) may be used, to allow timely checking of self.running. However, since this also affects socket connects, if there are outgoing socket connections used in the same program, then these will be prevented, if the specfied timeout is too short. Hence, this is unreliable. To respond to Modbus...Server.server_close() (which clears each handler's self.running), derive from this class to provide an alternative handler that awakens from time to time when no input is available and checks self.running. Use Modbus...Server( handler=... ) keyword to supply the alternative request handler class. """ reset_frame = False while self.running: try: units = self.server.context.slaves() data = self.request.recv(1024) if not data: self.running = False else: if not isinstance(units, (list, tuple)): units = [units] # if broadcast is enabled make sure to # process requests to address 0 if self.server.broadcast_enable: if 0 not in units: units.append(0) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) single = self.server.context.single self.framer.processIncomingPacket(data, self.execute, units, single=single) except socket.timeout as msg: if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Socket timeout occurred %s", msg) reset_frame = True except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False except: _logger.error("Socket exception occurred " "%s" % traceback.format_exc()) self.running = False reset_frame = True finally: if reset_frame: self.framer.resetFrame() reset_frame = False
def _execute(self, request, **kwargs): """ Starts the producer to send the next request to consumer.write(Frame(request)) """ request.transaction_id = self.transaction.getNextTID() packet = self.framer.buildPacket(request) _logger.debug("send: " + hexlify_packets(packet)) self.write_transport(packet) return self._buildResponse(request.transaction_id)
def handle(self): """Callback when we receive any data, until self.running becomes False. Blocks indefinitely awaiting data. If shutdown is required, then the global socket.settimeout(<seconds>) may be used, to allow timely checking of self.running. However, since this also affects socket connects, if there are outgoing socket connections used in the same program, then these will be prevented, if the specfied timeout is too short. Hence, this is unreliable. To respond to Modbus...Server.server_close() (which clears each handler's self.running), derive from this class to provide an alternative handler that awakens from time to time when no input is available and checks self.running. Use Modbus...Server( handler=... ) keyword to supply the alternative request handler class. """ reset_frame = False while self.running: try: units = self.server.context.slaves() data = self.request.recv(1024) if not data: self.running = False else: if not isinstance(units, (list, tuple)): units = [units] # if broadcast is enabled make sure to # process requests to address 0 if self.server.broadcast_enable: if 0 not in units: units.append(0) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) single = self.server.context.single self.framer.processIncomingPacket(data, self.execute, units, single=single) except socket.timeout as msg: if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Socket timeout occurred %s", msg) reset_frame = True except socket.error as msg: _logger.error("Socket error occurred %s" % msg) self.running = False except: _logger.error("Socket exception occurred " "%s" % traceback.format_exc() ) self.running = False reset_frame = True finally: if reset_frame: self.framer.resetFrame() reset_frame = False
def datagramReceived(self, data, addr): """ Callback when we receive any data :param data: The data sent by the client """ _logger.debug("Client Connected [%s]" % addr) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Datagram Received: "+ hexlify_packets(data)) if not self.control.ListenOnly: continuation = lambda request: self._execute(request, addr) self.framer.processIncomingPacket(data, continuation)
def execute(self, request=None): """ Executes a transaction :param request: :return: """ request.transaction_id = self.transaction.getNextTID() packet = self.framer.buildPacket(request) LOGGER.debug("send: " + hexlify_packets(packet)) self.stream.write(packet) return self._build_response(request.transaction_id)
def datagramReceived(self, data, addr): """ Callback when we receive any data :param data: The data sent by the client """ _logger.debug("Client Connected [%s]" % addr) if _logger.isEnabledFor(logging.DEBUG): _logger.debug("Datagram Received: " + hexlify_packets(data)) if not self.control.ListenOnly: continuation = lambda request: self._execute(request, addr) self.framer.processIncomingPacket(data, continuation)
def resetFrame(self): """ Reset the entire message frame. This allows us to skip over errors that may be in the stream. It is hard to know if we are simply out of sync or if there is an error in the stream as we have no way to check the start or end of the message (python just doesn't have the resolution to check for millisecond delays). """ _logger.debug("Resetting frame - Current Frame in " "buffer - {}".format(hexlify_packets(self._buffer))) self._buffer = b'' self._header = {}
def getFrame(self): """ Get the next frame from the buffer :returns: The frame data or '' """ start = self._hsize end = self._header['len'] - 2 buffer = self._buffer[start:end] if end > 0: _logger.debug("Getting Frame - {}".format(hexlify_packets(buffer))) return buffer return b''
def dataReceived(self, data): """ Callback when we receive any data :param data: The data sent by the client """ if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Data Received: ' + hexlify_packets(data)) if not self.factory.control.ListenOnly: units = self.factory.store.slaves() single = self.factory.store.single self.framer.processIncomingPacket(data, self._execute, single=single, unit=units)
def dataReceived(self, data): """ Callback when we receive any data :param data: The data sent by the client """ if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Data Received: ' + hexlify_packets(data)) if not self.factory.control.ListenOnly: units = self.factory.store.slaves() single = self.factory.store.single self.framer.processIncomingPacket(data, self._execute, single=single, unit=units)
def resetFrame(self): """ Reset the entire message frame. This allows us to skip over errors that may be in the stream. It is hard to know if we are simply out of sync or if there is an error in the stream as we have no way to check the start or end of the message (python just doesn't have the resolution to check for millisecond delays). """ _logger.debug("Resetting frame - Current Frame in " "buffer - {}".format(hexlify_packets(self._buffer))) self._buffer = b'' self._header = {}
def getFrame(self): """ Get the next frame from the buffer :returns: The frame data or '' """ start = self._hsize end = self._header['len'] - 2 buffer = self._buffer[start:end] if end > 0: _logger.debug("Getting Frame - {}".format(hexlify_packets(buffer))) return buffer return b''
def callback(*args): LOGGER.debug("in callback - {}".format(request.transaction_id)) while True: waiting = self.stream.connection.in_waiting if waiting: data = self.stream.connection.read(waiting) LOGGER.debug("recv: " + hexlify_packets(data)) unit = self.framer.decode_data(data).get("uid", 0) self.framer.processIncomingPacket( data, self._handle_response, unit, tid=request.transaction_id) break
def on_receive(self, *args): """ On data recieve call back :param args: data received :return: """ data = args[0] if len(args) > 0 else None if not data: return LOGGER.debug("recv: " + hexlify_packets(data)) unit = self.framer.decode_data(data).get("unit", 0) self.framer.processIncomingPacket(data, self._handle_response, unit=unit)
def dataReceived(self, data): """ Callback when we receive any data :param data: The data sent by the client """ log.event('Modbus, IPv4Address[{}]: Data Received (Raw): '.format( self.transport.getPeer().host) + str(data)) log.event('Modbus, IPv4Address[{}]: Data Received (Hex): '.format( self.transport.getPeer().host) + hexlify_packets(data)) if not self.factory.control.ListenOnly: units = self.factory.store.slaves() single = self.factory.store.single self.framer.processIncomingPacket(data, self._execute, single=single, unit=units)
def _execute(self, request): # Build Framer Packet request.transaction_id = self.transaction.getNextTID() packet = self.framer.buildPacket(message=request) logger.debug("send: " + hexlify_packets(packet)) # Send packet through QUIC self.send(packet) # Get response waiter = self.client._loop.create_future() self.transaction.addTransaction(waiter, request.transaction_id) self._ack_waiter = waiter self.client.transmit() return waiter
def processIncomingPacket(self, data, callback, unit, **kwargs): """ The new packet processing pattern This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that exist. This handles the case when we read N + 1 or 1 / N messages at a time instead of 1. The processed and decoded messages are pushed to the callback function to process and send. :param data: The new packet data :param callback: The function to send results to :param unit: Process if unit id matches, ignore otherwise (could be a list of unit ids (server) or single unit id(client/server) :param single: True or False (If True, ignore unit address validation) """ if not isinstance(unit, (list, tuple)): unit = [unit] single = kwargs.get("single", False) _logger.debug("Processing: " + hexlify_packets(data)) self.addToFrame(data) while True: if self.isFrameReady(): if self.checkFrame(): if self._validate_unit_id(unit, single): self._process(callback) else: _logger.debug("Not a valid unit id - {}, " "ignoring!!".format(self._header['uid'])) self.resetFrame() else: _logger.debug("Frame check failed, ignoring!!") self.resetFrame() else: if len(self._buffer): # Possible error ??? if self._header['len'] < 2: self._process(callback, error=True) break
def processIncomingPacket(self, data, callback, unit, **kwargs): """ The new packet processing pattern This takes in a new request packet, adds it to the current packet stream, and performs framing on it. That is, checks for complete messages, and once found, will process all that exist. This handles the case when we read N + 1 or 1 // N messages at a time instead of 1. The processed and decoded messages are pushed to the callback function to process and send. :param data: The new packet data :param callback: The function to send results to :param unit: Process if unit id matches, ignore otherwise (could be a \ list of unit ids (server) or single unit id(client/server) :param single: True or False (If True, ignore unit address validation) :return: """ if not isinstance(unit, (list, tuple)): unit = [unit] single = kwargs.get("single", False) _logger.debug("Processing: " + hexlify_packets(data)) self.addToFrame(data) while True: if self.isFrameReady(): if self.checkFrame(): if self._validate_unit_id(unit, single): self._process(callback) else: _logger.debug("Not a valid unit id - {}, " "ignoring!!".format(self._header['uid'])) self.resetFrame() else: _logger.debug("Frame check failed, ignoring!!") self.resetFrame() else: if len(self._buffer): # Possible error ??? if self._header['len'] < 2: self._process(callback, error=True) break
def testBaseModbusClient(self): ''' Test the base class for all the clients ''' client = BaseModbusClient(None) client.transaction = None self.assertRaises(NotImplementedException, lambda: client.connect()) self.assertRaises(NotImplementedException, lambda: client.send(None)) self.assertRaises(NotImplementedException, lambda: client.recv(None)) self.assertRaises(NotImplementedException, lambda: client.__enter__()) self.assertRaises(NotImplementedException, lambda: client.execute()) self.assertRaises(NotImplementedException, lambda: client.is_socket_open()) self.assertEqual("Null Transport", str(client)) client.close() client.__exit__(0, 0, 0) # Test information methods client.last_frame_end = 2 client.silent_interval = 2 self.assertEqual(4, client.idle_time()) client.last_frame_end = None self.assertEqual(0, client.idle_time()) # Test debug/trace/_dump methods self.assertEqual(False, client.debug_enabled()) writable = StringIO() client.trace(writable) client._dump(b'\x00\x01\x02', None) self.assertEqual(hexlify_packets(b'\x00\x01\x02'), writable.getvalue()) # a successful execute client.connect = lambda: True client.transaction = Mock(**{'execute.return_value': True}) self.assertEqual(client, client.__enter__()) self.assertTrue(client.execute()) # a unsuccessful connect client.connect = lambda: False self.assertRaises(ConnectionException, lambda: client.__enter__()) self.assertRaises(ConnectionException, lambda: client.execute())
def _on_timeout(): LOGGER.warning("timeout") _clear_timer() if self.stream: self.io_loop.remove_handler(self.stream.fileno()) self.framer.resetFrame() transaction = self.transaction.getTransaction(request.transaction_id) if self.state != ModbusTransactionState.IDLE: self.state = ModbusTransactionState.IDLE if self.stream: try: waiting = self.stream.connection.in_waiting if waiting: result = self.stream.connection.read(waiting) LOGGER.info( "Cleanup recv buffer after timeout: " + hexlify_packets(result)) except OSError as ex: self.close() if transaction: transaction.set_exception(ModbusIOException(ex)) return if transaction: transaction.set_exception(TimeOutException())
def _on_receive(fd, events): """ New data in serial buffer to read or serial port closed """ if events & IOLoop.ERROR: _on_fd_error(fd) return try: waiting = self.stream.connection.in_waiting if waiting: data = self.stream.connection.read(waiting) LOGGER.debug("recv: " + hexlify_packets(data)) self.last_frame_end = round(time.time(), 6) except OSError as ex: _on_fd_error(fd, ex) return self.framer.addToFrame(data) # check if we have regular frame or modbus exception fcode = self.framer.decode_data(self.framer.getRawFrame()).get( "fcode", 0) if fcode and ( (fcode > 0x80 and len(self.framer.getRawFrame()) == exception_response_length) or (len(self.framer.getRawFrame()) == expected_response_length)): _clear_timer() self.io_loop.remove_handler(fd) self.state = ModbusTransactionState.IDLE self.framer.processIncomingPacket( b'', # already sent via addToFrame() self._handle_response, 0, # don't care when `single=True` single=True, tid=request.transaction_id)
def dataReceived(self, data): logger.debug("recv: " + hexlify_packets(data)) unit = self.framer.decode_data(data=data).get("unit", 0) self.framer.processIncomingPacket(data, self._handleResponse, unit=unit)
def execute(self, request): """ Starts the producer to send the next request to consumer.write(Frame(request)) """ with self._transaction_lock: try: _logger.debug("Current transaction state - {}".format( ModbusTransactionState.to_string(self.client.state)) ) retries = self.retries request.transaction_id = self.getNextTID() _logger.debug("Running transaction " "{}".format(request.transaction_id)) _buffer = hexlify_packets(self.client.framer._buffer) if _buffer: _logger.debug("Clearing current Frame " ": - {}".format(_buffer)) self.client.framer.resetFrame() broadcast = (self.client.broadcast_enable and request.unit_id == 0) if broadcast: self._transact(request, None, broadcast=True) response = b'Broadcast write sent - no response expected' else: expected_response_length = None if not isinstance(self.client.framer, ModbusSocketFramer): if hasattr(request, "get_response_pdu_size"): response_pdu_size = request.get_response_pdu_size() if isinstance(self.client.framer, ModbusAsciiFramer): response_pdu_size = response_pdu_size * 2 if response_pdu_size: expected_response_length = self._calculate_response_length(response_pdu_size) if request.unit_id in self._no_response_devices: full = True else: full = False c_str = str(self.client) if "modbusudpclient" in c_str.lower().strip(): full = True if not expected_response_length: expected_response_length = Defaults.ReadSize response, last_exception = self._transact( request, expected_response_length, full=full, broadcast=broadcast ) if not response and ( request.unit_id not in self._no_response_devices): self._no_response_devices.append(request.unit_id) elif request.unit_id in self._no_response_devices and response: self._no_response_devices.remove(request.unit_id) if not response and self.retry_on_empty and retries: while retries > 0: if hasattr(self.client, "state"): _logger.debug("RESETTING Transaction state to " "'IDLE' for retry") self.client.state = ModbusTransactionState.IDLE _logger.debug("Retry on empty - {}".format(retries)) response, last_exception = self._transact( request, expected_response_length ) if not response: retries -= 1 continue # Remove entry self._no_response_devices.remove(request.unit_id) break addTransaction = partial(self.addTransaction, tid=request.transaction_id) self.client.framer.processIncomingPacket(response, addTransaction, request.unit_id) response = self.getTransaction(request.transaction_id) if not response: if len(self.transactions): response = self.getTransaction(tid=0) else: last_exception = last_exception or ( "No Response received from the remote unit" "/Unable to decode response") response = ModbusIOException(last_exception, request.function_code) if hasattr(self.client, "state"): _logger.debug("Changing transaction state from " "'PROCESSING REPLY' to " "'TRANSACTION_COMPLETE'") self.client.state = ( ModbusTransactionState.TRANSACTION_COMPLETE) return response except ModbusIOException as ex: # Handle decode errors in processIncomingPacket method _logger.exception(ex) self.client.state = ModbusTransactionState.TRANSACTION_COMPLETE return ex
async def handle(self): """Asyncio coroutine which represents a single conversation between the modbus slave and master Once the client connection is established, the data chunks will be fed to this coroutine via the asyncio.Queue object which is fed by the ModbusBaseRequestHandler class's callback Future. This callback future gets data from either asyncio.DatagramProtocol.datagram_received or from asyncio.BaseProtocol.data_received. This function will execute without blocking in the while-loop and yield to the asyncio event loop when the frame is exhausted. As a result, multiple clients can be interleaved without any interference between them. For ModbusConnectedRequestHandler, each connection will be given an instance of the handle() coroutine and this instance will be put in the active_connections dict. Calling server_close will individually cancel each running handle() task. For ModbusDisconnectedRequestHandler, a single handle() coroutine will be started and maintained. Calling server_close will cancel that task. """ reset_frame = False while self.running: try: units = self.server.context.slaves() data = await self._recv_( ) # this is an asyncio.Queue await, it will never fail if isinstance(data, tuple): data, *addr = data # addr is populated when talking over UDP else: addr = (None, ) # empty tuple if not isinstance(units, (list, tuple)): units = [units] # if broadcast is enabled make sure to # process requests to address 0 if self.server.broadcast_enable: # pragma: no cover if 0 not in units: units.append(0) if _logger.isEnabledFor(logging.DEBUG): _logger.debug('Handling data: ' + hexlify_packets(data)) single = self.server.context.single self.framer.processIncomingPacket( data=data, callback=lambda x: self.execute(x, *addr), unit=units, single=single) except asyncio.CancelledError: # catch and ignore cancelation errors if isinstance(self, ModbusConnectedRequestHandler): _logger.debug( "Handler for stream [%s:%s] has been canceled" % self.client_address) else: _logger.debug( "Handler for UDP socket [%s] has been canceled" % self.protocol._sock.getsockname()[1]) except Exception as e: # force TCP socket termination as processIncomingPacket should handle applicaiton layer errors # for UDP sockets, simply reset the frame if isinstance(self, ModbusConnectedRequestHandler): _logger.info( "Unknown exception '%s' on stream [%s:%s] forcing disconnect" % (e, *self.client_address)) self.transport.close() else: _logger.error("Unknown error occurred %s" % e) reset_frame = True # graceful recovery finally: if reset_frame: self.framer.resetFrame() reset_frame = False
def execute(self, request=None): """ Executes a transaction :param request: Request to be written on to the bus :return: """ request.transaction_id = self.transaction.getNextTID() def _clear_timer(): LOGGER.debug("_clear_timer()") if self.timeout_handle: self.io_loop.remove_timeout(self.timeout_handle) self.timeout_handle = None def _on_timeout(): LOGGER.warning("timeout") _clear_timer() if self.stream: self.io_loop.remove_handler(self.stream.fileno()) self.framer.resetFrame() transaction = self.transaction.getTransaction(request.transaction_id) if self.state != ModbusTransactionState.IDLE: self.state = ModbusTransactionState.IDLE if self.stream: try: waiting = self.stream.connection.in_waiting if waiting: result = self.stream.connection.read(waiting) LOGGER.info( "Cleanup recv buffer after timeout: " + hexlify_packets(result)) except OSError as ex: self.close() if transaction: transaction.set_exception(ModbusIOException(ex)) return if transaction: transaction.set_exception(TimeOutException()) def _on_write_done(*args): LOGGER.debug("frame sent, waiting for a reply") self.last_frame_end = round(time.time(), 6) self.state = ModbusTransactionState.WAITING_FOR_REPLY self.io_loop.add_handler(self.stream.fileno(), _on_receive, IOLoop.READ) def _on_receive(fd, events): LOGGER.debug("_on_receive: %s, %s", fd, events) try: waiting = self.stream.connection.in_waiting if waiting: LOGGER.debug("waiting = %d", waiting) data = self.stream.connection.read(waiting) LOGGER.debug( "recv: " + hexlify_packets(data)) except OSError as ex: _clear_timer() self.close() self.transaction.getTransaction(request.transaction_id).set_exception(ModbusIOException(ex)) return self.framer.addToFrame(data) # check if we have regular frame or modbus exception fcode = self.framer.decode_data(self.framer.getRawFrame()).get("fcode", 0) if fcode and ( (fcode > 0x80 and len(self.framer.getRawFrame()) == exception_response_length) or (len(self.framer.getRawFrame()) == expected_response_length) ): _clear_timer() self.io_loop.remove_handler(fd) self.state = ModbusTransactionState.IDLE self.framer.processIncomingPacket( b'', # already sent via addToFrame() self._handle_response, 0, # don't care for `single=True` single=True, tid=request.transaction_id ) LOGGER.debug("set timeout for %f sec", self.timeout) self.timeout_handle = self.io_loop.add_timeout(time.time() + self.timeout, _on_timeout) response_pdu_size = request.get_response_pdu_size() expected_response_length = self.transaction._calculate_response_length(response_pdu_size) LOGGER.debug("expected_response_length = %d", expected_response_length) exception_response_length = self.transaction._calculate_exception_length() # TODO: this does not change packet = self.framer.buildPacket(request) LOGGER.debug("send: " + hexlify_packets(packet)) self.state = ModbusTransactionState.SENDING f = self._build_response(request.transaction_id) self._sendPacket(packet, callback=_on_write_done) return f