891 lines
28 KiB
Python
891 lines
28 KiB
Python
"""
|
|
Asynchronous framework adapter for asyncio.
|
|
"""
|
|
import socket
|
|
import asyncio
|
|
import functools
|
|
import ssl
|
|
from pymodbus.exceptions import ConnectionException
|
|
from pymodbus.client.asynchronous.mixins import AsyncModbusClientMixin
|
|
from pymodbus.utilities import hexlify_packets
|
|
from pymodbus.transaction import FifoTransactionManager
|
|
import logging
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
DGRAM_TYPE = socket.SocketKind.SOCK_DGRAM
|
|
|
|
|
|
class BaseModbusAsyncClientProtocol(AsyncModbusClientMixin):
|
|
"""
|
|
Asyncio specific implementation of asynchronous modbus client protocol.
|
|
"""
|
|
|
|
#: Factory that created this instance.
|
|
factory = None
|
|
transport = None
|
|
|
|
async def execute(self, request=None):
|
|
"""
|
|
Executes requests asynchronously
|
|
:param request:
|
|
:return:
|
|
"""
|
|
req = self._execute(request)
|
|
resp = await asyncio.wait_for(req, timeout=self._timeout)
|
|
return resp
|
|
|
|
def connection_made(self, transport):
|
|
"""
|
|
Called when a connection is made.
|
|
|
|
The transport argument is the transport representing the connection.
|
|
:param transport:
|
|
:return:
|
|
"""
|
|
self.transport = transport
|
|
self._connectionMade()
|
|
|
|
if self.factory:
|
|
self.factory.protocol_made_connection(self)
|
|
|
|
def connection_lost(self, reason):
|
|
"""
|
|
Called when the connection is lost or closed.
|
|
|
|
The argument is either an exception object or None
|
|
:param reason:
|
|
:return:
|
|
"""
|
|
self.transport = None
|
|
self._connectionLost(reason)
|
|
|
|
if self.factory:
|
|
self.factory.protocol_lost_connection(self)
|
|
|
|
def data_received(self, data):
|
|
"""
|
|
Called when some data is received.
|
|
data is a non-empty bytes object containing the incoming data.
|
|
:param data:
|
|
:return:
|
|
"""
|
|
self._dataReceived(data)
|
|
|
|
def create_future(self):
|
|
"""
|
|
Helper function to create asyncio Future object
|
|
:return:
|
|
"""
|
|
return asyncio.Future()
|
|
|
|
def resolve_future(self, f, result):
|
|
"""
|
|
Resolves the completed future and sets the result
|
|
:param f:
|
|
:param result:
|
|
:return:
|
|
"""
|
|
if not f.done():
|
|
f.set_result(result)
|
|
|
|
def raise_future(self, f, exc):
|
|
"""
|
|
Sets exception of a future if not done
|
|
:param f:
|
|
:param exc:
|
|
:return:
|
|
"""
|
|
if not f.done():
|
|
f.set_exception(exc)
|
|
|
|
def _connectionMade(self):
|
|
"""
|
|
Called upon a successful client connection.
|
|
"""
|
|
_logger.debug("Client connected to modbus server")
|
|
self._connected = True
|
|
|
|
def _connectionLost(self, reason):
|
|
"""
|
|
Called upon a client disconnect
|
|
|
|
:param reason: The reason for the disconnect
|
|
"""
|
|
_logger.debug(
|
|
"Client disconnected from modbus server: %s" % reason)
|
|
self._connected = False
|
|
for tid in list(self.transaction):
|
|
self.raise_future(self.transaction.getTransaction(tid),
|
|
ConnectionException(
|
|
'Connection lost during request'))
|
|
|
|
@property
|
|
def connected(self):
|
|
"""
|
|
Return connection status.
|
|
"""
|
|
return self._connected
|
|
|
|
def write_transport(self, packet):
|
|
return self.transport.write(packet)
|
|
|
|
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 _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 _handleResponse(self, reply, **kwargs):
|
|
"""
|
|
Handle the processed response and link to correct deferred
|
|
|
|
:param reply: The reply to process
|
|
"""
|
|
if reply is not None:
|
|
tid = reply.transaction_id
|
|
handler = self.transaction.getTransaction(tid)
|
|
if handler:
|
|
self.resolve_future(handler, reply)
|
|
else:
|
|
_logger.debug("Unrequested message: " + str(reply))
|
|
|
|
def _buildResponse(self, tid):
|
|
"""
|
|
Helper method to return a deferred response
|
|
for the current request.
|
|
|
|
:param tid: The transaction identifier for this response
|
|
:returns: A defer linked to the latest request
|
|
"""
|
|
f = self.create_future()
|
|
if not self._connected:
|
|
self.raise_future(f, ConnectionException(
|
|
'Client is not connected'))
|
|
else:
|
|
self.transaction.addTransaction(f, tid)
|
|
return f
|
|
|
|
def close(self):
|
|
self.transport.close()
|
|
self._connected = False
|
|
|
|
|
|
class ModbusClientProtocol(BaseModbusAsyncClientProtocol, asyncio.Protocol):
|
|
"""
|
|
Asyncio specific implementation of asynchronous modbus client protocol.
|
|
"""
|
|
|
|
#: Factory that created this instance.
|
|
factory = None
|
|
transport = None
|
|
|
|
def data_received(self, data):
|
|
"""
|
|
Called when some data is received.
|
|
data is a non-empty bytes object containing the incoming data.
|
|
:param data:
|
|
:return:
|
|
"""
|
|
self._dataReceived(data)
|
|
|
|
|
|
class ModbusUdpClientProtocol(BaseModbusAsyncClientProtocol,
|
|
asyncio.DatagramProtocol):
|
|
"""
|
|
Asyncio specific implementation of asynchronous modbus udp client protocol.
|
|
"""
|
|
|
|
#: Factory that created this instance.
|
|
factory = None
|
|
|
|
def __init__(self, host=None, port=0, **kwargs):
|
|
self.host = host
|
|
self.port = port
|
|
super(self.__class__, self).__init__(**kwargs)
|
|
|
|
def datagram_received(self, data, addr):
|
|
self._dataReceived(data)
|
|
|
|
def write_transport(self, packet):
|
|
return self.transport.sendto(packet)
|
|
|
|
|
|
class ReconnectingAsyncioModbusTcpClient(object):
|
|
"""
|
|
Client to connect to modbus device repeatedly over TCP/IP."
|
|
"""
|
|
#: Minimum delay in milli seconds before reconnect is attempted.
|
|
DELAY_MIN_MS = 100
|
|
#: Maximum delay in milli seconds before reconnect is attempted.
|
|
DELAY_MAX_MS = 1000 * 60 * 5
|
|
|
|
def __init__(self, protocol_class=None, loop=None, **kwargs):
|
|
"""
|
|
Initialize ReconnectingAsyncioModbusTcpClient
|
|
:param protocol_class: Protocol used to talk to modbus device.
|
|
:param loop: Event loop to use
|
|
"""
|
|
#: Protocol used to talk to modbus device.
|
|
self.protocol_class = protocol_class or ModbusClientProtocol
|
|
#: Current protocol instance.
|
|
self.protocol = None
|
|
#: Event loop to use.
|
|
self.loop = loop or asyncio.get_event_loop()
|
|
self.host = None
|
|
self.port = 0
|
|
self.connected = False
|
|
#: Reconnect delay in milli seconds.
|
|
self.delay_ms = self.DELAY_MIN_MS
|
|
self._proto_args = kwargs
|
|
|
|
def reset_delay(self):
|
|
"""
|
|
Resets wait before next reconnect to minimal period.
|
|
"""
|
|
self.delay_ms = self.DELAY_MIN_MS
|
|
|
|
@asyncio.coroutine
|
|
def start(self, host, port=502):
|
|
"""
|
|
Initiates connection to start client
|
|
:param host:
|
|
:param port:
|
|
:return:
|
|
"""
|
|
# force reconnect if required:
|
|
self.stop()
|
|
|
|
_logger.debug('Connecting to %s:%s.' % (host, port))
|
|
self.host = host
|
|
self.port = port
|
|
yield from self._connect()
|
|
|
|
def stop(self):
|
|
"""
|
|
Stops client
|
|
:return:
|
|
"""
|
|
# prevent reconnect:
|
|
self.host = None
|
|
|
|
if self.connected:
|
|
if self.protocol:
|
|
if self.protocol.transport:
|
|
self.protocol.transport.close()
|
|
|
|
def _create_protocol(self):
|
|
"""
|
|
Factory function to create initialized protocol instance.
|
|
"""
|
|
protocol = self.protocol_class(**self._proto_args)
|
|
protocol.factory = self
|
|
return protocol
|
|
|
|
@asyncio.coroutine
|
|
def _connect(self):
|
|
_logger.debug('Connecting.')
|
|
try:
|
|
yield from self.loop.create_connection(self._create_protocol,
|
|
self.host,
|
|
self.port)
|
|
except Exception as ex:
|
|
_logger.warning('Failed to connect: %s' % ex)
|
|
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
|
self.reset_delay()
|
|
|
|
def protocol_made_connection(self, protocol):
|
|
"""
|
|
Protocol notification of successful connection.
|
|
"""
|
|
_logger.info('Protocol made connection.')
|
|
if not self.connected:
|
|
self.connected = True
|
|
self.protocol = protocol
|
|
else:
|
|
_logger.error('Factory protocol connect '
|
|
'callback called while connected.')
|
|
|
|
def protocol_lost_connection(self, protocol):
|
|
"""
|
|
Protocol notification of lost connection.
|
|
"""
|
|
if self.connected:
|
|
_logger.info('Protocol lost connection.')
|
|
if protocol is not self.protocol:
|
|
_logger.error('Factory protocol callback called '
|
|
'from unexpected protocol instance.')
|
|
|
|
self.connected = False
|
|
self.protocol = None
|
|
if self.host:
|
|
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.error('Factory protocol disconnect callback called while not connected.')
|
|
|
|
@asyncio.coroutine
|
|
def _reconnect(self):
|
|
_logger.debug('Waiting %d ms before next '
|
|
'connection attempt.' % self.delay_ms)
|
|
yield from asyncio.sleep(self.delay_ms / 1000)
|
|
self.delay_ms = min(2 * self.delay_ms, self.DELAY_MAX_MS)
|
|
yield from self._connect()
|
|
|
|
|
|
class AsyncioModbusTcpClient(object):
|
|
"""Client to connect to modbus device over TCP/IP."""
|
|
|
|
def __init__(self, host=None, port=502, protocol_class=None, loop=None, **kwargs):
|
|
"""
|
|
Initializes Asyncio Modbus Tcp Client
|
|
:param host: Host IP address
|
|
:param port: Port to connect
|
|
:param protocol_class: Protocol used to talk to modbus device.
|
|
:param loop: Asyncio Event loop
|
|
"""
|
|
#: Protocol used to talk to modbus device.
|
|
self.protocol_class = protocol_class or ModbusClientProtocol
|
|
#: Current protocol instance.
|
|
self.protocol = None
|
|
#: Event loop to use.
|
|
self.loop = loop or asyncio.get_event_loop()
|
|
|
|
self.host = host
|
|
self.port = port
|
|
|
|
self.connected = False
|
|
self._proto_args = kwargs
|
|
|
|
def stop(self):
|
|
"""
|
|
Stops the client
|
|
:return:
|
|
"""
|
|
if self.connected:
|
|
if self.protocol:
|
|
if self.protocol.transport:
|
|
self.protocol.transport.close()
|
|
|
|
def _create_protocol(self):
|
|
"""
|
|
Factory function to create initialized protocol instance.
|
|
"""
|
|
protocol = self.protocol_class(**self._proto_args)
|
|
protocol.factory = self
|
|
return protocol
|
|
|
|
@asyncio.coroutine
|
|
def connect(self):
|
|
"""
|
|
Connect and start Async client
|
|
:return:
|
|
"""
|
|
_logger.debug('Connecting.')
|
|
try:
|
|
yield from self.loop.create_connection(self._create_protocol,
|
|
self.host,
|
|
self.port)
|
|
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
|
except Exception as ex:
|
|
_logger.warning('Failed to connect: %s' % ex)
|
|
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
|
|
|
def protocol_made_connection(self, protocol):
|
|
"""
|
|
Protocol notification of successful connection.
|
|
"""
|
|
_logger.info('Protocol made connection.')
|
|
if not self.connected:
|
|
self.connected = True
|
|
self.protocol = protocol
|
|
else:
|
|
_logger.error('Factory protocol connect '
|
|
'callback called while connected.')
|
|
|
|
def protocol_lost_connection(self, protocol):
|
|
"""
|
|
Protocol notification of lost connection.
|
|
"""
|
|
if self.connected:
|
|
_logger.info('Protocol lost connection.')
|
|
if protocol is not self.protocol:
|
|
_logger.error('Factory protocol callback called'
|
|
' from unexpected protocol instance.')
|
|
|
|
self.connected = False
|
|
self.protocol = None
|
|
# if self.host:
|
|
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.error('Factory protocol disconnect'
|
|
' callback called while not connected.')
|
|
|
|
|
|
class ReconnectingAsyncioModbusTlsClient(ReconnectingAsyncioModbusTcpClient):
|
|
"""
|
|
Client to connect to modbus device repeatedly over TLS."
|
|
"""
|
|
def __init__(self, protocol_class=None, loop=None, framer=None, **kwargs):
|
|
"""
|
|
Initialize ReconnectingAsyncioModbusTcpClient
|
|
:param protocol_class: Protocol used to talk to modbus device.
|
|
:param loop: Event loop to use
|
|
"""
|
|
self.framer = framer
|
|
ReconnectingAsyncioModbusTcpClient.__init__(self, protocol_class, loop, **kwargs)
|
|
|
|
@asyncio.coroutine
|
|
def start(self, host, port=802, sslctx=None, server_hostname=None):
|
|
"""
|
|
Initiates connection to start client
|
|
:param host:
|
|
:param port:
|
|
:param sslctx:
|
|
:param server_hostname:
|
|
:return:
|
|
"""
|
|
self.sslctx = sslctx
|
|
if self.sslctx is None:
|
|
self.sslctx = ssl.create_default_context()
|
|
# According to MODBUS/TCP Security Protocol Specification, it is
|
|
# TLSv2 at least
|
|
self.sslctx.options |= ssl.OP_NO_TLSv1_1
|
|
self.sslctx.options |= ssl.OP_NO_TLSv1
|
|
self.sslctx.options |= ssl.OP_NO_SSLv3
|
|
self.sslctx.options |= ssl.OP_NO_SSLv2
|
|
self.server_hostname = server_hostname
|
|
yield from ReconnectingAsyncioModbusTcpClient.start(self, host, port)
|
|
|
|
@asyncio.coroutine
|
|
def _connect(self):
|
|
_logger.debug('Connecting.')
|
|
try:
|
|
yield from self.loop.create_connection(self._create_protocol,
|
|
self.host,
|
|
self.port,
|
|
ssl=self.sslctx,
|
|
server_hostname=self.server_hostname)
|
|
except Exception as ex:
|
|
_logger.warning('Failed to connect: %s' % ex)
|
|
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
|
self.reset_delay()
|
|
|
|
def _create_protocol(self):
|
|
"""
|
|
Factory function to create initialized protocol instance.
|
|
"""
|
|
protocol = self.protocol_class(framer=self.framer, **self._proto_args)
|
|
protocol.transaction = FifoTransactionManager(self)
|
|
protocol.factory = self
|
|
return protocol
|
|
|
|
class ReconnectingAsyncioModbusUdpClient(object):
|
|
"""
|
|
Client to connect to modbus device repeatedly over UDP.
|
|
"""
|
|
|
|
#: Reconnect delay in milli seconds.
|
|
delay_ms = 0
|
|
|
|
#: Maximum delay in milli seconds before reconnect is attempted.
|
|
DELAY_MAX_MS = 1000 * 60 * 5
|
|
|
|
def __init__(self, protocol_class=None, loop=None, **kwargs):
|
|
"""
|
|
Initializes ReconnectingAsyncioModbusUdpClient
|
|
:param protocol_class: Protocol used to talk to modbus device.
|
|
:param loop: Asyncio Event loop
|
|
"""
|
|
#: Protocol used to talk to modbus device.
|
|
self.protocol_class = protocol_class or ModbusUdpClientProtocol
|
|
#: Current protocol instance.
|
|
self.protocol = None
|
|
#: Event loop to use.
|
|
self.loop = loop or asyncio.get_event_loop()
|
|
|
|
self.host = None
|
|
self.port = 0
|
|
|
|
self.connected = False
|
|
self._proto_args = kwargs
|
|
self.reset_delay()
|
|
|
|
def reset_delay(self):
|
|
"""
|
|
Resets wait before next reconnect to minimal period.
|
|
"""
|
|
self.delay_ms = 100
|
|
|
|
@asyncio.coroutine
|
|
def start(self, host, port=502):
|
|
"""
|
|
Start reconnecting asynchronous udp client
|
|
:param host: Host IP to connect
|
|
:param port: Host port to connect
|
|
:return:
|
|
"""
|
|
# force reconnect if required:
|
|
self.stop()
|
|
|
|
_logger.debug('Connecting to %s:%s.' % (host, port))
|
|
|
|
# getaddrinfo returns a list of tuples
|
|
# - [(family, type, proto, canonname, sockaddr),]
|
|
# We want sockaddr which is a (ip, port) tuple
|
|
# udp needs ip addresses, not hostnames
|
|
addrinfo = yield from self.loop.getaddrinfo(host,
|
|
port,
|
|
type=DGRAM_TYPE)
|
|
self.host, self.port = addrinfo[0][-1]
|
|
|
|
yield from self._connect()
|
|
|
|
def stop(self):
|
|
"""
|
|
Stops connection and prevents reconnect
|
|
:return:
|
|
"""
|
|
# prevent reconnect:
|
|
self.host = None
|
|
|
|
if self.connected:
|
|
if self.protocol:
|
|
if self.protocol.transport:
|
|
self.protocol.transport.close()
|
|
|
|
def _create_protocol(self, host=None, port=0):
|
|
"""
|
|
Factory function to create initialized protocol instance.
|
|
"""
|
|
protocol = self.protocol_class(**self._proto_args)
|
|
protocol.host = host
|
|
protocol.port = port
|
|
protocol.factory = self
|
|
return protocol
|
|
|
|
@asyncio.coroutine
|
|
def _connect(self):
|
|
_logger.debug('Connecting.')
|
|
try:
|
|
yield from self.loop.create_datagram_endpoint(
|
|
functools.partial(self._create_protocol,
|
|
host=self.host,
|
|
port=self.port),
|
|
remote_addr=(self.host, self.port)
|
|
)
|
|
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
|
except Exception as ex:
|
|
_logger.warning('Failed to connect: %s' % ex)
|
|
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
|
|
|
def protocol_made_connection(self, protocol):
|
|
"""
|
|
Protocol notification of successful connection.
|
|
"""
|
|
_logger.info('Protocol made connection.')
|
|
if not self.connected:
|
|
self.connected = True
|
|
self.protocol = protocol
|
|
else:
|
|
_logger.error('Factory protocol connect callback '
|
|
'called while connected.')
|
|
|
|
def protocol_lost_connection(self, protocol):
|
|
"""
|
|
Protocol notification of lost connection.
|
|
"""
|
|
if self.connected:
|
|
_logger.info('Protocol lost connection.')
|
|
if protocol is not self.protocol:
|
|
_logger.error('Factory protocol callback called '
|
|
'from unexpected protocol instance.')
|
|
|
|
self.connected = False
|
|
self.protocol = None
|
|
if self.host:
|
|
asyncio.ensure_future(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.error('Factory protocol disconnect '
|
|
'callback called while not connected.')
|
|
|
|
@asyncio.coroutine
|
|
def _reconnect(self):
|
|
_logger.debug('Waiting %d ms before next '
|
|
'connection attempt.' % self.delay_ms)
|
|
yield from asyncio.sleep(self.delay_ms / 1000)
|
|
self.delay_ms = min(2 * self.delay_ms, self.DELAY_MAX_MS)
|
|
yield from self._connect()
|
|
|
|
|
|
class AsyncioModbusUdpClient(object):
|
|
"""
|
|
Client to connect to modbus device over UDP.
|
|
"""
|
|
|
|
def __init__(self, host=None, port=502, protocol_class=None, loop=None, **kwargs):
|
|
"""
|
|
Initializes Asyncio Modbus UDP Client
|
|
:param host: Host IP address
|
|
:param port: Port to connect
|
|
:param protocol_class: Protocol used to talk to modbus device.
|
|
:param loop: Asyncio Event loop
|
|
"""
|
|
#: Protocol used to talk to modbus device.
|
|
self.protocol_class = protocol_class or ModbusUdpClientProtocol
|
|
#: Current protocol instance.
|
|
self.protocol = None
|
|
#: Event loop to use.
|
|
self.loop = loop or asyncio.get_event_loop()
|
|
|
|
self.host = host
|
|
self.port = port
|
|
|
|
self.connected = False
|
|
self._proto_args = kwargs
|
|
|
|
def stop(self):
|
|
"""
|
|
Stops connection
|
|
:return:
|
|
"""
|
|
# prevent reconnect:
|
|
# self.host = None
|
|
|
|
if self.connected:
|
|
if self.protocol:
|
|
if self.protocol.transport:
|
|
self.protocol.transport.close()
|
|
|
|
def _create_protocol(self, host=None, port=0):
|
|
"""
|
|
Factory function to create initialized protocol instance.
|
|
"""
|
|
protocol = self.protocol_class(**self._proto_args)
|
|
protocol.host = host
|
|
protocol.port = port
|
|
protocol.factory = self
|
|
return protocol
|
|
|
|
@asyncio.coroutine
|
|
def connect(self):
|
|
_logger.debug('Connecting.')
|
|
try:
|
|
addrinfo = yield from self.loop.getaddrinfo(
|
|
self.host,
|
|
self.port,
|
|
type=DGRAM_TYPE)
|
|
_host, _port = addrinfo[0][-1]
|
|
yield from self.loop.create_datagram_endpoint(
|
|
functools.partial(self._create_protocol,
|
|
host=_host, port=_port),
|
|
remote_addr=(self.host, self.port)
|
|
)
|
|
_logger.info('Connected to %s:%s.' % (self.host, self.port))
|
|
except Exception as ex:
|
|
_logger.warning('Failed to connect: %s' % ex)
|
|
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
|
|
|
def protocol_made_connection(self, protocol):
|
|
"""
|
|
Protocol notification of successful connection.
|
|
"""
|
|
_logger.info('Protocol made connection.')
|
|
if not self.connected:
|
|
self.connected = True
|
|
self.protocol = protocol
|
|
else:
|
|
_logger.error('Factory protocol connect '
|
|
'callback called while connected.')
|
|
|
|
def protocol_lost_connection(self, protocol):
|
|
"""
|
|
Protocol notification of lost connection.
|
|
"""
|
|
if self.connected:
|
|
_logger.info('Protocol lost connection.')
|
|
if protocol is not self.protocol:
|
|
_logger.error('Factory protocol callback '
|
|
'called from unexpected protocol instance.')
|
|
|
|
self.connected = False
|
|
self.protocol = None
|
|
# if self.host:
|
|
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.error('Factory protocol disconnect '
|
|
'callback called while not connected.')
|
|
|
|
|
|
class AsyncioModbusSerialClient(object):
|
|
"""
|
|
Client to connect to modbus device over serial.
|
|
"""
|
|
transport = None
|
|
framer = None
|
|
|
|
def __init__(self, port, protocol_class=None, framer=None, loop=None,
|
|
baudrate=9600, bytesize=8, parity='N', stopbits=1, **serial_kwargs):
|
|
"""
|
|
Initializes Asyncio Modbus Serial Client
|
|
:param port: Port to connect
|
|
:param protocol_class: Protocol used to talk to modbus device.
|
|
:param framer: Framer to use
|
|
:param loop: Asyncio Event loop
|
|
"""
|
|
#: Protocol used to talk to modbus device.
|
|
self.protocol_class = protocol_class or ModbusClientProtocol
|
|
#: Current protocol instance.
|
|
self.protocol = None
|
|
#: Event loop to use.
|
|
self.loop = loop or asyncio.get_event_loop()
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.bytesize = bytesize
|
|
self.parity = parity
|
|
self.stopbits = stopbits
|
|
self.framer = framer
|
|
self._extra_serial_kwargs = serial_kwargs
|
|
self._connected_event = asyncio.Event()
|
|
|
|
def stop(self):
|
|
"""
|
|
Stops connection
|
|
:return:
|
|
"""
|
|
if self._connected:
|
|
if self.protocol:
|
|
if self.protocol.transport:
|
|
self.protocol.transport.close()
|
|
|
|
def _create_protocol(self):
|
|
protocol = self.protocol_class(framer=self.framer)
|
|
protocol.factory = self
|
|
return protocol
|
|
|
|
@property
|
|
def _connected(self):
|
|
return self._connected_event.is_set()
|
|
|
|
@asyncio.coroutine
|
|
def connect(self):
|
|
"""
|
|
Connect Async client
|
|
:return:
|
|
"""
|
|
_logger.debug('Connecting.')
|
|
try:
|
|
from serial_asyncio import create_serial_connection
|
|
|
|
yield from create_serial_connection(
|
|
self.loop, self._create_protocol, self.port, baudrate=self.baudrate,
|
|
bytesize=self.bytesize, stopbits=self.stopbits, parity=self.parity, **self._extra_serial_kwargs
|
|
)
|
|
yield from self._connected_event.wait()
|
|
_logger.info('Connected to %s', self.port)
|
|
except Exception as ex:
|
|
_logger.warning('Failed to connect: %s', ex)
|
|
|
|
def protocol_made_connection(self, protocol):
|
|
"""
|
|
Protocol notification of successful connection.
|
|
"""
|
|
_logger.info('Protocol made connection.')
|
|
if not self._connected:
|
|
self._connected_event.set()
|
|
self.protocol = protocol
|
|
else:
|
|
_logger.error('Factory protocol connect '
|
|
'callback called while connected.')
|
|
|
|
def protocol_lost_connection(self, protocol):
|
|
"""
|
|
Protocol notification of lost connection.
|
|
"""
|
|
if self._connected:
|
|
_logger.info('Protocol lost connection.')
|
|
if protocol is not self.protocol:
|
|
_logger.error('Factory protocol callback called'
|
|
' from unexpected protocol instance.')
|
|
|
|
self._connected_event.clear()
|
|
self.protocol = None
|
|
# if self.host:
|
|
# asyncio.asynchronous(self._reconnect(), loop=self.loop)
|
|
else:
|
|
_logger.error('Factory protocol disconnect callback '
|
|
'called while not connected.')
|
|
|
|
|
|
@asyncio.coroutine
|
|
def init_tcp_client(proto_cls, loop, host, port, **kwargs):
|
|
"""
|
|
Helper function to initialize tcp client
|
|
:param proto_cls:
|
|
:param loop:
|
|
:param host:
|
|
:param port:
|
|
:param kwargs:
|
|
:return:
|
|
"""
|
|
client = ReconnectingAsyncioModbusTcpClient(protocol_class=proto_cls,
|
|
loop=loop, **kwargs)
|
|
yield from client.start(host, port)
|
|
return client
|
|
|
|
|
|
@asyncio.coroutine
|
|
def init_tls_client(proto_cls, loop, host, port, sslctx=None,
|
|
server_hostname=None, framer=None, **kwargs):
|
|
"""
|
|
Helper function to initialize tcp client
|
|
:param proto_cls:
|
|
:param loop:
|
|
:param host:
|
|
:param port:
|
|
:param sslctx:
|
|
:param server_hostname:
|
|
:param framer:
|
|
:param kwargs:
|
|
:return:
|
|
"""
|
|
client = ReconnectingAsyncioModbusTlsClient(protocol_class=proto_cls,
|
|
loop=loop, framer=framer,
|
|
**kwargs)
|
|
yield from client.start(host, port, sslctx, server_hostname)
|
|
return client
|
|
|
|
|
|
@asyncio.coroutine
|
|
def init_udp_client(proto_cls, loop, host, port, **kwargs):
|
|
"""
|
|
Helper function to initialize UDP client
|
|
:param proto_cls:
|
|
:param loop:
|
|
:param host:
|
|
:param port:
|
|
:param kwargs:
|
|
:return:
|
|
"""
|
|
client = ReconnectingAsyncioModbusUdpClient(protocol_class=proto_cls,
|
|
loop=loop, **kwargs)
|
|
yield from client.start(host, port)
|
|
return client
|