diff --git a/doc/source/tutorial_t_control.rst b/doc/source/tutorial_t_control.rst index 786e7a4..fd5c2d9 100644 --- a/doc/source/tutorial_t_control.rst +++ b/doc/source/tutorial_t_control.rst @@ -405,7 +405,7 @@ Appendix 2: Extract from the LakeShore Manual Reply *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 diff --git a/frappy/io.py b/frappy/io.py index d2cca96..9156635 100644 --- a/frappy/io.py +++ b/frappy/io.py @@ -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 (, , ) + :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): diff --git a/frappy/lib/classdoc.py b/frappy/lib/classdoc.py index 957688e..4979b39 100644 --- a/frappy/lib/classdoc.py +++ b/frappy/lib/classdoc.py @@ -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): diff --git a/test/test_io.py b/test/test_io.py new file mode 100644 index 0000000..d041620 --- /dev/null +++ b/test/test_io.py @@ -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 +# +# ***************************************************************************** + + +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]