add StringIO.writeline, improve StringIO.multicomm

- StringIO.writeline sends a command and does not expect a reply
- StringIO.multicomm and BytesIO.multicomm is improved in order
  to insert individual delays in between lines and individual
  noreply flags

+ fix a bug in tutorial_t_control
+ improve readability of frappy.lib.classdoc.indent_description

Change-Id: I9dea113e19147684ec41aca5267a79816bbf202c
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/32267
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2023-10-05 17:49:31 +02:00
parent fd0e762d18
commit 158477792f
4 changed files with 189 additions and 22 deletions

View File

@ -405,7 +405,7 @@ Appendix 2: Extract from the LakeShore Manual
Reply <range> *term*
**Operation Complete Query**
----------------------------------------------
Command *OPC?
Command \*OPC?
Reply 1
Description in Frappy, we append this command to request in order
to generate a reply

View File

@ -25,17 +25,17 @@ other future extensions of AsynConn
"""
import re
import time
import threading
import time
from frappy.lib.asynconn import AsynConn, ConnectionClosed
from frappy.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, \
StringType, TupleOf, ValueType
from frappy.errors import CommunicationFailedError, ConfigError, ProgrammingError, \
SilentCommunicationFailedError as SilentError
from frappy.modules import Attached, Command, \
Communicator, Module, Parameter, Property
from frappy.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, \
IntRange, StringType, StructOf, TupleOf, ValueType
from frappy.errors import CommunicationFailedError, ConfigError, \
ProgrammingError, SilentCommunicationFailedError as SilentError
from frappy.lib import generalConfig
from frappy.lib.asynconn import AsynConn, ConnectionClosed
from frappy.modules import Attached, Command, Communicator, Module, \
Parameter, Property
generalConfig.set_default('legacy_hasiodev', False)
@ -76,8 +76,11 @@ class HasIO(Module):
def communicate(self, *args):
return self.io.communicate(*args)
def multicomm(self, *args):
return self.io.multicomm(*args)
def writeline(self, *args):
return self.io.writeline(*args)
def multicomm(self, *args, **kwds):
return self.io.multicomm(*args, **kwds)
class HasIodev(HasIO):
@ -287,7 +290,7 @@ class StringIO(IOBase):
f' does not match {regexp!r}')
@Command(StringType(), result=StringType())
def communicate(self, command):
def communicate(self, command, noreply=False):
"""send a command and receive a reply
using end_of_line, encoding and self._lock
@ -314,6 +317,8 @@ class StringIO(IOBase):
self.comLog('garbage: %r', garbage)
self._conn.send(cmd + self._eol_write)
self.comLog('> %s', cmd.decode(self.encoding))
if noreply:
return None
reply = self._conn.readline(self.timeout)
except ConnectionClosed:
self.closeConnection()
@ -329,13 +334,69 @@ class StringIO(IOBase):
self.log.error(self._last_error)
raise SilentError(repr(e)) from e
@Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
def multicomm(self, commands):
"""communicate multiple request/replies in one row"""
@Command(StringType())
def writeline(self, command):
"""send a command without needing a reply
For keeping a request-reply scheme it is recommended to overwrite
this method to append a query on the same line, for example:
.. code::
def writeline(self, command):
self.communicate(command + ';*OPC?')
or to add an additional query which is returning always a reply, e.g.:
.. code::
def writeline(self, command):
with self._lock: # important!
self.communicate(command, noreply=True)
self.communicate('*OPC?')
The first version is preferred when the hardware allows to join several
commands by a separator.
"""
self.communicate(command, noreply=True)
@Command(ArrayOf(TupleOf(StringType(), BoolType(), FloatRange(0, unit='s'))),
result=ArrayOf(StringType()))
def multicomm(self, requests):
"""communicate multiple request/replies in one go
:param requests: a sequence of tuple of (command, request_expected, delay)
if called internally, a sequence of strings (command) is also accepted
:return: list of replies
This method may be rarely used, it is intended when the hardware needs
that several commands are not intercepted by an other client or by the poller,
for example selecting a channel before reading it. Or when wait times different
from 'wait_before' have to be specified.
These cases may also handled by adding an additional method to the IO class.
This could also be a custom SECoP command.
Or, in the case where all useful commands in this IO class need it,
:meth:`communicate` may be overridden.
This method should be used in the following cases:
1) you want to use a generic communicator covering above use cases over SECoP.
2) you do not want to subclass the IO class.
"""
replies = []
with self._lock:
for cmd in commands:
replies.append(self.communicate(cmd))
for request in requests:
if isinstance(request, str):
cmd, expect_reply, delay = request, True, 0
else:
cmd, expect_reply, delay = request
if expect_reply:
replies.append(self.communicate(cmd))
else:
self.writeline(cmd)
if delay:
time.sleep(delay)
return replies
@ -426,13 +487,20 @@ class BytesIO(IOBase):
self.log.error(self._last_error)
raise SilentError(repr(e)) from e
@Command((ArrayOf(TupleOf(BLOBType(), IntRange(0)))), result=ArrayOf(BLOBType()))
@Command(StructOf(requests=ArrayOf(TupleOf(BLOBType(), IntRange(0), FloatRange(0, unit='s')))),
result=ArrayOf(BLOBType()))
def multicomm(self, requests):
"""communicate multiple request/replies in one row"""
"""communicate multiple request/replies in one go
:param requests: sequence of tuple (<command>, <expected reply length>, <delay>)
:return: list of replies
"""
replies = []
with self._lock:
for request in requests:
replies.append(self.communicate(*request))
for cmd, replylen, delay in requests:
replies.append(self.communicate(cmd, replylen))
if delay:
time.sleep(delay)
return replies
def readBytes(self, nbytes):

View File

@ -27,7 +27,8 @@ from frappy.modules import Command, HasProperties, Module, Parameter, Property
def indent_description(p):
"""indent lines except first one"""
return indent(p.description, ' ').replace(' ', '', 1)
space = ' ' * 6
return indent(p.description, space).replace(space, '', 1)
def fmt_param(name, param):

98
test/test_io.py Normal file
View File

@ -0,0 +1,98 @@
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
import time
import pytest
from frappy.io import StringIO
class Time:
def __init__(self, items):
self.items = items
def sleep(self, seconds):
self.items.append(seconds)
class IO(StringIO):
def __init__(self):
self.items = []
self.propertyValues = {}
self.earlyInit()
def communicate(self, command, noreply=False):
self.items.append(command)
return command.upper()
class AppendedIO(IO):
def communicate(self, command, noreply=False):
self.items.append(command)
if noreply:
assert not '?' in command
return None
assert '?' in command
return '1'
def writeline(self, command):
assert self.communicate(command + ';*OPC?') == '1'
class CompositeIO(AppendedIO):
def writeline(self, command):
# the following is not possible, as multicomm is recursively calling writeline:
# self.multicomm([(command, False, 0), ('*OPC?', True, 0)])
# anyway, above code is less nice
with self._lock:
self.communicate(command, noreply=True)
self.communicate('*OPC?')
def test_writeline_pure():
io = IO()
assert io.writeline('noreply') is None
assert io.items == ['noreply']
@pytest.mark.parametrize(
'ioclass, cmds', [
(AppendedIO, ['SETP 1,1;*OPC?']),
(CompositeIO, ['SETP 1,1', '*OPC?']),
])
def test_writeline_extended(ioclass, cmds):
io = ioclass()
io.writeline('SETP 1,1')
assert io.items == cmds
def test_multicomm_simple():
io = IO()
assert io.multicomm(['first', 'second']) == ['FIRST', 'SECOND']
assert io.items == ['first', 'second']
def test_multicomm_with_delay(monkeypatch):
io = IO()
tm = Time(io.items)
monkeypatch.setattr(time, 'sleep', tm.sleep)
assert io.multicomm([('noreply', False, 1), ('reply', True, 2)]) == ['REPLY']
assert io.items == ['noreply', 1, 'reply', 2]