Files
uarep-ctl/modbus/pymodbus/client/asynchronous/tornado/__init__.py

498 lines
15 KiB
Python

"""
Asynchronous framework adapter for tornado.
"""
from __future__ import unicode_literals
import abc
import logging
import time
import socket
from serial import Serial
from tornado import gen
from tornado.concurrent import Future
from tornado.ioloop import IOLoop
from tornado.iostream import IOStream
from tornado.iostream import BaseIOStream
from pymodbus.client.asynchronous.mixins import (AsyncModbusClientMixin,
AsyncModbusSerialClientMixin)
from pymodbus.compat import byte2int
from pymodbus.exceptions import (ConnectionException,
ModbusIOException,
TimeOutException)
from pymodbus.utilities import (hexlify_packets,
ModbusTransactionState)
from pymodbus.constants import Defaults
LOGGER = logging.getLogger(__name__)
class BaseTornadoClient(AsyncModbusClientMixin):
"""
Base Tornado client
"""
stream = None
io_loop = None
def __init__(self, *args, **kwargs):
"""
Initializes BaseTornadoClient.
ioloop to be passed as part of kwargs ('ioloop')
:param args:
:param kwargs:
"""
self.io_loop = kwargs.pop("ioloop", None)
super(BaseTornadoClient, self).__init__(*args, **kwargs)
@abc.abstractmethod
def get_socket(self):
"""
return instance of the socket to connect to
"""
@gen.coroutine
def connect(self):
"""
Connect to the socket identified by host and port
:returns: Future
:rtype: tornado.concurrent.Future
"""
conn = self.get_socket()
self.stream = IOStream(conn, io_loop=self.io_loop or IOLoop.current())
self.stream.connect((self.host, self.port))
self.stream.read_until_close(None,
streaming_callback=self.on_receive)
self._connected = True
LOGGER.debug("Client connected")
raise gen.Return(self)
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 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 _handle_response(self, reply, **kwargs):
"""
Handle response received
:param reply:
:param kwargs:
:return:
"""
if reply is not None:
tid = reply.transaction_id
future = self.transaction.getTransaction(tid)
if future:
future.set_result(reply)
else:
LOGGER.debug("Unrequested message: {}".format(reply))
def _build_response(self, tid):
"""
Builds a future response
:param tid:
:return:
"""
f = Future()
if not self._connected:
f.set_exception(ConnectionException("Client is not connected"))
return f
self.transaction.addTransaction(f, tid)
return f
def close(self):
"""
Closes the underlying IOStream
"""
LOGGER.debug("Client disconnected")
if self.stream:
self.stream.close_fd()
self.stream = None
self._connected = False
class BaseTornadoSerialClient(AsyncModbusSerialClientMixin):
"""
Base Tonado serial client
"""
stream = None
io_loop = None
def __init__(self, *args, **kwargs):
"""
Initializes BaseTornadoSerialClient.
ioloop to be passed as part of kwargs ('ioloop')
:param args:
:param kwargs:
"""
self.io_loop = kwargs.pop("ioloop", None)
super(BaseTornadoSerialClient, self).__init__(*args, **kwargs)
@abc.abstractmethod
def get_socket(self):
"""
return instance of the socket to connect to
"""
def on_receive(self, *args):
# Will be handled ine execute method
pass
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 _handle_response(self, reply, **kwargs):
"""
Handles a received response and updates a future
:param reply: Reply received
:param kwargs:
:return:
"""
if reply is not None:
tid = reply.transaction_id
future = self.transaction.getTransaction(tid)
if future:
future.set_result(reply)
else:
LOGGER.debug("Unrequested message: {}".format(reply))
def _build_response(self, tid):
"""
Prepare for a response, returns a future
:param tid:
:return: Future
"""
f = Future()
if not self._connected:
f.set_exception(ConnectionException("Client is not connected"))
return f
self.transaction.addTransaction(f, tid)
return f
def close(self):
"""
Closes the underlying IOStream
"""
LOGGER.debug("Client disconnected")
if self.stream:
self.stream.close_fd()
self.stream = None
self._connected = False
class SerialIOStream(BaseIOStream):
"""
Serial IO Stream class to control and handle serial connections
over tornado
"""
def __init__(self, connection, *args, **kwargs):
"""
Initializes Serial IO Stream
:param connection: serial object
:param args:
:param kwargs:
"""
self.connection = connection
super(SerialIOStream, self).__init__(*args, **kwargs)
def fileno(self):
"""
Returns serial fd
:return:
"""
return self.connection.fileno()
def close_fd(self):
"""
Closes a serial Fd
:return:
"""
if self.connection:
self.connection.close()
self.connection = None
def read_from_fd(self):
"""
Reads from a fd
:return:
"""
try:
chunk = self.connection.readline()
except Exception:
return None
return chunk
def write_to_fd(self, data):
"""
Writes to a fd
:param data:
:return:
"""
try:
return self.connection.write(data)
except Exception as e:
LOGGER.error(e)
class AsyncModbusSerialClient(BaseTornadoSerialClient):
"""
Tornado based asynchronous serial client
"""
def __init__(self, *args, **kwargs):
"""
Initializes AsyncModbusSerialClient.
:param args:
:param kwargs:
"""
self.state = ModbusTransactionState.IDLE
self.timeout = kwargs.get('timeout', Defaults.Timeout)
self.baudrate = kwargs.get('baudrate', Defaults.Baudrate)
if self.baudrate > 19200:
self.silent_interval = 1.75 / 1000 # ms
else:
self._t0 = float((1 + 8 + 2)) / self.baudrate
self.silent_interval = 3.5 * self._t0
self.silent_interval = round(self.silent_interval, 6)
self.last_frame_end = 0.0
super(AsyncModbusSerialClient, self).__init__(*args, **kwargs)
def get_socket(self):
"""
Creates Pyserial object
:return: serial object
"""
return Serial(port=self.port, **self.serial_settings)
@gen.coroutine
def connect(self):
"""Connect to the socket identified by host and port
:returns: Future
:rtype: tornado.concurrent.Future
"""
conn = self.get_socket()
if self.io_loop is None:
self.io_loop = IOLoop.current()
try:
self.stream = SerialIOStream(conn, io_loop=self.io_loop)
except Exception as e:
LOGGER.exception(e)
self._connected = True
LOGGER.debug("Client connected")
raise gen.Return(self)
def execute(self, request):
"""
Executes a transaction
:param request: Request to be written on to the bus
:return:
"""
request.transaction_id = self.transaction.getNextTID()
def _clear_timer():
"""
Clear serial waiting timeout
"""
if self.timeout_handle:
self.io_loop.remove_timeout(self.timeout_handle)
self.timeout_handle = None
def _on_timeout():
"""
Got timeout while waiting data from serial port
"""
LOGGER.warning("serial receive 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 transaction:
transaction.set_exception(TimeOutException())
def _on_write_done(*args):
"""
Set up reader part after sucessful write to the serial
"""
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_fd_error(fd, *args):
_clear_timer()
self.io_loop.remove_handler(fd)
self.close()
self.transaction.getTransaction(request.transaction_id).set_exception(ModbusIOException(*args))
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
)
packet = self.framer.buildPacket(request)
f = self._build_response(request.transaction_id)
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: calculate once
if self.timeout:
self.timeout_handle = self.io_loop.add_timeout(time.time() + self.timeout, _on_timeout)
self._sendPacket(packet, callback=_on_write_done)
return f
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)
class AsyncModbusTCPClient(BaseTornadoClient):
"""
Tornado based Async tcp client
"""
def get_socket(self):
"""
Creates socket object
:return: socket
"""
return socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
class AsyncModbusUDPClient(BaseTornadoClient):
"""
Tornado based Async UDP client
"""
def get_socket(self):
"""
Create socket object
:return: socket
"""
return socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)