stringio now works with serial connections

also allow SECoP client connections via serial

Change-Id: I10c02532a9f8e9b8f16599b98c439742da6d8f5c
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/22525
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2020-02-26 11:36:28 +01:00
parent d021a116f1
commit 4bb11e249d
2 changed files with 141 additions and 111 deletions

View File

@ -16,21 +16,25 @@
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""asynchonous connections
"""asynchronous connections
generic class for byte oriented communication
includes implementation for TCP connections
support for asynchronous communication, but may be used also for StringIO
"""
import socket
import select
import time
import ast
from serial import Serial
from secop.lib import parseHostPort, tcpSocket, closeSocket
from secop.errors import ConfigError
class ConnectionClosed(ConnectionError):
@ -43,7 +47,7 @@ class AsynConn:
connection = None # is not None, if connected
defaultport = None
def __new__(cls, uri):
def __new__(cls, uri, end_of_line=b'\n'):
scheme = uri.split('://')[0]
iocls = cls.SCHEME_MAP.get(scheme, None)
if not iocls:
@ -51,12 +55,19 @@ class AsynConn:
try:
host_port = parseHostPort(uri, cls.defaultport)
except (ValueError, TypeError, AssertionError):
if 'COM' in uri:
raise ValueError("the correct uri for a COM port is: "
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'" )
if '/dev' in uri:
raise ValueError("the correct uri for a serial port is: "
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'" )
raise ValueError('invalid uri: %s' % uri)
iocls = cls.SCHEME_MAP['tcp']
uri = 'tcp://%s:%d' % host_port
return object.__new__(iocls)
def __init__(self, *args):
def __init__(self, uri, end_of_line=b'\n'):
self.end_of_line = end_of_line
self._rxbuffer = b''
def __del__(self):
@ -84,33 +95,40 @@ class AsynConn:
"""
raise NotImplementedError
def flush_recv(self):
"""flush all available bytes (return them)"""
raise NotImplementedError
def readline(self, timeout=None):
"""read one line
return either a complete line or None in case of timeout
the timeout argument may increase, but not decrease the default timeout
return either a complete line or None if no data available within 1 sec (self.timeout)
if a non-zero timeout is given, a timeout error is raised instead of returning None
the timeout effectively used will not be lower than self.timeout (1 sec)
"""
if timeout:
end = time.time() + timeout
while b'\n' not in self._rxbuffer:
while True:
splitted = self._rxbuffer.split(self.end_of_line, 1)
if len(splitted) == 2:
line, self._rxbuffer = splitted
return line
data = self.recv()
if not data:
if timeout:
if time.time() < end:
continue
raise TimeoutError('timeout in readline')
raise TimeoutError('timeout in readline (%g sec)' % timeout)
return None
self._rxbuffer += data
line, self._rxbuffer = self._rxbuffer.split(b'\n', 1)
return line
def writeline(self, line):
self.send(line + b'\n')
self.send(line + self.end_of_line)
class AsynTcp(AsynConn):
def __init__(self, uri):
super().__init__()
def __init__(self, uri, *args, **kwargs):
super().__init__(uri, *args, **kwargs)
self.uri = uri
if uri.startswith('tcp://'):
# should be the case always
@ -126,6 +144,13 @@ class AsynTcp(AsynConn):
"""send data (bytes!)"""
self.connection.sendall(data)
def flush_recv(self):
"""flush recv buffer"""
data = []
while select.select([self.connection], [], [], 0)[0]:
data.append(self.recv())
return b''.join(data)
def recv(self):
"""return bytes received within 1 sec"""
try:
@ -138,3 +163,80 @@ class AsynTcp(AsynConn):
raise ConnectionClosed() # marks end of connection
AsynTcp.register_scheme('tcp')
class AsynSerial(AsynConn):
"""a serial connection using pyserial
uri syntax:
serial://<path>?[<option>=<value>[+<option>=<value> ...]]
options (defaults, other examples):
baudrate=9600 # 4800, 115200
bytesize=8 # 5,6,7
parity=none # even, odd, mark, space
stopbits=1 # 1.5, 2
xonxoff=False # True
and others (see documentation of serial.Serial)
"""
PARITY_NAMES = {name[0]: name for name in ['NONE', 'ODD', 'EVEN', 'MASK', 'SPACE']}
def __init__(self, uri, *args, **kwargs):
super().__init__(uri, *args, **kwargs)
self.uri = uri
if uri.startswith('serial://'):
# should be the case always
uri = uri[9:]
uri = uri.split('?', 1)
dev = uri[0]
try:
options = dict((kv.split('=') for kv in uri[1].split('+')))
except IndexError: # no uri[1], no options
options = {}
except ValueError:
raise ConfigError('illegal serial options')
parity = options.pop('parity', None) # only parity is to be treated as text
for k, v in options.items():
try:
options[k] = ast.literal_eval(v.title()) # title(): turn false/true into False/True
except ValueError:
pass
if parity is not None:
name = parity.upper()
fullname = self.PARITY_NAMES[name[0]]
if not fullname.startswith(name):
raise ConfigError('illegal parity: %s' % parity)
options['parity'] = name[0]
if 'timeout' not in options:
options['timeout'] = self.timeout
try:
self.connection = Serial(dev, **options)
except ValueError as e:
raise ConfigError(e)
def disconnect(self):
if self.connection:
self.connection.close()
self.connection = None
def send(self, data):
"""send data (bytes!)"""
self.connection.write(data)
def flush_recv(self):
return self.connection.read(self.connection.in_waiting)
def recv(self):
"""return bytes received within 1 sec"""
if not self.connection: # disconnect() might have been called in between
raise ConnectionClosed()
n = self.connection.in_waiting
if n:
return self.connection.read(n)
data = self.connection.read(1)
return data + self.connection.read(self.connection.in_waiting)
AsynSerial.register_scheme('serial')