migrated secop_psi drivers to new syntax

- includes all changes up to 'fix inheritance order' from git_mlz
  6a32ecf34224c559ae558efd7c0d20078d09463b

Change-Id: Ie3ceee3dbd0a9284b47b1d5b5dbe262eebe8f283
This commit is contained in:
zolliker 2021-02-24 16:15:23 +01:00
parent bc5edec06f
commit 41baf5805f
79 changed files with 2610 additions and 3952 deletions

View File

@ -1,51 +1,20 @@
/* this is for the sphinx_rtd_theme */
div.wy-nav-content div.wy-nav-content
{ {
max-width: 100% !important; max-width: 100% !important;
} }
/* this is for the alabaser theme */
div.body {
max-width: 100%;
}
div.bodywrapper {
margin: 0 0 0 26%;
}
div.sphinxsidebar {
width: 25%;
}
pre, tt, code {
font-size: 0.75em;
}
@media screen and (max-width: 875px) {
div.bodywrapper {
margin: 0;
}
div.sphinxsidebar {
width: 102.5%;
}
div.document {
width: 100%;
}
}
dd {
padding-bottom: 0.5em;
}
/* make nested bullet lists nicer (ales too much space above inner nested list) */
.rst-content .section ul li ul {
margin-top: 0px;
margin-bottom: 6px;
}
/* make some bullet lists more dense (this rule exists in theme.css, but not important)*/ /* make some bullet lists more dense (this rule exists in theme.css, but not important)*/
.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child { .wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
/* overwrite custom font (to save bandwidth not using a custom font) */
body {
font-family: "proxima-nova", "Helvetica Neue", Arial, sans-serif;
}
h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
font-family: "ff-tisa-web-pro", "Georgia", Arial, sans-serif;
}

View File

@ -100,19 +100,8 @@ default_role = 'any'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
if False: # alabaster import sphinx_rtd_theme
html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'page_width': '100%',
'fixed_sidebar': True,
}
else:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
# If not None, a 'Last updated on:' timestamp is inserted at every page # If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format. # bottom, using the given strftime format.
@ -223,3 +212,8 @@ epub_exclude_files = ['search.html']
# Example configuration for intersphinx: refer to the Python standard library. # Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None} intersphinx_mapping = {'https://docs.python.org/': None}
from secop.lib.classdoc import class_doc_handler
def setup(app):
app.connect('autodoc-process-docstring', class_doc_handler)

View File

@ -5,10 +5,10 @@ Module Base Classes
................... ...................
.. autoclass:: secop.modules.Module .. autoclass:: secop.modules.Module
:members: earlyInit, initModule, startModule :members: earlyInit, initModule, startModule, pollerClass
.. autoclass:: secop.modules.Readable .. autoclass:: secop.modules.Readable
:members: pollerClass, Status :members: Status
.. autoclass:: secop.modules.Writable .. autoclass:: secop.modules.Writable
@ -21,13 +21,11 @@ Parameters, Commands and Properties
.. autoclass:: secop.params.Parameter .. autoclass:: secop.params.Parameter
.. autoclass:: secop.params.Command .. autoclass:: secop.params.Command
.. autoclass:: secop.params.Override
.. autoclass:: secop.properties.Property .. autoclass:: secop.properties.Property
.. autoclass:: secop.modules.Attached .. autoclass:: secop.modules.Attached
:show-inheritance: :show-inheritance:
Datatypes Datatypes
......... .........

View File

@ -1,6 +1,14 @@
PSI (SINQ) PSI (SINQ)
---------- ----------
CCU4 tutorial example
.....................
.. automodule:: secop_psi.ccu4
:show-inheritance:
:members:
PPMS PPMS
.... ....

View File

@ -1,7 +0,0 @@
Tutorial
--------
.. toctree::
:maxdepth: 2
tutorial_helevel

View File

@ -3,12 +3,13 @@ HeLevel - a Simple Driver
Coding the Driver Coding the Driver
----------------- -----------------
For this tutorial we choose as an example a cryostat. Let us start with the helium level meter, For this tutorial we choose as an example a cryostat. Let us start with the helium level
as this is the simplest module. meter, as this is the simplest module.
As mentioned in the introduction, we have to code the access to the hardware (driver), and the Frappy As mentioned in the introduction, we have to code the access to the hardware (driver),
framework will deal with the SECoP interface. The code for the driver is located in a subdirectory and the Frappy framework will deal with the SECoP interface. The code for the driver is
named after the facility or institute programming the driver in our case *secop_psi*. located in a subdirectory named after the facility or institute programming the driver
We create a file named from the electronic device CCU4 we use here for the He level reading. in our case *secop_psi*. We create a file named from the electronic device CCU4 we use
here for the He level reading.
CCU4 luckily has a very simple and logical protocol: CCU4 luckily has a very simple and logical protocol:
@ -20,9 +21,8 @@ CCU4 luckily has a very simple and logical protocol:
.. code:: python .. code:: python
# the most common classes can be imported from secop.core # the most common Frappy classes can be imported from secop.core
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, \ from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
StringIO, HasIodev
class CCU4IO(StringIO): class CCU4IO(StringIO):
@ -33,30 +33,48 @@ CCU4 luckily has a very simple and logical protocol:
identification = [('cid', r'CCU4.*')] identification = [('cid', r'CCU4.*')]
# inheriting the HasIodev mixin creates us the things needed for talking # inheriting the HasIodev mixin creates us a private attribute *_iodev*
# with a device by means of the sendRecv method # for talking with the hardware
# Readable as a base class defines the value and status parameters # Readable as a base class defines the value and status parameters
class HeLevel(HasIodev, Readable): class HeLevel(HasIodev, Readable):
"""He Level channel of CCU4""" """He Level channel of CCU4"""
# define or alter the parameters
parameters = {
# we are changing the 'unit' parameter property of the inherited 'value'
# parameter, therefore 'Override'
'value': Override(unit='%'),
}
# define the communication class to create the IO module # define the communication class to create the IO module
iodevClass = CCU4IO iodevClass = CCU4IO
# define or alter the parameters
# as Readable.value exists already, we give only the modified property 'unit'
value = Parameter(unit='%')
def read_value(self): def read_value(self):
# method for reading the main value # method for reading the main value
reply = self.sendRecv('h') # send 'h\n' and get the reply 'h=<value>\n' reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
name, txtvalue = reply.split('=') name, txtvalue = reply.split('=')
assert name == 'h' # check that we got a reply to our command assert name == 'h' # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float return txtvalue # the framework will automatically convert the string to a float
The class :class:`CCU4`, an extension of (:class:`secop.stringio.StringIO`) serves as
communication class. The class :class:`secop_psi.ccu4.CCU4IO`, an extension of (:class:`secop.stringio.StringIO`)
serves as communication class.
:Note:
You might wonder why the parameter *value* is declared here as class attribute.
In Python, usually class attributes are used to set a default value which might
be overwritten in a method. But class attributes can do more, look for Python
descriptors or properties if you are interested in details.
In Frappy, the *Parameter* class is a descriptor, which does the magic needed for
the SECoP interface. Given ``lev`` as an instance of the class ``HeLevel`` above,
``lev.value`` will just return its internal cached value.
``lev.value = 85.3`` will try to convert to the data type of the parameter,
put it to the internal cache and send a messages to the SECoP clients telling
that ``lev.value`` has got a new value.
For getting a value from the hardware, you have to call ``lev.read_value()``.
Frappy has replaced your version of *read_value* with a wrapped one which
also takes care to announce the change to the clients.
Even when you did not code this method, Frappy adds it silently, so calling
``<module>.read_<parameter>`` will be possible for all parameters declared
in a module.
Above is already the code for a very simple working He Level meter driver. For a next step, Above is already the code for a very simple working He Level meter driver. For a next step,
we want to improve it: we want to improve it:
@ -66,32 +84,26 @@ we want to improve it:
* We want to be able to switch the Level Monitor to fast reading before we start to fill. * We want to be able to switch the Level Monitor to fast reading before we start to fill.
Let us start to code these additions. We do not need to declare the status parameter, Let us start to code these additions. We do not need to declare the status parameter,
as it is inherited from *Readable*. But we declare the new parameters *empty*, *full* and *fast*, as it is inherited from *Readable*. But we declare the new parameters *empty_length*,
and we have to code the communication and convert the status codes from the hardware to *full_length* and *sample_rate*, and we have to code the communication and convert
the standard SECoP status codes. the status codes from the hardware to the standard SECoP status codes.
.. code:: python .. code:: python
... ...
# define or alter the parameters # the first two arguments to Parameter are 'description' and 'datatype'
parameters = { # it is highly recommended to define always the physical unit
empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'),
... readonly=False)
full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'),
# the first two arguments to Parameter are 'description' and 'datatype' readonly=False)
# it is highly recommended to define always the physical unit sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False)
'empty': Parameter('warm length when empty', FloatRange(0, 2000),
readonly=False, unit='mm'),
'full': Parameter('warm length when full', FloatRange(0, 2000),
readonly=False, unit='mm'),
'fast': Parameter('fast reading', BoolType(),
readonly=False),
}
... ...
Status = Readable.Status Status = Readable.Status
# conversion of the code from the CCU4 parameter 'hsf'
STATUS_MAP = { STATUS_MAP = {
0: (Status.IDLE, 'sensor ok'), 0: (Status.IDLE, 'sensor ok'),
1: (Status.ERROR, 'sensor warm'), 1: (Status.ERROR, 'sensor warm'),
@ -102,69 +114,98 @@ the standard SECoP status codes.
} }
def read_status(self): def read_status(self):
name, txtvalue = self.sendRecv('hsf').split('=') name, txtvalue = self._iodev.communicate('hsf').split('=')
assert name == 'hsf' assert name == 'hsf'
return self.STATUS_MAP(int(txtvalue)) return self.STATUS_MAP(int(txtvalue))
def read_emtpy(self): def read_empty_length(self):
name, txtvalue = self.sendRecv('hem').split('=') name, txtvalue = self._iodev.communicate('hem').split('=')
assert name == 'hem' assert name == 'hem'
return txtvalue return txtvalue
def write_empty(self, value): def write_empty_length(self, value):
name, txtvalue = self.sendRecv('hem=%g' % value).split('=') name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
assert name == 'hem' assert name == 'hem'
return txtvalue return txtvalue
... ...
Here we start to realize, that we will repeat similar code for other parameters, which means it might be
worth to create our own *_sendRecv* method, and then the *read_<param>* and *write_<param>* methods Here we start to realize, that we will repeat similar code for other parameters,
will become shorter: which means it might be worth to create a *query* method, and then the
*read_<param>* and *write_<param>* methods will become shorter:
.. code:: python .. code:: python
... ...
def _sendRecv(self, cmd): class HeLevel(Readable):
# method may be used for reading and writing parameters
name, txtvalue = self.sendRecv(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
return txtvalue # the framework will automatically convert the string to a float
def read_value(self):
return self._sendRecv('h')
...
def read_status(self):
return self.STATUS_MAP(int(self._sendRecv('hsf')))
def read_empty(self): ...
return self._sendRecv('hem')
def write_empty(self, value): def query(self, cmd):
return self._sendRecv('hem=%g' % value) """send a query and get the response
def read_full(self): :param cmd: the name of the parameter to query or '<parameter>=<value'
return self._sendRecv('hfu') for changing a parameter
:returns: the (new) value of the parameter
def write_full(self, value): """
return self._sendRecv('hfu=%g' % value) name, txtvalue = self._iodev.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command
def read_fast(self): return txtvalue # Frappy will automatically convert the string to the needed data type
return self._sendRecv('hf')
def read_value(self):
def write_fast(self, value): return self.query('h')
return self._sendRecv('hf=%s' % value)
def read_status(self):
return self.STATUS_MAP[int(self.query('hsf'))]
def read_empty_length(self):
return self.query('hem')
def write_empty_length(self, value):
return self.query('hem=%g' % value)
def read_full_length(self):
return self.query('hfu')
def write_full_length(self, value):
return self.query('hfu=%g' % value)
def read_sample_rate(self):
return self.query('hf')
def write_sample_rate(self, value):
return self.query('hf=%d' % value)
:Note:
It make sense to unify *empty_length* and *full_length* to one parameter *calibration*,
as a :class:`secop.datatypes.StructOf` with members *empty_length* and *full_length*:
.. code:: python
calibration = Parameter(
'sensor calibration',
StructOf(empty_length=FloatRange(0, 2000, unit='mm'),
full_length=FloatRange(0, 2000, unit='mm')),
readonly=False)
For simplicity we stay with two float parameters for this tutorial.
The full documentation of the example can be found here: :class:`secop_psi.ccu4.HeLevel`
Configuration Configuration
------------- -------------
Before we continue coding, we may try out what we have coded and create a configuration file. Before we continue coding, we may try out what we have coded and create a configuration file.
The directory tree of the Frappy framework contains the code for all drivers, but the configuration The directory tree of the Frappy framework contains the code for all drivers, but the
file determines, which code will finally be loaded. We choose the name *example_cryo* configuration file determines, which code will be loaded when a server is started.
and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdirectory: We choose the name *example_cryo* and create therefore a configuration file
*example_cryo.cfg* in the *cfg* subdirectory:
``cfg/example_cryo.cfg``: ``cfg/example_cryo.cfg``:
@ -172,7 +213,7 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
[NODE] [NODE]
description = this is an example cryostat for the Frappy tutorial description = this is an example cryostat for the Frappy tutorial
id = example_cryo.sampleenvironment.org id = example_cryo.psi.ch
[INTERFACE] [INTERFACE]
uri = tcp://5000 uri = tcp://5000
@ -181,28 +222,29 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
description = He level of the cryostat He reservoir description = He level of the cryostat He reservoir
class = secop_psi.ccu4.HeLevel class = secop_psi.ccu4.HeLevel
uri = linse-moxa-4.psi.ch:3001 uri = linse-moxa-4.psi.ch:3001
empty = 380 empty_length = 380
full = 0 full_length = 0
A configuration file contains several sections with a header encloded by rectangular brackets. A configuration file contains several sections with a header enclosed by rectangular brackets.
The *NODE* section describes the main properties of the SEC Node: a description of the node and The *NODE* section describes the main properties of the SEC Node: a description of the node
an id, which should be globally unique. and an id, which should be globally unique.
The *INTERFACE* section defines the address of the server, usually the only important value here The *INTERFACE* section defines the address of the server, usually the only important value
is the TCP port under which the server will be accessible. Currently only tcp is supported. here is the TCP port under which the server will be accessible. Currently only tcp is
supported.
All the other sections define the SECoP modules to be used. A module section at least contains a All the other sections define the SECoP modules to be used. A module section at least contains a
human readable *description*, and the Python *class* used. Other properties or parameter values may human readable *description*, and the Python *class* used. Other properties or parameter values may
follow, in this case the *uri* for the communication with the He level monitor and the values for follow, in this case the *uri* for the communication with the He level monitor and the values for
configuring the He Level sensor. We might also alter parameter properties, for example we may hide configuring the He Level sensor. We might also alter parameter properties, for example we may hide
the parameters *empty* and *full* from the client by defining: the parameters *empty_length* and *full_length* from the client by defining:
.. code:: ini .. code:: ini
empty.export = False empty_length.export = False
full.export = False full_length.export = False
However, we do not do this here, as it is nice to try out chaning parameters for a test! However, we do not put this here, as it is nice to try out changing parameters for a test!
**name** *(x)* *to be continued*

View File

@ -1,147 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""basic validators (for properties)"""
import re
from secop.errors import ProgrammingError
def FloatProperty(value):
return float(value)
def PositiveFloatProperty(value):
value = float(value)
if value > 0:
return value
raise ValueError('Value must be >0 !')
def NonNegativeFloatProperty(value):
value = float(value)
if value >= 0:
return value
raise ValueError('Value must be >=0 !')
def IntProperty(value):
if int(value) == float(value):
return int(value)
raise ValueError('Can\'t convert %r to int!' % value)
def PositiveIntProperty(value):
value = IntProperty(value)
if value > 0:
return value
raise ValueError('Value must be >0 !')
def NonNegativeIntProperty(value):
value = IntProperty(value)
if value >= 0:
return value
raise ValueError('Value must be >=0 !')
def BoolProperty(value):
try:
if value.lower() in ['0', 'false', 'no', 'off',]:
return False
if value.lower() in ['1', 'true', 'yes', 'on', ]:
return True
except AttributeError: # was no string
if bool(value) == value:
return value
raise ValueError('%r is no valid boolean: try one of True, False, "on", "off",...' % value)
def StringProperty(value):
return str(value)
def UnitProperty(value):
# probably too simple!
for s in str(value):
if s.lower() not in '°abcdefghijklmnopqrstuvwxyz':
raise ValueError('%r is not a valid unit!')
def FmtStrProperty(value, regexp=re.compile(r'^%\.?\d+[efg]$')):
value=str(value)
if regexp.match(value):
return value
raise ValueError('%r is not a valid fmtstr!' % value)
def OneOfProperty(*args):
# literally oneof!
if not args:
raise ProgrammingError('OneOfProperty needs some argumets to check against!')
def OneOfChecker(value):
if value not in args:
raise ValueError('Value must be one of %r' % list(args))
return value
return OneOfChecker
def NoneOr(checker):
if not callable(checker):
raise ProgrammingError('NoneOr needs a basic validator as Argument!')
def NoneOrChecker(value):
if value is None:
return None
return checker(value)
return NoneOrChecker
def EnumProperty(**kwds):
if not kwds:
raise ProgrammingError('EnumProperty needs a mapping!')
def EnumChecker(value):
if value in kwds:
return kwds[value]
if value in kwds.values():
return value
raise ValueError('Value must be one of %r' % list(kwds))
return EnumChecker
def TupleProperty(*checkers):
if not checkers:
checkers = [None]
for c in checkers:
if not callable(c):
raise ProgrammingError('TupleProperty needs basic validators as Arguments!')
def TupleChecker(values):
if len(values)==len(checkers):
return tuple(c(v) for c, v in zip(checkers, values))
raise ValueError('Value needs %d elements!' % len(checkers))
return TupleChecker
def ListOfProperty(checker):
if not callable(checker):
raise ProgrammingError('ListOfProperty needs a basic validator as Argument!')
def ListOfChecker(values):
return [checker(v) for v in values]
return ListOfChecker

View File

@ -22,21 +22,22 @@
# ***************************************************************************** # *****************************************************************************
"""general SECoP client""" """general SECoP client"""
import time
import queue
import json import json
from threading import Event, RLock, current_thread import queue
import time
from collections import defaultdict from collections import defaultdict
from threading import Event, RLock, current_thread
from secop.lib import mkthread, formatExtendedTraceback, formatExtendedStack
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.datatypes import get_datatype
from secop.protocol.interface import encode_msg_frame, decode_msg
from secop.protocol.messages import REQUEST2REPLY, ERRORPREFIX, EVENTREPLY, WRITEREQUEST, WRITEREPLY, \
READREQUEST, READREPLY, IDENTREQUEST, IDENTPREFIX, ENABLEEVENTSREQUEST, COMMANDREQUEST, \
DESCRIPTIONREQUEST, HEARTBEATREQUEST
import secop.errors import secop.errors
import secop.params import secop.params
from secop.datatypes import get_datatype
from secop.lib import mkthread
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.protocol.interface import decode_msg, encode_msg_frame
from secop.protocol.messages import COMMANDREQUEST, \
DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST, ERRORPREFIX, \
EVENTREPLY, HEARTBEATREQUEST, IDENTPREFIX, IDENTREQUEST, \
READREPLY, READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST
# replies to be handled for cache # replies to be handled for cache
UPDATE_MESSAGES = {EVENTREPLY, READREPLY, WRITEREPLY, ERRORPREFIX + READREQUEST, ERRORPREFIX + EVENTREPLY} UPDATE_MESSAGES = {EVENTREPLY, READREPLY, WRITEREPLY, ERRORPREFIX + READREQUEST, ERRORPREFIX + EVENTREPLY}
@ -160,7 +161,6 @@ class ProxyClient:
if not cblist: if not cblist:
self.callbacks[cbname].pop(key) self.callbacks[cbname].pop(key)
def callback(self, key, cbname, *args): def callback(self, key, cbname, *args):
"""perform callbacks """perform callbacks

View File

@ -1,584 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Define Client side proxies"""
import json
import queue
import socket
import threading
import time
from collections import OrderedDict
from select import select
try:
import mlzlog
except ImportError:
pass
import serial
from secop.datatypes import CommandType, EnumType, get_datatype
from secop.errors import EXCEPTIONS
from secop.lib import formatException, formatExtendedStack, mkthread
from secop.lib.parsing import format_time, parse_time
from secop.protocol.messages import BUFFERREQUEST, COMMANDREQUEST, \
DESCRIPTIONREPLY, DESCRIPTIONREQUEST, DISABLEEVENTSREQUEST, \
ENABLEEVENTSREQUEST, ERRORPREFIX, EVENTREPLY, \
HEARTBEATREQUEST, HELPREQUEST, IDENTREQUEST, READREPLY, \
READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST
class TCPConnection:
# disguise a TCP connection as serial one
def __init__(self, host, port, getLogger=None):
if getLogger:
self.log = getLogger('TCPConnection')
else:
self.log = mlzlog.getLogger('TCPConnection')
self._host = host
self._port = int(port)
self._thread = None
self.callbacks = [] # called if SEC-node shuts down
self._io = None
self.connect()
def connect(self):
self._readbuffer = queue.Queue(100)
time.sleep(1)
io = socket.create_connection((self._host, self._port))
io.setblocking(False)
self.stopflag = False
self._io = io
if self._thread and self._thread.is_alive():
return
self._thread = mkthread(self._run)
def _run(self):
try:
data = b''
while not self.stopflag:
rlist, _, xlist = select([self._io], [], [self._io], 1)
if xlist:
# on some strange systems, a closed connection is indicated by
# an exceptional condition instead of "read ready" + "empty recv"
newdata = b''
else:
if not rlist:
continue # check stopflag every second
# self._io is now ready to read some bytes
try:
newdata = self._io.recv(1024)
except socket.error as err:
if err.args[0] == socket.EAGAIN:
# if we receive an EAGAIN error, just continue
continue
newdata = b''
except Exception:
newdata = b''
if not newdata: # no data on recv indicates a closed connection
raise IOError('%s:%d disconnected' % (self._host, self._port))
lines = (data + newdata).split(b'\n')
for line in lines[:-1]: # last line is incomplete or empty
try:
self._readbuffer.put(line.strip(b'\r').decode('utf-8'),
block=True, timeout=1)
except queue.Full:
self.log.debug('rcv queue full! dropping line: %r' % line)
data = lines[-1]
except Exception as err:
self.log.error(err)
try:
self._io.shutdown(socket.SHUT_RDWR)
except socket.error:
pass
try:
self._io.close()
except socket.error:
pass
for cb, args in self.callbacks:
cb(*args)
def readline(self, timeout=None):
"""blocks until a full line was read and returns it
returns None when connection is stopped"""
if self.stopflag:
return None
return self._readbuffer.get(block=True, timeout=timeout)
def stop(self):
self.stopflag = True
self._readbuffer.put(None) # terminate pending readline
def readable(self):
return not self._readbuffer.empty()
def write(self, data):
if self._io is None:
self.connect()
self._io.sendall(data.encode('latin-1'))
def writeline(self, line):
self.write(line + '\n')
def writelines(self, *lines):
for line in lines:
self.writeline(line)
class Value:
t = None # pylint: disable = C0103
u = None
e = None
fmtstr = '%s'
def __init__(self, value, qualifiers=None):
self.value = value
if qualifiers:
self.__dict__.update(qualifiers)
if 't' in qualifiers:
try:
self.t = float(qualifiers['t'])
except Exception:
self.t = parse_time(qualifiers['t'])
def __repr__(self):
r = []
if self.t is not None:
r.append("timestamp=%r" % format_time(self.t))
if self.u is not None:
r.append('unit=%r' % self.u)
if self.e is not None:
r.append(('error=%s' % self.fmtstr) % self.e)
if r:
return (self.fmtstr + '(%s)') % (self.value, ', '.join(r))
return self.fmtstr % self.value
class Client:
secop_id = 'unknown'
describing_data = {}
stopflag = False
connection_established = False
def __init__(self, opts, autoconnect=True, getLogger=None):
if 'testing' not in opts:
if getLogger:
self.log = getLogger('client')
else:
self.log = mlzlog.getLogger('client', True)
else:
class logStub:
def info(self, *args):
pass
debug = info
error = info
warning = info
exception = info
self.log = logStub()
self._cache = dict()
if 'module' in opts:
# serial port
devport = opts.pop('module')
baudrate = int(opts.pop('baudrate', 115200))
self.contactPoint = "serial://%s:%s" % (devport, baudrate)
self.connection = serial.Serial(
devport, baudrate=baudrate, timeout=1)
self.connection.callbacks = []
elif 'testing' not in opts:
host = opts.pop('host', 'localhost')
port = int(opts.pop('port', 10767))
self.contactPoint = "tcp://%s:%d" % (host, port)
self.connection = TCPConnection(host, port, getLogger=getLogger)
else:
self.contactPoint = 'testing'
self.connection = opts.pop('testing')
# maps an expected reply to a list containing a single Event()
# upon rcv of that reply, entry is appended with False and
# the data of the reply.
# if an error is received, the entry is appended with True and an
# appropriate Exception.
# Then the Event is set.
self.expected_replies = {}
# maps spec to a set of callback functions (or single_shot callbacks)
self.callbacks = dict()
self.single_shots = dict()
# mapping the modulename to a dict mapping the parameter names to their values
# note: the module value is stored as the value of the parameter value
# of the module
self._syncLock = threading.RLock()
self._thread = threading.Thread(target=self._run)
self._thread.daemon = True
self._thread.start()
if autoconnect:
self.startup()
def _run(self):
while not self.stopflag:
try:
self._inner_run()
except Exception as err:
print(formatExtendedStack())
self.log.exception(err)
raise
def _inner_run(self):
data = ''
self.connection.writeline('*IDN?')
while not self.stopflag:
line = self.connection.readline()
if line is None: # connection stopped
break
self.connection_established = True
self.log.debug('got answer %r' % line)
if line.startswith(('SECoP', 'SINE2020&ISSE,SECoP')):
self.log.info('connected to: ' + line.strip())
self.secop_id = line
continue
msgtype, spec, data = self.decode_message(line)
if msgtype in (EVENTREPLY, READREPLY, WRITEREPLY):
# handle async stuff
self._handle_event(spec, data)
# handle sync stuff
self._handle_sync_reply(msgtype, spec, data)
def _handle_sync_reply(self, msgtype, spec, data):
# handle sync stuff
if msgtype.startswith(ERRORPREFIX):
# find originating msgtype and map to expected_reply_type
# errormessages carry to offending request as the first
# result in the resultist
request = msgtype[len(ERRORPREFIX):]
reply = REQUEST2REPLY.get(request, request)
entry = self.expected_replies.get((reply, spec), None)
if entry:
self.log.error("request %r resulted in Error %r" %
("%s %s" % (request, spec), (data[0], data[1])))
entry.extend([True, EXCEPTIONS[data[0]](*data[1:])])
entry[0].set()
return
self.log.error("got an unexpected %s %r" % (msgtype,data[0:1]))
self.log.error(repr(data))
return
if msgtype == DESCRIPTIONREPLY:
entry = self.expected_replies.get((msgtype, ''), None)
else:
entry = self.expected_replies.get((msgtype, spec), None)
if entry:
self.log.debug("got expected reply '%s %s'" % (msgtype, spec)
if spec else "got expected reply '%s'" % msgtype)
entry.extend([False, msgtype, spec, data])
entry[0].set()
def encode_message(self, requesttype, spec='', data=None):
"""encodes the given message to a string
"""
req = [str(requesttype)]
if spec:
req.append(str(spec))
if data is not None:
req.append(json.dumps(data))
req = ' '.join(req)
return req
def decode_message(self, msg):
"""return a decoded message triple"""
msg = msg.strip()
if ' ' not in msg:
return msg, '', None
msgtype, spec = msg.split(' ', 1)
data = None
if ' ' in spec:
spec, json_data = spec.split(' ', 1)
try:
data = json.loads(json_data)
except ValueError:
# keep as string
data = json_data
# print formatException()
return msgtype, spec, data
def _handle_event(self, spec, data):
"""handles event"""
# self.log.debug('handle_event %r %r' % (spec, data))
if ':' not in spec:
self.log.warning("deprecated specifier %r" % spec)
spec = '%s:value' % spec
modname, pname = spec.split(':', 1)
if data:
self._cache.setdefault(modname, {})[pname] = Value(*data)
else:
self.log.warning(
'got malformed answer! (%s,%s)' % (spec, data))
# self.log.info('cache: %s:%s=%r (was: %s)', modname, pname, data, previous)
if spec in self.callbacks:
for func in self.callbacks[spec]:
try:
mkthread(func, modname, pname, data)
except Exception as err:
self.log.exception('Exception in Callback!', err)
run = set()
if spec in self.single_shots:
for func in self.single_shots[spec]:
try:
mkthread(func, data)
except Exception as err:
self.log.exception('Exception in Single-shot Callback!',
err)
run.add(func)
self.single_shots[spec].difference_update(run)
def _getDescribingModuleData(self, module):
return self.describingModulesData[module]
def _getDescribingParameterData(self, module, parameter):
return self._getDescribingModuleData(module)['accessibles'][parameter]
def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102
# take a dict and move all keys which are not in specialkeys
# into a 'properties' subdict
# specialkeys entries are converted from list to ordereddict
try:
result = {}
for k in specialkeys:
result[k] = OrderedDict(data.pop(k, []))
result['properties'] = data
return result
except Exception as err:
raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data))
def _issueDescribe(self):
_, _, describing_data = self._communicate(DESCRIPTIONREQUEST)
try:
describing_data = self._decode_substruct(
['modules'], describing_data)
for modname, module in list(describing_data['modules'].items()):
# convert old namings of interface_classes
if 'interface_class' in module:
module['interface_classes'] = module.pop('interface_class')
elif 'interfaces' in module:
module['interface_classes'] = module.pop('interfaces')
describing_data['modules'][modname] = self._decode_substruct(
['accessibles'], module)
self.describing_data = describing_data
for module, moduleData in self.describing_data['modules'].items():
for aname, adata in moduleData['accessibles'].items():
datatype = get_datatype(adata.pop('datainfo'))
# *sigh* special handling for 'some' parameters....
if isinstance(datatype, EnumType):
datatype._enum.name = aname
if aname == 'status':
datatype.members[0]._enum.name = 'Status'
self.describing_data['modules'][module]['accessibles'] \
[aname]['datatype'] = datatype
except Exception as _exc:
print(formatException(verbose=True))
raise
def register_callback(self, module, parameter, cb):
self.log.debug('registering callback %r for %s:%s' %
(cb, module, parameter))
self.callbacks.setdefault('%s:%s' % (module, parameter), set()).add(cb)
def unregister_callback(self, module, parameter, cb):
self.log.debug('unregistering callback %r for %s:%s' %
(cb, module, parameter))
self.callbacks.setdefault('%s:%s' % (module, parameter),
set()).discard(cb)
def register_shutdown_callback(self, func, *args):
self.connection.callbacks.append((func, args))
def communicate(self, msgtype, spec='', data=None):
# only return the data portion....
return self._communicate(msgtype, spec, data)[2]
def _communicate(self, msgtype, spec='', data=None):
self.log.debug('communicate: %r %r %r' % (msgtype, spec, data))
if self.stopflag:
raise RuntimeError('alreading stopping!')
if msgtype == IDENTREQUEST:
return self.secop_id
# sanitize input
msgtype = str(msgtype)
spec = str(spec)
if msgtype not in (DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST,
DISABLEEVENTSREQUEST, COMMANDREQUEST,
WRITEREQUEST, BUFFERREQUEST,
READREQUEST, HEARTBEATREQUEST, HELPREQUEST):
raise EXCEPTIONS['Protocol'](args=[
self.encode_message(msgtype, spec, data),
dict(
errorclass='Protocol',
errorinfo='%r: No Such Messagetype defined!' % msgtype, ),
])
# handle syntactic sugar
if msgtype == WRITEREQUEST and ':' not in spec:
spec = spec + ':target'
if msgtype == READREQUEST and ':' not in spec:
spec = spec + ':value'
# check if such a request is already out
rply = REQUEST2REPLY[msgtype]
if (rply, spec) in self.expected_replies:
raise RuntimeError(
"can not have more than one requests of the same type at the same time!"
)
# prepare sending request
event = threading.Event()
self.expected_replies[(rply, spec)] = [event]
self.log.debug('prepared reception of %r msg' % rply)
# send request
msg = self.encode_message(msgtype, spec, data)
while not self.connection_established:
self.log.debug('connection not established yet, waiting ...')
time.sleep(0.1)
self.connection.writeline(msg)
self.log.debug('sent msg %r' % msg)
# wait for reply. timeout after 10s
if event.wait(10):
self.log.debug('checking reply')
entry = self.expected_replies.pop((rply, spec))
# entry is: event, is_error, exc_or_msgtype [,spec, date]<- if !err
is_error = entry[1]
if is_error:
# if error, entry[2] contains the rigth Exception to raise
raise entry[2]
# valid reply: entry[2:5] contain msgtype, spec, data
return tuple(entry[2:5])
# timed out
del self.expected_replies[(rply, spec)]
# XXX: raise a TimedOut ?
raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype)
def quit(self):
# after calling this the client is dysfunctional!
# self.communicate(DISABLEEVENTSREQUEST)
self.stopflag = True
self.connection.stop()
if self._thread and self._thread.is_alive():
self._thread.join(10)
def startup(self, _async=False):
self._issueDescribe()
# always fill our cache
self.communicate(ENABLEEVENTSREQUEST)
# deactivate updates if not wanted
if not _async:
self.communicate(DISABLEEVENTSREQUEST)
def queryCache(self, module, parameter=None):
result = self._cache.get(module, {})
if parameter is not None:
result = result[parameter]
return result
def getParameter(self, module, parameter):
return self.communicate(READREQUEST, '%s:%s' % (module, parameter))
def setParameter(self, module, parameter, value):
datatype = self._getDescribingParameterData(module,
parameter)['datatype']
value = datatype.from_string(value)
value = datatype.export_value(value)
self.communicate(WRITEREQUEST, '%s:%s' % (module, parameter), value)
@property
def describingData(self):
return self.describing_data
@property
def describingModulesData(self):
return self.describingData['modules']
@property
def equipmentId(self):
if self.describingData:
return self.describingData['properties']['equipment_id']
return 'Undetermined'
@property
def protocolVersion(self):
return self.secop_id
@property
def modules(self):
return list(self.describing_data['modules'].keys())
def getParameters(self, module):
params = filter(lambda item: not isinstance(item[1]['datatype'], CommandType),
self.describing_data['modules'][module]['accessibles'].items())
return list(param[0] for param in params)
def getModuleProperties(self, module):
return self.describing_data['modules'][module]['properties']
def getModuleBaseClass(self, module):
return self.getModuleProperties(module)['interface_classes']
def getCommands(self, module):
cmds = filter(lambda item: isinstance(item[1]['datatype'], CommandType),
self.describing_data['modules'][module]['accessibles'].items())
return OrderedDict(cmds)
def execCommand(self, module, command, args):
# ignore reply message + reply specifier, only return data
return self._communicate(COMMANDREQUEST, '%s:%s' % (module, command), list(args) if args else None)[2]
def getProperties(self, module, parameter):
return self.describing_data['modules'][module]['accessibles'][parameter]
def syncCommunicate(self, *msg):
res = self._communicate(*msg) # pylint: disable=E1120
try:
res = self.encode_message(*res)
except Exception:
res = str(res)
return res
def ping(self, pingctr=[0]): # pylint: disable=W0102
pingctr[0] = pingctr[0] + 1
self.communicate(HEARTBEATREQUEST, pingctr[0])

View File

@ -1,193 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""console client"""
# this needs to be reworked or removed
import code
import socket
import threading
from collections import deque
from os import path
import configparser
import mlzlog
from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
from secop.protocol.messages import EVENTREPLY
class NameSpace(dict):
def __init__(self):
dict.__init__(self)
self.__const = set()
def setconst(self, name, value):
dict.__setitem__(self, name, value)
self.__const.add(name)
def __setitem__(self, name, value):
if name in self.__const:
raise RuntimeError('%s cannot be assigned' % name)
dict.__setitem__(self, name, value)
def __delitem__(self, name):
if name in self.__const:
raise RuntimeError('%s cannot be deleted' % name)
dict.__delitem__(self, name)
def getClientOpts(cfgfile):
parser = configparser.SafeConfigParser()
if not parser.read([cfgfile + '.cfg']):
print("Error reading cfg file %r" % cfgfile)
return {}
if not parser.has_section('client'):
print("No Server section found!")
return dict(item for item in parser.items('client'))
class ClientConsole:
def __init__(self, cfgname, basepath):
self.namespace = NameSpace()
self.namespace.setconst('help', self.helpCmd)
cfgfile = path.join(basepath, 'etc', cfgname)
cfg = getClientOpts(cfgfile)
self.client = Client(cfg)
self.client.populateNamespace(self.namespace)
def run(self):
console = code.InteractiveConsole(self.namespace)
console.interact("Welcome to the SECoP console")
def close(self):
pass
def helpCmd(self, arg=Ellipsis):
if arg is Ellipsis:
print("No help available yet")
else:
help(arg)
class TCPConnection:
def __init__(self, connect, port, **kwds):
self.log = mlzlog.log.getChild('connection', False)
port = int(port)
self.connection = socket.create_connection((connect, port), 3)
self.queue = deque()
self._rcvdata = ''
self.callbacks = set()
self._thread = threading.Thread(target=self.thread)
self._thread.daemonize = True
self._thread.start()
def send(self, msg):
self.log.debug("Sending msg %r" % msg)
data = encode_msg_frame(*msg.serialize())
self.log.debug("raw data: %r" % data)
self.connection.sendall(data)
def thread(self):
while True:
try:
self.thread_step()
except Exception as e:
self.log.exception("Exception in RCV thread: %r" % e)
def thread_step(self):
data = b''
while True:
newdata = self.connection.recv(1024)
self.log.debug("RCV: got raw data %r" % newdata)
data = data + newdata
while True:
origin, data = get_msg(data)
if origin is None:
break # no more messages to process
if not origin: # empty string
continue # ???
_ = decode_msg(origin)
# construct msgObj from msg
try:
#msgObj = Message(*msg)
#msgObj.origin = origin.decode('latin-1')
#self.handle(msgObj)
pass
except Exception:
# ??? what to do here?
pass
def handle(self, msg):
if msg.action == EVENTREPLY:
self.log.info("got Async: %r" % msg)
for cb in self.callbacks:
try:
cb(msg)
except Exception as e:
self.log.debug(
"handle_async: got exception %r" % e, exception=True)
else:
self.queue.append(msg)
def read(self):
while not self.queue:
pass # XXX: remove BUSY polling
return self.queue.popleft()
def register_callback(self, callback):
"""registers callback for async data"""
self.callbacks.add(callback)
def unregister_callback(self, callback):
"""unregisters callback for async data"""
self.callbacks.discard(callback)
class Client:
def __init__(self, opts):
self.log = mlzlog.log.getChild('client', True)
self._cache = dict()
self.connection = TCPConnection(**opts)
self.connection.register_callback(self.handle_async)
def handle_async(self, msg):
self.log.info("Got async update %r" % msg)
module = msg.module
param = msg.param
value = msg.value
self._cache.getdefault(module, {})[param] = value
# XXX: further notification-callbacks needed ???
def populateNamespace(self, namespace):
#self.connection.send(Message(DESCRIPTIONREQUEST))
# reply = self.connection.read()
# self.log.info("found modules %r" % reply)
# create proxies, populate cache....
namespace.setconst('connection', self.connection)

View File

@ -26,14 +26,14 @@
# allow to import the most important classes from 'secop' # allow to import the most important classes from 'secop'
# pylint: disable=unused-import # pylint: disable=unused-import
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf FloatRange, IntRange, ScaledInteger, StringType, StructOf, TupleOf
from secop.lib.enum import Enum
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
from secop.properties import Property
from secop.params import Parameter, Command, Override
from secop.metaclass import Done
from secop.iohandler import IOHandler, IOHandlerBase from secop.iohandler import IOHandler, IOHandlerBase
from secop.stringio import StringIO, HasIodev from secop.lib.enum import Enum
from secop.proxy import SecNode, Proxy, proxy_class from secop.modules import Attached, Communicator, \
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC Done, Drivable, Module, Readable, Writable
from secop.params import Command, Parameter
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class
from secop.stringio import HasIodev, StringIO

View File

@ -28,13 +28,13 @@
import sys import sys
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from secop.errors import ProgrammingError, ProtocolError, BadValueError, ConfigError from secop.errors import BadValueError, \
ConfigError, ProgrammingError, ProtocolError
from secop.lib import clamp from secop.lib import clamp
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.parse import Parser from secop.parse import Parser
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
# Only export these classes for 'from secop.datatypes import *' # Only export these classes for 'from secop.datatypes import *'
__all__ = [ __all__ = [
'DataType', 'get_datatype', 'DataType', 'get_datatype',
@ -53,6 +53,7 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
Parser = Parser() Parser = Parser()
# base class for all DataTypes
class DataType(HasProperties): class DataType(HasProperties):
"""base class for all data types""" """base class for all data types"""
IS_COMMAND = False IS_COMMAND = False
@ -97,7 +98,7 @@ class DataType(HasProperties):
def set_properties(self, **kwds): def set_properties(self, **kwds):
"""init datatype properties""" """init datatype properties"""
try: try:
for k,v in kwds.items(): for k, v in kwds.items():
self.setProperty(k, v) self.setProperty(k, v)
self.checkProperties() self.checkProperties()
except Exception as e: except Exception as e:
@ -126,10 +127,6 @@ class DataType(HasProperties):
""" """
raise NotImplementedError raise NotImplementedError
def short_doc(self):
"""short description for automatic extension of doc strings"""
return None
class Stub(DataType): class Stub(DataType):
"""incomplete datatype, to be replaced with a proper one later during module load """incomplete datatype, to be replaced with a proper one later during module load
@ -154,42 +151,35 @@ class Stub(DataType):
""" """
for dtcls in globals().values(): for dtcls in globals().values():
if isinstance(dtcls, type) and issubclass(dtcls, DataType): if isinstance(dtcls, type) and issubclass(dtcls, DataType):
for prop in dtcls.properties.values(): for prop in dtcls.propertyDict.values():
stub = prop.datatype stub = prop.datatype
if isinstance(stub, cls): if isinstance(stub, cls):
prop.datatype = globals()[stub.name](*stub.args) prop.datatype = globals()[stub.name](*stub.args)
def short_doc(self):
return self.name.replace('Type', '').replace('Range', '').lower()
# SECoP types: # SECoP types:
class FloatRange(DataType): class FloatRange(DataType):
"""(restricted) float type """(restricted) float type
:param minval: (property **min**) :param minval: (property **min**)
:param maxval: (property **max**) :param maxval: (property **max**)
:param properties: any of the properties below :param kwds: any of the properties below
""" """
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
extname='absolute_resolution', default=0.0)
relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
extname='relative_resolution', default=1.2e-7)
properties = { def __init__(self, minval=None, maxval=None, **kwds):
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0),
extname='absolute_resolution', default=0.0),
'relative_resolution': Property('relative resolution', Stub('FloatRange', 0),
extname='relative_resolution', default=1.2e-7),
}
def __init__(self, minval=None, maxval=None, **properties):
super().__init__() super().__init__()
properties['min'] = minval if minval is not None else -sys.float_info.max kwds['min'] = minval if minval is not None else -sys.float_info.max
properties['max'] = maxval if maxval is not None else sys.float_info.max kwds['max'] = maxval if maxval is not None else sys.float_info.max
self.set_properties(**properties) self.set_properties(**kwds)
def checkProperties(self): def checkProperties(self):
self.default = 0 if self.min <= 0 <= self.max else self.min self.default = 0 if self.min <= 0 <= self.max else self.min
@ -213,7 +203,7 @@ class FloatRange(DataType):
if self.min - prec <= value <= self.max + prec: if self.min - prec <= value <= self.max + prec:
return min(max(value, self.min), self.max) return min(max(value, self.min), self.max)
raise BadValueError('%.14g should be a float between %.14g and %.14g' % raise BadValueError('%.14g should be a float between %.14g and %.14g' %
(value, self.min, self.max)) (value, self.min, self.max))
def __repr__(self): def __repr__(self):
hints = self.get_info() hints = self.get_info()
@ -221,7 +211,7 @@ class FloatRange(DataType):
hints['minval'] = hints.pop('min') hints['minval'] = hints.pop('min')
if 'max' in hints: if 'max' in hints:
hints['maxval'] = hints.pop('max') hints['maxval'] = hints.pop('max')
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k,v) for k,v in hints.items())) return 'FloatRange(%s)' % (', '.join('%s=%r' % (k, v) for k, v in hints.items()))
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -249,9 +239,6 @@ class FloatRange(DataType):
other(max(sys.float_info.min, self.min)) other(max(sys.float_info.min, self.min))
other(min(sys.float_info.max, self.max)) other(min(sys.float_info.max, self.max))
def short_doc(self):
return 'float'
class IntRange(DataType): class IntRange(DataType):
"""restricted int type """restricted int type
@ -259,12 +246,10 @@ class IntRange(DataType):
:param minval: (property **min**) :param minval: (property **min**)
:param maxval: (property **max**) :param maxval: (property **max**)
""" """
properties = { min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True)
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True), max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True)
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True), # a unit on an int is now allowed in SECoP, but do we need them in Frappy?
# a unit on an int is now allowed in SECoP, but do we need them in Frappy? # unit = Property('physical unit', StringType(), extname='unit', default='')
# 'unit': Property('physical unit', StringType(), extname='unit', default=''),
}
def __init__(self, minval=None, maxval=None): def __init__(self, minval=None, maxval=None):
super().__init__() super().__init__()
@ -290,7 +275,12 @@ class IntRange(DataType):
raise BadValueError('Can not convert %r to int' % value) raise BadValueError('Can not convert %r to int' % value)
def __repr__(self): def __repr__(self):
return 'IntRange(%d, %d)' % (self.min, self.max) args = (self.min, self.max)
if args[1] == DEFAULT_MAX_INT:
args = args[:1]
if args[0] == DEFAULT_MIN_INT:
args = ()
return 'IntRange%s' % repr(args)
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -316,48 +306,38 @@ class IntRange(DataType):
for i in range(self.min, self.max + 1): for i in range(self.min, self.max + 1):
other(i) other(i)
def short_doc(self):
return 'int'
class ScaledInteger(DataType): class ScaledInteger(DataType):
"""scaled integer (= fixed resolution float) type """scaled integer (= fixed resolution float) type
| In general *ScaledInteger* is needed only in special cases,
e.g. when the a SEC node is running on very limited hardware
without floating point support.
| Please use *FloatRange* instead.
:param minval: (property **min**) :param minval: (property **min**)
:param maxval: (property **max**) :param maxval: (property **max**)
:param properties: any of the properties below :param kwds: any of the properties below
{properties} note: limits are for the scaled float value
:note: - limits are for the scaled float value the scale is only used for calculating to/from transport serialisation
- the scale is only used for calculating to/from transport serialisation
""" """
properties = { scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True), min = Property('low limit', FloatRange(), extname='min', mandatory=True)
'min': Property('low limit', FloatRange(), extname='min', mandatory=True), max = Property('high limit', FloatRange(), extname='max', mandatory=True)
'max': Property('high limit', FloatRange(), extname='max', mandatory=True), unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''), fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'), absolute_resolution = Property('absolute resolution', FloatRange(0),
'absolute_resolution': Property('absolute resolution', FloatRange(0), extname='absolute_resolution', default=0.0)
extname='absolute_resolution', default=0.0), relative_resolution = Property('relative resolution', FloatRange(0),
'relative_resolution': Property('relative resolution', FloatRange(0), extname='relative_resolution', default=1.2e-7)
extname='relative_resolution', default=1.2e-7),
}
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **properties): def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
super().__init__() super().__init__()
scale = float(scale) scale = float(scale)
if absolute_resolution is None: if absolute_resolution is None:
absolute_resolution = scale absolute_resolution = scale
self.set_properties(scale=scale, self.set_properties(
scale=scale,
min=DEFAULT_MIN_INT * scale if minval is None else float(minval), min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval), max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
absolute_resolution=absolute_resolution, absolute_resolution=absolute_resolution,
**properties) **kwds)
def checkProperties(self): def checkProperties(self):
self.default = 0 if self.min <= 0 <= self.max else self.min self.default = 0 if self.min <= 0 <= self.max else self.min
@ -384,8 +364,8 @@ class ScaledInteger(DataType):
def export_datatype(self): def export_datatype(self):
return self.get_info(type='scaled', return self.get_info(type='scaled',
min = int((self.min + self.scale * 0.5) // self.scale), min=int((self.min + self.scale * 0.5) // self.scale),
max = int((self.max + self.scale * 0.5) // self.scale)) max=int((self.max + self.scale * 0.5) // self.scale))
def __call__(self, value): def __call__(self, value):
try: try:
@ -398,15 +378,15 @@ class ScaledInteger(DataType):
value = min(max(value, self.min), self.max) value = min(max(value, self.min), self.max)
else: else:
raise BadValueError('%g should be a float between %g and %g' % raise BadValueError('%g should be a float between %g and %g' %
(value, self.min, self.max)) (value, self.min, self.max))
intval = int((value + self.scale * 0.5) // self.scale) intval = int((value + self.scale * 0.5) // self.scale)
value = float(intval * self.scale) value = float(intval * self.scale)
return value # return 'actual' value (which is more discrete than a float) return value # return 'actual' value (which is more discrete than a float)
def __repr__(self): def __repr__(self):
hints = self.get_info(scale=float('%g' % self.scale), hints = self.get_info(scale=float('%g' % self.scale),
min = int((self.min + self.scale * 0.5) // self.scale), min=int((self.min + self.scale * 0.5) // self.scale),
max = int((self.max + self.scale * 0.5) // self.scale)) max=int((self.max + self.scale * 0.5) // self.scale))
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items())) return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
def export_value(self, value): def export_value(self, value):
@ -435,25 +415,19 @@ class ScaledInteger(DataType):
other(self.min) other(self.min)
other(self.max) other(self.max)
def short_doc(self):
return 'float'
class EnumType(DataType): class EnumType(DataType):
"""enumeration """enumeration
:param enum_or_name: the name of the Enum or an Enum to inherit from :param enum_or_name: the name of the Enum or an Enum to inherit from
:param members: each argument denotes <member name>=<member int value> :param members: members dict or None when using kwds only
:param kwds: (additional) members
exception: use members=<member dict> to add members from a dict
""" """
def __init__(self, enum_or_name='', **members): def __init__(self, enum_or_name='', *, members=None, **kwds):
super().__init__() super().__init__()
if 'members' in members: if members is not None:
members = dict(members) kwds.update(members)
members.update(members['members']) self._enum = Enum(enum_or_name, **kwds)
members.pop('members')
self._enum = Enum(enum_or_name, **members)
self.default = self._enum[self._enum.members[0]] self.default = self._enum[self._enum.members[0]]
def copy(self): def copy(self):
@ -461,10 +435,11 @@ class EnumType(DataType):
return EnumType(self._enum) return EnumType(self._enum)
def export_datatype(self): def export_datatype(self):
return {'type': 'enum', 'members':dict((m.name, m.value) for m in self._enum.members)} return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)}
def __repr__(self): def __repr__(self):
return "EnumType(%r, %s)" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members)) return "EnumType(%r, %s)" % (self._enum.name,
', '.join('%s=%d' % (m.name, m.value) for m in self._enum.members))
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -478,7 +453,7 @@ class EnumType(DataType):
"""return the validated (internal) value or raise""" """return the validated (internal) value or raise"""
try: try:
return self._enum[value] return self._enum[value]
except (KeyError, TypeError): # TypeError will be raised when value is not hashable except (KeyError, TypeError): # TypeError will be raised when value is not hashable
raise BadValueError('%r is not a member of enum %r' % (value, self._enum)) raise BadValueError('%r is not a member of enum %r' % (value, self._enum))
def from_string(self, text): def from_string(self, text):
@ -487,25 +462,24 @@ class EnumType(DataType):
def format_value(self, value, unit=None): def format_value(self, value, unit=None):
return '%s<%s>' % (self._enum[value].name, self._enum[value].value) return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
def set_name(self, name):
self._enum.name = name
def compatible(self, other): def compatible(self, other):
for m in self._enum.members: for m in self._enum.members:
other(m) other(m)
def short_doc(self):
return 'one of %s' % str(tuple(self._enum.keys()))
class BLOBType(DataType): class BLOBType(DataType):
"""binary large object """binary large object
internally treated as bytes internally treated as bytes
""" """
properties = {
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes', minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
default=0), default=0)
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes', maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
mandatory=True), mandatory=True)
}
def __init__(self, minbytes=0, maxbytes=None): def __init__(self, minbytes=0, maxbytes=None):
super().__init__() super().__init__()
@ -565,21 +539,20 @@ class BLOBType(DataType):
class StringType(DataType): class StringType(DataType):
"""string """string
for parameters see properties below
""" """
properties = { minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED), extname='minchars', default=0)
extname='minchars', default=0), maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED), extname='maxchars', default=UNLIMITED)
extname='maxchars', default=UNLIMITED), isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII', Stub('BoolType'), extname='isUTF8', default=False)
Stub('BoolType'), extname='isUTF8', default=False),
}
def __init__(self, minchars=0, maxchars=None, isUTF8=False): def __init__(self, minchars=0, maxchars=None, **kwds):
super().__init__() super().__init__()
if maxchars is None: if maxchars is None:
maxchars = minchars or UNLIMITED maxchars = minchars or UNLIMITED
self.set_properties(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8) self.set_properties(minchars=minchars, maxchars=maxchars, **kwds)
def checkProperties(self): def checkProperties(self):
self.default = ' ' * self.minchars self.default = ' ' * self.minchars
@ -635,24 +608,13 @@ class StringType(DataType):
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'str'
# TextType is a special StringType intended for longer texts (i.e. embedding \n), # TextType is a special StringType intended for longer texts (i.e. embedding \n),
# whereas StringType is supposed to not contain '\n' # whereas StringType is supposed to not contain '\n'
# unfortunately, SECoP makes no distinction here.... # unfortunately, SECoP makes no distinction here....
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation # note: content is supposed to follow the format of a git commit message,
# i.e. a line of text, 2 '\n' + a longer explanation
class TextType(StringType): class TextType(StringType):
"""special string type, intended for longer texts
:param maxchars: maximum number of characters
whereas StringType is supposed to not contain '\n'
unfortunately, SECoP makes no distinction here....
note: content is supposed to follow the format of a git commit message,
i.e. a line of text, 2 '\n' + a longer explanation
"""
def __init__(self, maxchars=None): def __init__(self, maxchars=None):
if maxchars is None: if maxchars is None:
maxchars = UNLIMITED maxchars = UNLIMITED
@ -661,7 +623,7 @@ class TextType(StringType):
def __repr__(self): def __repr__(self):
if self.maxchars == UNLIMITED: if self.maxchars == UNLIMITED:
return 'TextType()' return 'TextType()'
return 'TextType(%d)' % (self.maxchars) return 'TextType(%d)' % self.maxchars
def copy(self): def copy(self):
# DataType.copy will not work, because it is exported as 'string' # DataType.copy will not work, because it is exported as 'string'
@ -669,9 +631,7 @@ class TextType(StringType):
class BoolType(DataType): class BoolType(DataType):
"""boolean """boolean"""
"""
default = False default = False
def export_datatype(self): def export_datatype(self):
@ -707,9 +667,6 @@ class BoolType(DataType):
other(False) other(False)
other(True) other(True)
def short_doc(self):
return 'bool'
Stub.fix_datatypes() Stub.fix_datatypes()
@ -721,14 +678,12 @@ Stub.fix_datatypes()
class ArrayOf(DataType): class ArrayOf(DataType):
"""data structure with fields of homogeneous type """data structure with fields of homogeneous type
:param members: the datatype for all elements :param members: the datatype of the elements
""" """
properties = { minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen', default=0)
default=0), maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen', mandatory=True)
mandatory=True),
}
def __init__(self, members, minlen=0, maxlen=None): def __init__(self, members, minlen=0, maxlen=None):
super().__init__() super().__init__()
@ -759,14 +714,14 @@ class ArrayOf(DataType):
def setProperty(self, key, value): def setProperty(self, key, value):
"""set also properties of members""" """set also properties of members"""
if key in self.__class__.properties: if key in self.propertyDict:
super().setProperty(key, value) super().setProperty(key, value)
else: else:
self.members.setProperty(key, value) self.members.setProperty(key, value)
def export_datatype(self): def export_datatype(self):
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen, return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
members=self.members.export_datatype()) members=self.members.export_datatype())
def __repr__(self): def __repr__(self):
return 'ArrayOf(%s, %s, %s)' % ( return 'ArrayOf(%s, %s, %s)' % (
@ -818,16 +773,12 @@ class ArrayOf(DataType):
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'array of %s' % self.members.short_doc()
class TupleOf(DataType): class TupleOf(DataType):
"""data structure with fields of inhomogeneous type """data structure with fields of inhomogeneous type
:param members: each argument is a datatype of an element types are given as positional arguments
""" """
def __init__(self, *members): def __init__(self, *members):
super().__init__() super().__init__()
if not members: if not members:
@ -855,11 +806,10 @@ class TupleOf(DataType):
try: try:
if len(value) != len(self.members): if len(value) != len(self.members):
raise BadValueError( raise BadValueError(
'Illegal number of Arguments! Need %d arguments.' % 'Illegal number of Arguments! Need %d arguments.' % len(self.members))
(len(self.members)))
# validate elements and return as list # validate elements and return as list
return tuple(sub(elem) return tuple(sub(elem)
for sub, elem in zip(self.members, value)) for sub, elem in zip(self.members, value))
except Exception as exc: except Exception as exc:
raise BadValueError('Can not validate:', str(exc)) raise BadValueError('Can not validate:', str(exc))
@ -879,19 +829,16 @@ class TupleOf(DataType):
def format_value(self, value, unit=None): def format_value(self, value, unit=None):
return '(%s)' % (', '.join([sub.format_value(elem) return '(%s)' % (', '.join([sub.format_value(elem)
for sub, elem in zip(self.members, value)])) for sub, elem in zip(self.members, value)]))
def compatible(self, other): def compatible(self, other):
if not isinstance(other, TupleOf): if not isinstance(other, TupleOf):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
if len(self.members) != len(other.members) : if len(self.members) != len(other.members):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
for a, b in zip(self.members, other.members): for a, b in zip(self.members, other.members):
a.compatible(b) a.compatible(b)
def short_doc(self):
return 'tuple of (%s)' % ', '.join(m.short_doc() for m in self.members)
class ImmutableDict(dict): class ImmutableDict(dict):
def _no(self, *args, **kwds): def _no(self, *args, **kwds):
@ -902,8 +849,8 @@ class ImmutableDict(dict):
class StructOf(DataType): class StructOf(DataType):
"""data structure with named fields """data structure with named fields
:param optional: (*sequence*) optional members :param optional: a list of optional members
:param members: each argument denotes <member name>=<member data type> :param members: names as keys and types as values for all members
""" """
def __init__(self, optional=None, **members): def __init__(self, optional=None, **members):
super().__init__() super().__init__()
@ -919,15 +866,15 @@ class StructOf(DataType):
if name not in members: if name not in members:
raise ProgrammingError( raise ProgrammingError(
'Only members of StructOf may be declared as optional!') 'Only members of StructOf may be declared as optional!')
self.default = dict((k,el.default) for k, el in members.items()) self.default = dict((k, el.default) for k, el in members.items())
def copy(self): def copy(self):
"""DataType.copy does not work when members contain enums""" """DataType.copy does not work when members contain enums"""
return StructOf(self.optional, **{k: v.copy() for k,v in self.members.items()}) return StructOf(self.optional, **{k: v.copy() for k, v in self.members.items()})
def export_datatype(self): def export_datatype(self):
res = dict(type='struct', members=dict((n, s.export_datatype()) res = dict(type='struct', members=dict((n, s.export_datatype())
for n, s in list(self.members.items()))) for n, s in list(self.members.items())))
if self.optional: if self.optional:
res['optional'] = self.optional res['optional'] = self.optional
return res return res
@ -979,18 +926,11 @@ class StructOf(DataType):
except (AttributeError, TypeError, KeyError): except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
def short_doc(self):
return 'dict'
class CommandType(DataType): class CommandType(DataType):
"""command """command
a pseudo datatype for commands with arguments and return values a pseudo datatype for commands with arguments and return values
:param argument: None or the data type of the argument. multiple arguments may be simulated
by TupleOf or StructOf
:param result: None or the data type of the result
""" """
IS_COMMAND = True IS_COMMAND = True
@ -1049,16 +989,10 @@ class CommandType(DataType):
except AttributeError: except AttributeError:
raise BadValueError('incompatible datatypes') raise BadValueError('incompatible datatypes')
def short_doc(self):
argument = self.argument.short_doc() if self.argument else ''
result = ' -> %s' % self.argument.short_doc() if self.result else ''
return '(%s)%s' % (argument, result) # return argument list only
# internally used datatypes (i.e. only for programming the SEC-node) # internally used datatypes (i.e. only for programming the SEC-node)
class DataTypeType(DataType): class DataTypeType(DataType):
"""DataType type"""
def __call__(self, value): def __call__(self, value):
"""check if given value (a python obj) is a valid datatype """check if given value (a python obj) is a valid datatype
@ -1102,9 +1036,7 @@ class ValueType(DataType):
class NoneOr(DataType): class NoneOr(DataType):
"""validates a None or other """validates a None or smth. else"""
:param other: the other datatype"""
default = None default = None
def __init__(self, other): def __init__(self, other):
@ -1119,16 +1051,8 @@ class NoneOr(DataType):
return None return None
return self.other.export_value(value) return self.other.export_value(value)
def short_doc(self):
other = self.other.short_doc()
return '%s or None' % other if other else None
class OrType(DataType): class OrType(DataType):
"""validates one of the
:param types: each argument denotes one allowed type
"""
def __init__(self, *types): def __init__(self, *types):
super().__init__() super().__init__()
self.types = types self.types = types
@ -1142,12 +1066,6 @@ class OrType(DataType):
pass pass
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types)))) raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
def short_doc(self):
types = [t.short_doc() for t in self.types]
if None in types:
return None
return ' or '.join(types)
Int8 = IntRange(-(1 << 7), (1 << 7) - 1) Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
Int16 = IntRange(-(1 << 15), (1 << 15) - 1) Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
@ -1161,12 +1079,6 @@ UInt64 = IntRange(0, (1 << 64) - 1)
# Goodie: Convenience Datatypes for Programming # Goodie: Convenience Datatypes for Programming
class LimitsType(TupleOf): class LimitsType(TupleOf):
"""limit (min, max) tuple
:param members: the type of both members
checks for min <= max
"""
def __init__(self, members): def __init__(self, members):
TupleOf.__init__(self, members, members) TupleOf.__init__(self, members, members)
@ -1178,22 +1090,13 @@ class LimitsType(TupleOf):
class StatusType(TupleOf): class StatusType(TupleOf):
"""SECoP status type # shorten initialisation and allow access to status enumMembers from status values
:param enum: the status code enum type
allows to access enum members directly
"""
def __init__(self, enum): def __init__(self, enum):
TupleOf.__init__(self, EnumType(enum), StringType()) TupleOf.__init__(self, EnumType(enum), StringType())
self.enum = enum self._enum = enum
def __getattr__(self, key): def __getattr__(self, key):
enum = TupleOf.__getattr__(self, 'enum') return getattr(self._enum, key)
if hasattr(enum, key):
return getattr(enum, key)
return TupleOf.__getattr__(self, key)
def floatargs(kwds): def floatargs(kwds):

View File

@ -22,7 +22,6 @@
"""Define (internal) SECoP Errors""" """Define (internal) SECoP Errors"""
class SECoPError(RuntimeError): class SECoPError(RuntimeError):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
@ -138,12 +137,6 @@ def secop_error(exception):
return InternalError(repr(exception)) return InternalError(repr(exception))
def fmt_error(exception):
if isinstance(exception, SECoPError):
return str(exception)
return repr(exception)
EXCEPTIONS = dict( EXCEPTIONS = dict(
NoSuchModule=NoSuchModuleError, NoSuchModule=NoSuchModuleError,
NoSuchParameter=NoSuchParameterError, NoSuchParameter=NoSuchParameterError,

View File

@ -24,11 +24,10 @@
from secop.datatypes import ArrayOf, BoolType, EnumType, \ from secop.datatypes import ArrayOf, BoolType, EnumType, \
FloatRange, StringType, StructOf, TupleOf FloatRange, StringType, StructOf, TupleOf
from secop.metaclass import ModuleMeta from secop.modules import Command, HasAccessibles, Parameter
from secop.modules import Command, Parameter
class Feature(metaclass=ModuleMeta): class Feature(HasAccessibles):
"""all things belonging to a small, predefined functionality influencing the working of a module""" """all things belonging to a small, predefined functionality influencing the working of a module"""
@ -39,33 +38,37 @@ class HAS_PID(Feature):
# note: (i would still but them in the same group, though) # note: (i would still but them in the same group, though)
# note: if extra elements are implemented in the pid struct they MUST BE # note: if extra elements are implemented in the pid struct they MUST BE
# properly described in the description of the pid Parameter # properly described in the description of the pid Parameter
parameters = {
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ), # parameters
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ), use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True), # pylint: disable=invalid-name
'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True), p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True), i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
'pid': Parameter('(optional) Struct of p,i,d, minimum output value', d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True)
datatype=StructOf(p=FloatRange(0), base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True)
i=FloatRange(0), pid = Parameter('(optional) Struct of p,i,d, minimum output value',
d=FloatRange(0), datatype=StructOf(p=FloatRange(0),
base_output=FloatRange(0), i=FloatRange(0),
), optional=True, d=FloatRange(0),
), # note: struct may be extended with custom elements (names should be prefixed with '_') base_output=FloatRange(0),
'output' : Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False), ), optional=True,
} ) # note: struct may be extended with custom elements (names should be prefixed with '_')
output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False)
class Has_PIDTable(HAS_PID): class Has_PIDTable(HAS_PID):
parameters = {
'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)), # parameters
'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0), use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1))
StructOf(p=FloatRange(0), pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
i=FloatRange(0), StructOf(p=FloatRange(0),
d=FloatRange(0), i=FloatRange(0),
_heater_range=FloatRange(0), d=FloatRange(0),
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange' _heater_range=FloatRange(0),
} _base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
class HAS_Persistent(Feature): class HAS_Persistent(Feature):
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
# 'coupled' : Status.BUSY+2, # to be discussed. # 'coupled' : Status.BUSY+2, # to be discussed.
# 'decoupling' : Status.BUSY+3, # to be discussed. # 'decoupling' : Status.BUSY+3, # to be discussed.
#} #}
parameters = {
'persistent_mode': Parameter('Use persistent mode', # parameters
datatype=EnumType(off=0,on=1), persistent_mode = Parameter('Use persistent mode',
default=0, readonly=False), datatype=EnumType(off=0,on=1),
'is_persistent': Parameter('current state of persistence', default=0, readonly=False)
datatype=BoolType(), optional=True), is_persistent = Parameter('current state of persistence',
'stored_value': Parameter('current persistence value, often used as the modules value', datatype=BoolType(), optional=True)
datatype='main', unit='$', optional=True), stored_value = Parameter('current persistence value, often used as the modules value',
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)', datatype='main', unit='$', optional=True)
datatype='main', unit='$' ), driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
} datatype='main', unit='$' )
class HAS_Tolerance(Feature): class HAS_Tolerance(Feature):
# detects IDLE status by checking if the value lies in a given window: # detects IDLE status by checking if the value lies in a given window:
# tolerance is the maximum allowed deviation from target, value must lie in this interval # tolerance is the maximum allowed deviation from target, value must lie in this interval
# for at least ´timewindow´ seconds. # for at least ´timewindow´ seconds.
parameters = {
'tolerance': Parameter('Half height of the Window', # parameters
datatype=FloatRange(0), default=1, unit='$'), tolerance = Parameter('Half height of the Window',
'timewindow': Parameter('Length of the timewindow to check', datatype=FloatRange(0), default=1, unit='$')
datatype=FloatRange(0), default=30, unit='s', timewindow = Parameter('Length of the timewindow to check',
optional=True), datatype=FloatRange(0), default=30, unit='s',
} optional=True)
class HAS_Timeout(Feature): class HAS_Timeout(Feature):
parameters = {
'timeout': Parameter('timeout for movement', # parameters
datatype=FloatRange(0), default=0, unit='s'), timeout = Parameter('timeout for movement',
} datatype=FloatRange(0), default=0, unit='s')
class HAS_Pause(Feature): class HAS_Pause(Feature):
# just a proposal, can't agree on it.... # just a proposal, can't agree on it....
parameters = {
'pause': Command('pauses movement', argument=None, result=None), @Command(argument=None, result=None)
'go': Command('continues movement or start a new one if target was change since the last pause', def pause(self):
argument=None, result=None), """pauses movement"""
}
@Command(argument=None, result=None)
def go(self):
"""continues movement or start a new one if target was change since the last pause"""
class HAS_Ramp(Feature): class HAS_Ramp(Feature):
parameters = {
'ramp': Parameter('speed of movement', unit='$/min', # parameters
datatype=FloatRange(0)), ramp =Parameter('speed of movement', unit='$/min',
'use_ramp': Parameter('use the ramping of the setpoint, or jump', datatype=FloatRange(0))
datatype=EnumType(disable_ramp=0, use_ramp=1), use_ramp = Parameter('use the ramping of the setpoint, or jump',
optional=True), datatype=EnumType(disable_ramp=0, use_ramp=1),
'setpoint': Parameter('currently active setpoint', optional=True)
datatype=FloatRange(0), unit='$', setpoint = Parameter('currently active setpoint',
readonly=True, ), datatype=FloatRange(0), unit='$',
} readonly=True, )
class HAS_Speed(Feature): class HAS_Speed(Feature):
parameters = {
'speed' : Parameter('(maximum) speed of movement (of the main value)', # parameters
unit='$/s', datatype=FloatRange(0)), speed = Parameter('(maximum) speed of movement (of the main value)',
} unit='$/s', datatype=FloatRange(0))
class HAS_Accel(HAS_Speed): class HAS_Accel(HAS_Speed):
parameters = {
'accel' : Parameter('acceleration of movement', unit='$/s^2', # parameters
datatype=FloatRange(0)), accel = Parameter('acceleration of movement', unit='$/s^2',
'decel' : Parameter('deceleration of movement', unit='$/s^2', datatype=FloatRange(0))
datatype=FloatRange(0), optional=True), decel = Parameter('deceleration of movement', unit='$/s^2',
} datatype=FloatRange(0), optional=True)
class HAS_MotorCurrents(Feature): class HAS_MotorCurrents(Feature):
parameters = {
'movecurrent' : Parameter('Current while moving', # parameters
datatype=FloatRange(0)), movecurrent = Parameter('Current while moving',
'idlecurrent' : Parameter('Current while idle', datatype=FloatRange(0))
datatype=FloatRange(0), optional=True), idlecurrent = Parameter('Current while idle',
} datatype=FloatRange(0), optional=True)
class HAS_Curve(Feature): class HAS_Curve(Feature):
# proposed, not yet agreed upon! # proposed, not yet agreed upon!
parameters = {
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'), # parameters
# XXX: tbd. (how to upload/download/select a curve?) curve = Parameter('Calibration curve', datatype=StringType(80), default='<unset>')
}

View File

@ -21,13 +21,13 @@
# ***************************************************************************** # *****************************************************************************
import configparser import configparser
from configparser import NoOptionError
from collections import OrderedDict from collections import OrderedDict
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem from configparser import NoOptionError
from secop.gui.cfg_editor.utils import get_all_items, get_params, get_props,\
get_all_children_with_names, get_module_class_from_name, \
get_interface_class_from_name
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
from secop.gui.cfg_editor.utils import get_all_children_with_names, \
get_all_items, get_interface_class_from_name, \
get_module_class_from_name, get_params, get_props
NODE = 'node' NODE = 'node'
INTERFACE = 'interface' INTERFACE = 'interface'
@ -58,7 +58,7 @@ def write_config(file_name, tree_widget):
value = value.replace('\n\n', '\n.\n') value = value.replace('\n\n', '\n.\n')
value = value.replace('\n', '\n ') value = value.replace('\n', '\n ')
itm_lines[id(itm)] = '[%s %s]\n' % (itm.kind, itm.name) +\ itm_lines[id(itm)] = '[%s %s]\n' % (itm.kind, itm.name) +\
value_str % (SECTIONS[itm.kind], value) value_str % (SECTIONS[itm.kind], value)
# TODO params and props # TODO params and props
elif itm.kind == PARAMETER and value: elif itm.kind == PARAMETER and value:
itm_lines[id(itm)] = value_str % (itm.name, value) itm_lines[id(itm)] = value_str % (itm.name, value)
@ -142,7 +142,7 @@ def read_config(file_path):
else: else:
param.addChild(TreeWidgetItem(PROPERTY, param.addChild(TreeWidgetItem(PROPERTY,
separated[1], get_value(config, section, separated[1], get_value(config, section,
option))) option)))
node = get_comments(node, ifs, mods, file_path) node = get_comments(node, ifs, mods, file_path)
return node, ifs, mods return node, ifs, mods

View File

@ -21,11 +21,11 @@
# ***************************************************************************** # *****************************************************************************
import os import os
from secop.gui.qt import QMainWindow, QMessageBox
from secop.gui.cfg_editor.node_display import NodeDisplay
from secop.gui.cfg_editor.utils import loadUi, get_file_paths
from secop.gui.cfg_editor.widgets import TabBar
from secop.gui.cfg_editor.node_display import NodeDisplay
from secop.gui.cfg_editor.utils import get_file_paths, loadUi
from secop.gui.cfg_editor.widgets import TabBar
from secop.gui.qt import QMainWindow, QMessageBox
# TODO move secop mainwinodw to gui/client and all specific stuff # TODO move secop mainwinodw to gui/client and all specific stuff
NODE = 'node' NODE = 'node'

View File

@ -20,8 +20,8 @@
# #
# ***************************************************************************** # *****************************************************************************
from secop.gui.qt import QWidget, Qt, QHBoxLayout, QSpacerItem, QSizePolicy
from secop.gui.cfg_editor.utils import loadUi from secop.gui.cfg_editor.utils import loadUi
from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
class NodeDisplay(QWidget): class NodeDisplay(QWidget):

View File

@ -20,10 +20,11 @@
# #
# ***************************************************************************** # *****************************************************************************
from secop.gui.qt import QTreeWidgetItem, QFont, QWidget, QVBoxLayout, QLabel, \ from secop.gui.cfg_editor.utils import loadUi, \
QHBoxLayout, QPushButton, QSize, QSizePolicy, QDialog, QTextEdit, pyqtSignal set_name_edit_style, setIcon, setTreeIcon
from secop.gui.cfg_editor.utils import setTreeIcon, setIcon, loadUi, \ from secop.gui.qt import QDialog, QFont, QHBoxLayout, \
set_name_edit_style QLabel, QPushButton, QSize, QSizePolicy, QTextEdit, \
QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
from secop.gui.valuewidgets import get_widget from secop.gui.valuewidgets import get_widget
from secop.properties import Property from secop.properties import Property

View File

@ -20,15 +20,16 @@
# #
# ***************************************************************************** # *****************************************************************************
from os import path, listdir
import sys
import inspect import inspect
from secop.gui.qt import uic, QIcon, QSize, QFileDialog, QDialogButtonBox import sys
from secop.server import getGeneralConfig from os import listdir, path
from secop.gui.qt import QDialogButtonBox, QFileDialog, QIcon, QSize, uic
from secop.modules import Module from secop.modules import Module
from secop.params import Parameter from secop.params import Parameter
from secop.properties import Property from secop.properties import Property
from secop.protocol.interface.tcp import TCPServer from secop.protocol.interface.tcp import TCPServer
from secop.server import getGeneralConfig
uipath = path.dirname(__file__) uipath = path.dirname(__file__)

View File

@ -23,15 +23,15 @@
import os import os
from secop.gui.cfg_editor.config_file import write_config, read_config from secop.gui.cfg_editor.config_file import read_config, write_config
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
from secop.gui.cfg_editor.utils import get_file_paths, get_modules, \ from secop.gui.cfg_editor.utils import get_all_items, \
get_interfaces, loadUi, set_name_edit_style, get_module_class_from_name, \ get_file_paths, get_interface_class_from_name, get_interfaces, \
get_all_items, get_interface_class_from_name, get_params, get_props, \ get_module_class_from_name, get_modules, get_params, \
setActionIcon get_props, loadUi, set_name_edit_style, setActionIcon
from secop.gui.qt import QWidget, QDialog, QLabel, QTabBar, Qt, QPoint, QMenu, \ from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
QTreeWidget, QSize, pyqtSignal, QLineEdit, QComboBox, QDialogButtonBox, \ QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
QTextEdit, QTreeView, QStandardItemModel, QStandardItem Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
NODE = 'node' NODE = 'node'
MODULE = 'module' MODULE = 'module'

View File

@ -26,9 +26,9 @@ import secop.client
from secop.gui.modulectrl import ModuleCtrl from secop.gui.modulectrl import ModuleCtrl
from secop.gui.nodectrl import NodeCtrl from secop.gui.nodectrl import NodeCtrl
from secop.gui.paramview import ParameterView from secop.gui.paramview import ParameterView
from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \ from secop.gui.qt import QBrush, QColor, QInputDialog, QMainWindow, \
QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot, QBrush, QColor QMessageBox, QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot
from secop.gui.util import loadUi, Value from secop.gui.util import Value, loadUi
from secop.lib import formatExtendedTraceback from secop.lib import formatExtendedTraceback
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1 ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
@ -90,7 +90,7 @@ class QSECNode(QObject):
def queryCache(self, module): def queryCache(self, module):
return {k: Value(*self.conn.cache[(module, k)]) return {k: Value(*self.conn.cache[(module, k)])
for k in self.modules[module]['parameters']} for k in self.modules[module]['parameters']}
def syncCommunicate(self, action, ident='', data=None): def syncCommunicate(self, action, ident='', data=None):
reply = self.conn.request(action, ident, data) reply = self.conn.request(action, ident, data)

View File

@ -36,19 +36,19 @@ class CommandDialog(QDialog):
loadUi(self, 'cmddialog.ui') loadUi(self, 'cmddialog.ui')
self.setWindowTitle('Arguments for %s' % cmdname) self.setWindowTitle('Arguments for %s' % cmdname)
#row = 0 # row = 0
self._labels = [] self._labels = []
self.widgets = [] self.widgets = []
# improve! recursive? # improve! recursive?
dtype = argument dtype = argument
l = QLabel(repr(dtype)) label = QLabel(repr(dtype))
l.setWordWrap(True) label.setWordWrap(True)
w = get_widget(dtype, readonly=False) widget = get_widget(dtype, readonly=False)
self.gridLayout.addWidget(l, 0, 0) self.gridLayout.addWidget(label, 0, 0)
self.gridLayout.addWidget(w, 0, 1) self.gridLayout.addWidget(widget, 0, 1)
self._labels.append(l) self._labels.append(label)
self.widgets.append(w) self.widgets.append(widget)
self.gridLayout.setRowStretch(1, 1) self.gridLayout.setRowStretch(1, 1)
self.setModal(True) self.setModal(True)

View File

@ -25,14 +25,15 @@
import json import json
import pprint import pprint
from time import sleep from time import sleep
import mlzlog import mlzlog
import secop.lib
from secop.datatypes import EnumType, StringType from secop.datatypes import EnumType, StringType
from secop.errors import SECoPError from secop.errors import SECoPError
from secop.gui.qt import QFont, QFontMetrics, QLabel, \ from secop.gui.qt import QFont, QFontMetrics, QLabel, \
QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped
from secop.gui.util import loadUi, Value from secop.gui.util import Value, loadUi
import secop.lib
class NodeCtrl(QWidget): class NodeCtrl(QWidget):
@ -167,7 +168,6 @@ class NodeCtrl(QWidget):
print(secop.lib.formatExtendedTraceback()) print(secop.lib.formatExtendedTraceback())
widget = QLabel('Bad configured Module %s! (%s)' % (modname, e)) widget = QLabel('Bad configured Module %s! (%s)' % (modname, e))
if unit: if unit:
labelstr = '%s (%s):' % (modname, unit) labelstr = '%s (%s):' % (modname, unit)
else: else:
@ -289,7 +289,7 @@ class DrivableWidget(ReadableWidget):
def update_current(self, value): def update_current(self, value):
self.currentLineEdit.setText(str(value)) self.currentLineEdit.setText(str(value))
#elif self._is_enum: # elif self._is_enum:
# member = self._map[self._revmap[value.value]] # member = self._map[self._revmap[value.value]]
# self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value)) # self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))

View File

@ -22,12 +22,9 @@
# ***************************************************************************** # *****************************************************************************
from secop.datatypes import EnumType, FloatRange, IntRange from secop.datatypes import EnumType
from secop.gui.qt import QPushButton as QButton from secop.gui.qt import QWidget, pyqtSignal, pyqtSlot
from secop.gui.qt import QCheckBox, QLabel, QLineEdit, \
QMessageBox, QSizePolicy, Qt, QWidget, pyqtSignal, pyqtSlot
from secop.gui.util import loadUi from secop.gui.util import loadUi
from secop.lib import formatExtendedStack
class ParameterWidget(QWidget): class ParameterWidget(QWidget):

View File

@ -32,6 +32,7 @@ uipath = path.dirname(__file__)
def loadUi(widget, uiname, subdir='ui'): def loadUi(widget, uiname, subdir='ui'):
uic.loadUi(path.join(uipath, subdir, uiname), widget) uic.loadUi(path.join(uipath, subdir, uiname), widget)
class Value: class Value:
def __init__(self, value, timestamp=None, readerror=None): def __init__(self, value, timestamp=None, readerror=None):
self.value = value self.value = value

View File

@ -23,12 +23,13 @@
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
FloatRange, IntRange, StringType, StructOf, TupleOf, TextType FloatRange, IntRange, StringType, StructOf, TextType, TupleOf
from secop.gui.qt import QCheckBox, QComboBox, QDialog, QDoubleSpinBox, \ from secop.gui.qt import QCheckBox, QComboBox, QDialog, \
QFrame, QGridLayout, QGroupBox, QLabel, QLineEdit, QSpinBox, QVBoxLayout, \ QDoubleSpinBox, QFrame, QGridLayout, QGroupBox, \
QTextEdit QLabel, QLineEdit, QSpinBox, QTextEdit, QVBoxLayout
from secop.gui.util import loadUi from secop.gui.util import loadUi
# XXX: implement live validators !!!! # XXX: implement live validators !!!!
# XXX: signals upon change of value # XXX: signals upon change of value
# XXX: honor readonly in all cases! # XXX: honor readonly in all cases!
@ -171,12 +172,12 @@ class StructWidget(QGroupBox):
self._labels = [] self._labels = []
for idx, name in enumerate(sorted(datatype.members)): for idx, name in enumerate(sorted(datatype.members)):
dt = datatype.members[name] dt = datatype.members[name]
w = get_widget(dt, readonly=readonly, parent=self) widget = get_widget(dt, readonly=readonly, parent=self)
l = QLabel(name) label = QLabel(name)
self.layout.addWidget(l, idx, 0) self.layout.addWidget(label, idx, 0)
self.layout.addWidget(w, idx, 1) self.layout.addWidget(widget, idx, 1)
self._labels.append(l) self._labels.append(label)
self.subwidgets[name] = (w, dt) self.subwidgets[name] = (widget, dt)
self.datatypes.append(dt) self.datatypes.append(dt)
self.setLayout(self.layout) self.setLayout(self.layout)
@ -215,21 +216,22 @@ class ArrayWidget(QGroupBox):
w.set_value(v) w.set_value(v)
def get_widget(datatype, readonly=False, parent=None): def get_widget(datatype, readonly=False, parent=None):
return {FloatRange: FloatWidget, return {
IntRange: IntWidget, FloatRange: FloatWidget,
StringType: StringWidget, IntRange: IntWidget,
TextType: TextWidget, StringType: StringWidget,
BLOBType: BlobWidget, TextType: TextWidget,
EnumType: EnumWidget, BLOBType: BlobWidget,
BoolType: BoolWidget, EnumType: EnumWidget,
TupleOf: TupleWidget, BoolType: BoolWidget,
StructOf: StructWidget, TupleOf: TupleWidget,
ArrayOf: ArrayWidget, StructOf: StructWidget,
ArrayOf: ArrayWidget,
}.get(datatype.__class__)(datatype, readonly, parent) }.get(datatype.__class__)(datatype, readonly, parent)
# TODO: handle NoneOr # TODO: handle NoneOr
class msg(QDialog): class msg(QDialog):
def __init__(self, stuff, parent=None): def __init__(self, stuff, parent=None):
super(msg, self).__init__(parent) super(msg, self).__init__(parent)
@ -242,7 +244,7 @@ class msg(QDialog):
dt = StructOf(i=IntRange(0, 10), f=FloatRange(), b=BoolType()) dt = StructOf(i=IntRange(0, 10), f=FloatRange(), b=BoolType())
w = StructWidget(dt) w = StructWidget(dt)
self.gridLayout.addWidget(w, row, 1) self.gridLayout.addWidget(w, row, 1)
row+=1 row += 1
self.gridLayout.addWidget(QLabel('stuff'), row, 0, 1, 0) self.gridLayout.addWidget(QLabel('stuff'), row, 0, 1, 0)
row += 1 # at pos (0,0) span 2 cols, 1 row row += 1 # at pos (0,0) span 2 cols, 1 row

View File

@ -54,8 +54,8 @@ method has to be called explicitly int the write_<parameter> method, if needed.
""" """
import re import re
from secop.metaclass import Done
from secop.errors import ProgrammingError from secop.errors import ProgrammingError
from secop.modules import Done
class CmdParser: class CmdParser:
@ -202,13 +202,19 @@ class IOHandler(IOHandlerBase):
:param replyfmt: the format for reading the reply with some scanf like behaviour :param replyfmt: the format for reading the reply with some scanf like behaviour
:param changecmd: the first part of the change command (without values), may be :param changecmd: the first part of the change command (without values), may be
omitted if no write happens omitted if no write happens
"""
"""
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
def __init__(self, group, querycmd, replyfmt, changecmd=None): def __init__(self, group, querycmd, replyfmt, changecmd=None):
"""initialize the IO handler""" """initialize the IO handler
group: the handler group (used for analyze_<group> and change_<group>)
querycmd: the command for a query, may contain named formats for cmdargs
replyfmt: the format for reading the reply with some scanf like behaviour
changecmd: the first part of the change command (without values), may be
omitted if no write happens
"""
self.group = group self.group = group
self.parameters = set() self.parameters = set()
self._module_class = None self._module_class = None

View File

@ -21,13 +21,13 @@
# ***************************************************************************** # *****************************************************************************
"""Define helpers""" """Define helpers"""
import importlib
import linecache import linecache
import socket import socket
import sys import sys
import threading import threading
import traceback import traceback
import importlib from os import environ, path
from os import path, environ
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..')) repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
@ -58,6 +58,7 @@ CONFIG['basedir'] = repodir
unset_value = object() unset_value = object()
class lazy_property: class lazy_property:
"""A property that calculates its value only once.""" """A property that calculates its value only once."""

View File

@ -28,16 +28,18 @@ support for asynchronous communication, but may be used also for
synchronous IO (see secop.stringio.StringIO) synchronous IO (see secop.stringio.StringIO)
""" """
import socket
import select
import time
import ast import ast
import select
import socket
import time
from secop.errors import CommunicationFailedError, ConfigError
from secop.lib import closeSocket, parseHostPort, tcpSocket
try: try:
from serial import Serial from serial import Serial
except ImportError: except ImportError:
Serial = None Serial = None
from secop.lib import parseHostPort, tcpSocket, closeSocket
from secop.errors import ConfigError, CommunicationFailedError
class ConnectionClosed(ConnectionError): class ConnectionClosed(ConnectionError):
@ -60,10 +62,10 @@ class AsynConn:
except (ValueError, TypeError, AssertionError): except (ValueError, TypeError, AssertionError):
if 'COM' in uri: if 'COM' in uri:
raise ValueError("the correct uri for a COM port is: " raise ValueError("the correct uri for a COM port is: "
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'" ) "'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
if '/dev' in uri: if '/dev' in uri:
raise ValueError("the correct uri for a serial port is: " raise ValueError("the correct uri for a serial port is: "
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'" ) "'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
raise ValueError('invalid uri: %s' % uri) raise ValueError('invalid uri: %s' % uri)
iocls = cls.SCHEME_MAP['tcp'] iocls = cls.SCHEME_MAP['tcp']
uri = 'tcp://%s:%d' % host_port uri = 'tcp://%s:%d' % host_port

View File

@ -20,57 +20,168 @@
# #
# ***************************************************************************** # *****************************************************************************
from inspect import cleandoc
from textwrap import indent from textwrap import indent
from secop.modules import Command, HasProperties, Module, Parameter, Property
def indent_description(p): def indent_description(p):
"""indent lines except first one""" """indent lines except first one"""
return indent(p.description, ' ').replace(' ', '', 1) return indent(p.description, ' ').replace(' ', '', 1)
def append_to_doc(cls, name, title, attrname, newitems, fmtfunc): def fmt_param(name, param):
desc = indent_description(param)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [short_doc(param.datatype), 'rd' if param.readonly else 'wr',
None if param.export else 'hidden']
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
def fmt_command(name, command):
desc = indent_description(command)
if '(' in desc[0:2]:
dtinfo = '' # note: we expect that desc contains argument list
else:
dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*')
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
def fmt_property(name, prop):
desc = indent_description(prop)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden']
dtinfo = ', '.join(filter(None, dtinfo))
if dtinfo:
dtinfo = '*(%s)* ' % dtinfo
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
SIMPLETYPES = {
'FloatRange': 'float',
'ScaledInteger': 'float',
'IntRange': 'int',
'BlobType': 'bytes',
'StringType': 'str',
'TextType': 'str',
'BoolType': 'bool',
'StructOf': 'dict',
}
def short_doc(datatype):
# pylint: disable=possibly-unused-variable
def doc_EnumType(dt):
return 'one of %s' % str(tuple(dt._enum.keys()))
def doc_ArrayOf(dt):
return 'array of %s' % short_doc(dt.members)
def doc_TupleOf(dt):
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
def doc_CommandType(dt):
argument = short_doc(dt.argument) if dt.argument else ''
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
return '(%s)%s' % (argument, result) # return argument list only
def doc_NoneOr(dt):
other = short_doc(dt.other)
return '%s or None' % other if other else None
def doc_OrType(dt):
types = [short_doc(t) for t in dt.types]
if None in types: # type is anyway broad: no doc
return None
return ' or '.join(types)
def doc_Stub(dt):
return dt.name.replace('Type', '').replace('Range', '').lower()
clsname = datatype.__class__.__name__
result = SIMPLETYPES.get(clsname)
if result:
return result
fun = locals().get('doc_' + clsname)
if fun:
return fun(datatype)
return None # broad type like ValueType: no doc
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
"""add information about some items to the doc """add information about some items to the doc
:param cls: the class with the doc string to be extended :param cls: the class with the doc string to be extended
:param name: the name of the attribute dict to be used :param lines: content of the docstring, as lines
:param title: the title to be used :param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
:param newitems: the set of new items defined for this class :param attrname: the name of the attribute dict to look for
:param name: the name of the items to be collected (used for the title and for the tags)
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end :param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
or an empty string to suppress output for this item or an empty string to suppress output for this item
:type fmtfunc: function(key, value) :type fmtfunc: function(key, value)
rules, assuming name='properties':
- if the docstring contains ``{properties}``, new properties are inserted here
- if the docstring contains ``{all properties}``, all properties are inserted here
- if the docstring contains ``{no properties}``, no properties are inserted
only the first appearance of a tag above is considered
""" """
doc = cleandoc(cls.__doc__ or '') doc = '\n'.join(lines)
title = 'SECoP %s' % name.title()
allitems = getattr(cls, attrname, {}) allitems = getattr(cls, attrname, {})
fmtdict = {n: fmtfunc(n, p) or ' - **%s** *removed*\n' % n for n, p in allitems.items()} fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)}
head, _, tail = doc.partition('{all %s}' % name) head, _, tail = doc.partition('{all %s}' % name)
clsset = set()
if tail: # take all if tail: # take all
inherited = set() fmted = fmtdict.values()
fmted = ''.join(fmtdict.values())
else: else:
inherited = {n: p for n, p in allitems.items() if fmtdict.get(n) and n not in newitems}
fmted = ''.join(' ' + v for k, v in fmtdict.items() if k in newitems)
head, _, tail = doc.partition('{%s}' % name) head, _, tail = doc.partition('{%s}' % name)
if not tail: if not tail:
head, _, tail = doc.partition('{no %s}' % name) head, _, tail = doc.partition('{no %s}' % name)
if tail: # add no information if tail: # add no information
return return
# no tag found: append to the end # no tag found: append to the end
if fmted:
clsset = set() fmted = []
for name in inherited: for key, formatted_item in fmtdict.items():
p = allitems[name] if not formatted_item:
refcls = cls continue
# find where item is defined or modified
refcls = None
for base in cls.__mro__: for base in cls.__mro__:
dp = getattr(base, attrname, {}).get(name) p = getattr(base, attrname, {}).get(key)
if dp: if isinstance(p, itemcls):
if dp == p: if fmtfunc(key, p) == formatted_item:
refcls = base refcls = base
else: else:
break break
clsset.add(refcls) if refcls == cls:
clsset.discard(cls) # definition in cls is new or modified
fmted.append(formatted_item)
else:
# definition of last modification in refcls
clsset.add(refcls)
if fmted:
if clsset: if clsset:
fmted += ' - see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__) fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
for c in cls.__mro__ if c in clsset)) for c in cls.__mro__ if c in clsset)))
cls.__doc__ = '%s\n\n:%s: %s\n%s' % (head, title, fmted, tail)
doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail)
lines[:] = doc.split('\n')
def class_doc_handler(app, what, name, cls, options, lines):
if what == 'class':
if issubclass(cls, HasProperties):
append_to_doc(cls, lines, Property, 'properties', 'propertyDict', fmt_property)
if issubclass(cls, Module):
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)

View File

@ -32,6 +32,7 @@ class EnumMember:
has an int-type value and attributes 'name' and 'value' has an int-type value and attributes 'name' and 'value'
""" """
__slots__ = ['name', 'value', 'enum'] __slots__ = ['name', 'value', 'enum']
def __init__(self, enum, name, value): def __init__(self, enum, name, value):
if not isinstance(enum, Enum): if not isinstance(enum, Enum):
raise TypeError('1st Argument must be an instance of class Enum()') raise TypeError('1st Argument must be an instance of class Enum()')
@ -49,7 +50,7 @@ class EnumMember:
try: try:
other = int(other) other = int(other)
except Exception: except Exception:
#raise TypeError('%r can not be compared to %r!' %(other, self)) # raise TypeError('%r can not be compared to %r!' %(other, self))
return -1 # XXX:! return -1 # XXX:!
if self.value < other: if self.value < other:
return -1 return -1
@ -59,10 +60,12 @@ class EnumMember:
def __lt__(self, other): def __lt__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1 return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
def __le__(self, other): def __le__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1 return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, (EnumMember)): if isinstance(other, EnumMember):
return other.value == self.value return other.value == self.value
if isinstance(other, int): if isinstance(other, int):
return other == self.value return other == self.value
@ -72,10 +75,13 @@ class EnumMember:
return self.name == other return self.name == other
return False return False
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0 return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def __ge__(self, other): def __ge__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1 return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
def __gt__(self, other): def __gt__(self, other):
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1 return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
@ -100,77 +106,105 @@ class EnumMember:
def __repr__(self): def __repr__(self):
return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value) return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value)
# numeric operations: delegate to int. Do we really need any of those? # numeric operations: delegate to int. Do we really need any of those?
def __add__(self, other): def __add__(self, other):
return self.value.__add__(other.value if isinstance(other, EnumMember) else other) return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
def __sub__(self, other): def __sub__(self, other):
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other) return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
def __mul__(self, other): def __mul__(self, other):
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other) return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
def __truediv__(self, other): def __truediv__(self, other):
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other) return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
def __floordiv__(self, other): def __floordiv__(self, other):
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other) return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
def __mod__(self, other): def __mod__(self, other):
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other) return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
def __divmod__(self, other): def __divmod__(self, other):
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other) return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
def __pow__(self, other, *args): def __pow__(self, other, *args):
return self.value.__pow__(other, *args) return self.value.__pow__(other, *args)
def __lshift__(self, other): def __lshift__(self, other):
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other) return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
def __rshift__(self, other): def __rshift__(self, other):
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other) return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
def __radd__(self, other): def __radd__(self, other):
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other) return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
def __rsub__(self, other): def __rsub__(self, other):
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other) return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
def __rmul__(self, other): def __rmul__(self, other):
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other) return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
def __rtruediv__(self, other): def __rtruediv__(self, other):
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other) return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
def __rfloordiv__(self, other): def __rfloordiv__(self, other):
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other) return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
def __rmod__(self, other): def __rmod__(self, other):
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other) return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
def __rdivmod__(self, other): def __rdivmod__(self, other):
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other) return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
def __rpow__(self, other, *args): def __rpow__(self, other, *args):
return self.value.__rpow__(other, *args) return self.value.__rpow__(other, *args)
def __rlshift__(self, other): def __rlshift__(self, other):
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other) return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
def __rrshift__(self, other): def __rrshift__(self, other):
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other) return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
# logical operations # logical operations
def __and__(self, other): def __and__(self, other):
return self.value.__and__(other.value if isinstance(other, EnumMember) else other) return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
def __xor__(self, other): def __xor__(self, other):
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other) return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
def __or__(self, other): def __or__(self, other):
return self.value.__or__(other.value if isinstance(other, EnumMember) else other) return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
def __rand__(self, other): def __rand__(self, other):
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other) return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
def __rxor__(self, other): def __rxor__(self, other):
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other) return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
def __ror__(self, other): def __ror__(self, other):
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other) return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
# other stuff # other stuff
def __neg__(self): def __neg__(self):
return self.value.__neg__() return self.value.__neg__()
def __pos__(self): def __pos__(self):
return self.value.__pos__() return self.value.__pos__()
def __abs__(self): def __abs__(self):
return self.value.__abs__() return self.value.__abs__()
def __invert__(self): def __invert__(self):
return self.value.__invert__() return self.value.__invert__()
def __int__(self): def __int__(self):
return self.value.__int__() return self.value.__int__()
def __float__(self): def __float__(self):
return self.value.__float__() return self.value.__float__()
#return NotImplemented # makes no sense
def __index__(self): def __index__(self):
return self.value.__index__() return self.value.__index__()
@ -206,6 +240,7 @@ class Enum(dict):
You only can create an extended Enum. You only can create an extended Enum.
""" """
name = '' name = ''
def __init__(self, name='', parent=None, **kwds): def __init__(self, name='', parent=None, **kwds):
super(Enum, self).__init__() super(Enum, self).__init__()
if isinstance(name, (dict, Enum)) and parent is None: if isinstance(name, (dict, Enum)) and parent is None:
@ -217,7 +252,7 @@ class Enum(dict):
# if name was not given, use that of the parent # if name was not given, use that of the parent
# this means, an extended Enum behaves like the parent # this means, an extended Enum behaves like the parent
# THIS MAY BE CONFUSING SOMETIMES! # THIS MAY BE CONFUSING SOMETIMES!
name=parent.name name = parent.name
# else: # else:
# raise TypeError('Enum instances need a name or an Enum parent!') # raise TypeError('Enum instances need a name or an Enum parent!')
if not isinstance(name, str): if not isinstance(name, str):
@ -225,8 +260,9 @@ class Enum(dict):
names = set() names = set()
values = set() values = set()
# pylint: disable=dangerous-default-value # pylint: disable=dangerous-default-value
def add(self, k, v, names = names, value = values): def add(self, k, v, names=names, value=values):
"""helper for creating the enum members""" """helper for creating the enum members"""
if v is None: if v is None:
# sugar: take the next free number if value was None # sugar: take the next free number if value was None
@ -237,7 +273,7 @@ class Enum(dict):
if v in names: if v in names:
v = self[v].value v = self[v].value
while v in values: while v in values:
v +=1 v += 1
# check that the value is an int # check that the value is an int
_v = int(v) _v = int(v)
@ -290,7 +326,6 @@ class Enum(dict):
def __repr__(self): def __repr__(self):
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members)) return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
# return '<Enum %r (%d values)>' % (self.name, len(self)//2)
def __call__(self, key): def __call__(self, key):
return self[key] return self[key]

View File

@ -21,6 +21,7 @@
# ***************************************************************************** # *****************************************************************************
"""Define parsing helpers""" """Define parsing helpers"""
# TODO: remove, as currently not used
import re import re
import time import time

View File

@ -141,7 +141,7 @@ class SequencerMixin:
return self.read_hw_status() return self.read_hw_status()
return self.Status.IDLE, '' return self.Status.IDLE, ''
def do_stop(self): def stop(self):
if self.seq_is_alive(): if self.seq_is_alive():
self._seq_stopflag = True self._seq_stopflag = True

View File

@ -1,259 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# 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:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""Define Metaclass for Modules/Features"""
from collections import OrderedDict
from secop.errors import ProgrammingError, BadValueError
from secop.params import Command, Override, Parameter
from secop.datatypes import EnumType
from secop.properties import PropertyMeta
from secop.lib.classdoc import append_to_doc, indent_description
class Done:
"""a special return value for a read/write function
indicating that the setter is triggered already"""
# warning: MAGIC!
class ModuleMeta(PropertyMeta):
"""Metaclass
joining the class's properties, parameters and commands dicts with
those of base classes.
also creates getters/setter for parameter access
and wraps read_*/write_* methods
(so the dispatcher will get notfied of changed values)
"""
def __new__(cls, name, bases, attrs):
commands = attrs.pop('commands', {})
parameters = attrs.pop('parameters', {})
overrides = attrs.pop('overrides', {})
newtype = type.__new__(cls, name, bases, attrs)
if '__constructed__' in attrs:
return newtype
newtype = PropertyMeta.__join_properties__(newtype, name, bases, attrs)
# merge accessibles from all sub-classes, treat overrides
# for now, allow to use also the old syntax (parameters/commands dict)
accessibles_list = []
for base in reversed(bases):
if hasattr(base, "accessibles"):
accessibles_list.append(base.accessibles)
for accessibles in [parameters, commands, overrides]:
accessibles_list.append(accessibles)
accessibles = {} # unordered dict of accessibles, will be sorted later
for accessibles_dict in accessibles_list:
for key, obj in accessibles_dict.items():
if isinstance(obj, Override):
if key not in accessibles:
raise ProgrammingError("module %s: can not apply Override on %s: no such accessible!"
% (name, key))
obj = obj.apply(accessibles[key])
accessibles[key] = obj
else:
if obj is None: # allow removal of accessibles
accessibles.pop(key, None)
continue
if key in accessibles:
# for now, accept redefinitions:
print("WARNING: module %s: %s should not be redefined"
% (name, key))
# raise ProgrammingError("module %s: %s must not be redefined"
# % (name, key))
if isinstance(obj, Parameter):
accessibles[key] = obj
elif isinstance(obj, Command):
# XXX: convert to param with datatype=CommandType???
accessibles[key] = obj
else:
raise ProgrammingError('%r: accessibles entry %r should be a '
'Parameter or Command object!' % (name, key))
# Correct naming of EnumTypes
for k, v in accessibles.items():
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
v.datatype._enum.name = k
# newtype.accessibles will be used in 2 places only:
# 1) for inheritance (see above)
# 2) for the describing message
newtype.accessibles = OrderedDict(sorted(accessibles.items(), key=lambda item: item[1].ctr))
# check for attributes overriding parameter values
for pname, pobj in newtype.accessibles.items():
if pname in attrs:
try:
value = pobj.datatype(attrs[pname])
except BadValueError:
raise ProgrammingError('parameter %s can not be set to %r'
% (pname, attrs[pname]))
newtype.accessibles[pname] = Override(default=value).apply(pobj)
# check validity of Parameter entries
for pname, pobj in newtype.accessibles.items():
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
if isinstance(pobj, Command):
# skip commands for now
continue
rfunc = attrs.get('read_' + pname, None)
rfunc_handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None
if rfunc_handler:
if rfunc:
raise ProgrammingError("parameter '%s' can not have a handler "
"and read_%s" % (pname, pname))
rfunc = rfunc_handler
else:
for base in bases:
if rfunc is not None:
break
rfunc = getattr(base, 'read_' + pname, None)
# create wrapper except when read function is already wrapped
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
if rfunc:
self.log.debug("calling %r" % rfunc)
try:
value = rfunc(self)
self.log.debug("rfunc(%s) returned %r" % (pname, value))
if value is Done: # the setter is already triggered
return getattr(self, pname)
except Exception as e:
self.log.debug("rfunc(%s) failed %r" % (pname, e))
self.announceUpdate(pname, None, e)
raise
else:
# return cached value
self.log.debug("rfunc(%s): return cached value" % pname)
value = self.accessibles[pname].value
setattr(self, pname, value) # important! trigger the setter
return value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(newtype, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None)
if wfunc is None: # ignore the handler, if a write function is present
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
for base in bases:
if wfunc is not None:
break
wfunc = getattr(base, 'write_' + pname, None)
# create wrapper except when write function is already wrapped
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("check validity of %s = %r" % (pname, value))
pobj = self.accessibles[pname]
value = pobj.datatype(value)
if wfunc:
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
returned_value = wfunc(self, value)
if returned_value is Done: # the setter is already triggered
return getattr(self, pname)
if returned_value is not None: # goodie: accept missing return value
value = returned_value
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(newtype, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
def getter(self, pname=pname):
return self.accessibles[pname].value
def setter(self, value, pname=pname):
self.announceUpdate(pname, value)
setattr(newtype, pname, property(getter, setter))
# check information about Command's
for attrname in attrs:
if attrname.startswith('do_'):
if attrname[3:] not in newtype.accessibles:
raise ProgrammingError('%r: command %r has to be specified '
'explicitly!' % (name, attrname[3:]))
def fmt_param(name, param):
if not isinstance(param, Parameter):
return ''
desc = indent_description(param)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [param.datatype.short_doc(), 'rd' if param.readonly else 'wr',
None if param.export else 'hidden']
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
def fmt_command(name, command):
if not isinstance(command, Command):
return ''
desc = indent_description(command)
if '(' in desc[0:2]:
dtinfo = '' # note: we expect that desc contains argument list
else:
dtinfo = '*%s*' % command.datatype.short_doc() + ' -%s ' % ('' if command.export else ' *(hidden)*')
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
append_to_doc(newtype, 'parameters', 'SECOP Parameters',
'accessibles', set(parameters) | set(overrides), fmt_param)
append_to_doc(newtype, 'commands', 'SECOP Commands',
'accessibles', set(commands) | set(overrides), fmt_command)
attrs['__constructed__'] = True
return newtype
@property
def configurables(cls):
# note: this ends up as an property of the Module class (not on the instance)!
# dict of properties with Property and Parameter with dict of properties
res = {}
# collect info about properties
for pn, pv in cls.properties.items():
if pv.settable:
res[pn] = pv
# collect info about parameters and their properties
for param, pobj in cls.accessibles.items():
res[param] = {}
for pn, pv in pobj.getProperties().items():
if pv.settable:
res[param][pn] = pv
return res

View File

@ -20,32 +20,158 @@
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# #
# ***************************************************************************** # *****************************************************************************
"""Define Baseclasses for real Modules implemented in the server""" """Define base classes for real Modules implemented in the server"""
import sys import sys
import time import time
from collections import OrderedDict
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \ from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType IntRange, StatusType, StringType, TextType, TupleOf, get_datatype
from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueError,\ from secop.errors import BadValueError, ConfigError, InternalError, \
SilentError, InternalError, secop_error ProgrammingError, SECoPError, SilentError, secop_error
from secop.lib import formatException, formatExtendedStack, mkthread from secop.lib import formatException, formatExtendedStack, mkthread
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.metaclass import ModuleMeta from secop.params import PREDEFINED_ACCESSIBLES, Accessible, Command, Parameter
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands from secop.poller import BasicPoller, Poller
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
from secop.poller import Poller, BasicPoller
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
# XXX: connect with 'protocol'-Modules. class HasAccessibles(HasProperties):
# Idea: every Module defined herein is also a 'protocol'-Module, """base class of module
# all others MUST derive from those, the 'interface'-class is still derived
# from these base classes (how to do this?) joining the class's properties, parameters and commands dicts with
those of base classes.
wrap read_*/write_* methods
(so the dispatcher will get notified of changed values)
"""
@classmethod
def __init_subclass__(cls): # pylint: disable=too-many-branches
super().__init_subclass__()
# merge accessibles from all sub-classes, treat overrides
# for now, allow to use also the old syntax (parameters/commands dict)
accessibles = {}
for base in reversed(cls.__bases__):
accessibles.update(getattr(base, 'accessibles', {}))
newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)}
for aname, aobj in accessibles.items():
value = getattr(cls, aname, None)
if not isinstance(value, Accessible): # else override is already done in __set_name__
anew = aobj.override(value)
newaccessibles[aname] = anew
setattr(cls, aname, anew)
anew.__set_name__(cls, aname)
ordered = {}
for aname in cls.__dict__.get('paramOrder', ()):
if aname in accessibles:
ordered[aname] = accessibles.pop(aname)
elif aname in newaccessibles:
ordered[aname] = newaccessibles.pop(aname)
# ignore unknown names
# starting from old accessibles not mentioned, append items from 'order'
accessibles.update(ordered)
# then new accessibles not mentioned
accessibles.update(newaccessibles)
cls.accessibles = accessibles
# Correct naming of EnumTypes
for k, v in accessibles.items():
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
v.datatype.set_name(k)
# check validity of Parameter entries
for pname, pobj in accessibles.items():
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
if isinstance(pobj, Command):
# nothing to do for now
continue
rfunc = cls.__dict__.get('read_' + pname, None)
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
if rfunc_handler:
if rfunc:
raise ProgrammingError("parameter '%s' can not have a handler "
"and read_%s" % (pname, pname))
rfunc = rfunc_handler
# create wrapper except when read function is already wrapped
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
if rfunc:
self.log.debug("calling %r" % rfunc)
try:
value = rfunc(self)
self.log.debug("rfunc(%s) returned %r" % (pname, value))
if value is Done: # the setter is already triggered
return getattr(self, pname)
except Exception as e:
self.log.debug("rfunc(%s) failed %r" % (pname, e))
self.announceUpdate(pname, None, e)
raise
else:
# return cached value
self.log.debug("rfunc(%s): return cached value" % pname)
value = self.accessibles[pname].value
setattr(self, pname, value) # important! trigger the setter
return value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
setattr(cls, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = getattr(cls, 'write_' + pname, None)
if wfunc is None: # ignore the handler, if a write function is present
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
# create wrapper except when write function is already wrapped
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("check validity of %s = %r" % (pname, value))
pobj = self.accessibles[pname]
value = pobj.datatype(value)
if wfunc:
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
returned_value = wfunc(self, value)
if returned_value is Done: # the setter is already triggered
return getattr(self, pname)
if returned_value is not None: # goodie: accept missing return value
value = returned_value
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
setattr(cls, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
# check information about Command's
for attrname in cls.__dict__:
if attrname.startswith('do_'):
raise ProgrammingError('%r: old style command %r not supported anymore'
% (cls.__name__, attrname))
res = {}
# collect info about properties
for pn, pv in cls.propertyDict.items():
if pv.settable:
res[pn] = pv
# collect info about parameters and their properties
for param, pobj in cls.accessibles.items():
res[param] = {}
for pn, pv in pobj.getProperties().items():
if pv.settable:
res[param][pn] = pv
cls.configurables = res
class Module(HasProperties, metaclass=ModuleMeta): class Module(HasAccessibles):
"""basic module """basic module
all SECoP modules derive from this. all SECoP modules derive from this.
@ -58,7 +184,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
Notes: Notes:
- the programmer normally should not need to reimplement :meth:`__init__` - the programmer normally should not need to reimplement :meth:`__init__`
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc... - within modules, parameters should only be addressed as ``self.<pname>``,
i.e. ``self.value``, ``self.target`` etc...
- these are accessing the cached version. - these are accessing the cached version.
- they can also be written to, generating an async update - they can also be written to, generating an async update
@ -77,25 +204,21 @@ class Module(HasProperties, metaclass=ModuleMeta):
# note: properties don't change after startup and are usually filled # note: properties don't change after startup and are usually filled
# with data from a cfg file... # with data from a cfg file...
# note: only the properties predefined here are allowed to be set in the cfg file # note: only the properties predefined here are allowed to be set in the cfg file
# note: the names map to a [datatype, value] list, value comes from the cfg file, export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
# datatype is fixed! group = Property('optional group the module belongs to', StringType(), default='', extname='group')
properties = { description = Property('description of the module', TextType(), extname='description', mandatory=True)
'export': Property('flag if this Module is to be exported', BoolType(), default=True, export=False), meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
'group': Property('optional group the Module belongs to', StringType(), default='', extname='group'), default=('', 0), extname='meaning')
'description': Property('description of the module', TextType(), extname='description', mandatory=True), visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
'meaning': Property('dptional Meaning indicator', TupleOf(StringType(),IntRange(0,50)), default='user', extname='visibility')
default=('',0), extname='meaning'), implementation = Property('internal name of the implementation class of the module', StringType(),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), extname='implementation')
default='user', extname='visibility'), interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()),
'implementation': Property('internal name of the implementation class of the module', StringType(), extname='interface_classes')
extname='implementation'),
'interface_classes': Property('offical highest Interface-class of the module', ArrayOf(StringType()),
extname='interface_classes'),
}
# properties, parameters and commands are auto-merged upon subclassing # properties, parameters and commands are auto-merged upon subclassing
parameters = {} #: definition of parameters parameters = {}
commands = {} #: definition of commands commands = {}
# reference to the dispatcher (used for sending async updates) # reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
@ -112,14 +235,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
# handle module properties # handle module properties
# 1) make local copies of properties # 1) make local copies of properties
super(Module, self).__init__() super().__init__()
# 2) check and apply properties specified in cfgdict # 2) check and apply properties specified in cfgdict
# specified as '.<propertyname> = <propertyvalue>' # specified as '.<propertyname> = <propertyvalue>'
# (this is for legacy config files only) # (this is for legacy config files only)
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if k[0] == '.': if k[0] == '.':
if k[1:] in self.__class__.properties: if k[1:] in self.propertyDict:
self.setProperty(k[1:], cfgdict.pop(k)) self.setProperty(k[1:], cfgdict.pop(k))
else: else:
raise ConfigError('Module %r has no property %r' % raise ConfigError('Module %r has no property %r' %
@ -127,20 +250,20 @@ class Module(HasProperties, metaclass=ModuleMeta):
# 3) check and apply properties specified in cfgdict as # 3) check and apply properties specified in cfgdict as
# '<propertyname> = <propertyvalue>' (without '.' prefix) # '<propertyname> = <propertyvalue>' (without '.' prefix)
for k in self.__class__.properties: for k in self.propertyDict:
if k in cfgdict: if k in cfgdict:
self.setProperty(k, cfgdict.pop(k)) self.setProperty(k, cfgdict.pop(k))
# 4) set automatic properties # 4) set automatic properties
mycls = self.__class__ mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.properties['implementation'] = myclassname self.implementation = myclassname
# list of all 'secop' modules # list of all 'secop' modules
self.properties['interface_classes'] = [ # self.interface_classes = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')] # b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
# list of only the 'highest' secop module class # list of only the 'highest' secop module class
self.properties['interface_classes'] = [[ self.interface_classes = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]] b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
# handle Features # handle Features
# XXX: todo # XXX: todo
@ -149,7 +272,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
# 1) make local copies of parameter objects # 1) make local copies of parameter objects
# they need to be individual per instance since we use them also # they need to be individual per instance since we use them also
# to cache the current value + qualifiers... # to cache the current value + qualifiers...
accessibles = OrderedDict() accessibles = {}
# conversion from exported names to internal attribute names # conversion from exported names to internal attribute names
accessiblename2attr = {} accessiblename2attr = {}
for aname, aobj in self.accessibles.items(): for aname, aobj in self.accessibles.items():
@ -158,31 +281,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
if isinstance(aobj, Parameter): if isinstance(aobj, Parameter):
# fix default properties poll and needscfg # fix default properties poll and needscfg
if aobj.poll is None: if aobj.poll is None:
aobj.properties['poll'] = bool(aobj.handler) aobj.poll = bool(aobj.handler)
if aobj.needscfg is None: if aobj.needscfg is None:
aobj.properties['needscfg'] = not aobj.poll aobj.needscfg = not aobj.poll
if not self.export: # do not export parameters of a module not exported if not self.export: # do not export parameters of a module not exported
aobj.properties['export'] = False aobj.export = False
if aobj.export: if aobj.export:
if aobj.export is True: if aobj.export is True:
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None) predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
if predefined_obj: if predefined_obj:
if isinstance(aobj, predefined_obj): if isinstance(aobj, predefined_obj):
aobj.setProperty('export', aname) aobj.export = aname
else: else:
raise ProgrammingError("can not use '%s' as name of a %s" % raise ProgrammingError("can not use '%s' as name of a %s" %
(aname, aobj.__class__.__name__)) (aname, aobj.__class__.__name__))
else: # create custom parameter else: # create custom parameter
aobj.setProperty('export', '_' + aname) aobj.export = '_' + aname
accessiblename2attr[aobj.export] = aname accessiblename2attr[aobj.export] = aname
accessibles[aname] = aobj accessibles[aname] = aobj
# do not re-use self.accessibles as this is the same for all instances # do not re-use self.accessibles as this is the same for all instances
self.accessibles = accessibles self.accessibles = accessibles
self.accessiblename2attr = accessiblename2attr self.accessiblename2attr = accessiblename2attr
# provide properties to 'filter' out the parameters/commands # provide properties to 'filter' out the parameters/commands
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter)) self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command)) self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
# 2) check and apply parameter_properties # 2) check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>' # specified as '<paramname>.<propertyname> = <propertyvalue>'
@ -199,6 +322,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
else: else:
raise ConfigError('Module %s: Parameter %r has no property %r!' % raise ConfigError('Module %s: Parameter %r has no property %r!' %
(self.name, paramname, propname)) (self.name, paramname, propname))
else:
raise ConfigError('Module %s has no Parameter %r!' %
(self.name, paramname))
# 3) check config for problems: # 3) check config for problems:
# only accept remaining config items specified in parameters # only accept remaining config items specified in parameters
@ -208,7 +334,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
'Module %s:config Parameter %r ' 'Module %s:config Parameter %r '
'not understood! (use one of %s)' % 'not understood! (use one of %s)' %
(self.name, k, ', '.join(list(self.parameters) + (self.name, k, ', '.join(list(self.parameters) +
list(self.__class__.properties)))) list(self.propertyDict))))
# 4) complain if a Parameter entry has no default value and # 4) complain if a Parameter entry has no default value and
# is not specified in cfgdict and deal with parameters to be written. # is not specified in cfgdict and deal with parameters to be written.
@ -220,6 +346,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
if pname in cfgdict: if pname in cfgdict:
if not pobj.readonly and pobj.initwrite is not False: if not pobj.readonly and pobj.initwrite is not False:
# parameters given in cfgdict have to call write_<pname> # parameters given in cfgdict have to call write_<pname>
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
try: try:
pobj.value = pobj.datatype(cfgdict[pname]) pobj.value = pobj.datatype(cfgdict[pname])
except BadValueError as e: except BadValueError as e:
@ -228,7 +355,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
else: else:
if pobj.default is None: if pobj.default is None:
if pobj.needscfg: if pobj.needscfg:
raise ConfigError('Module %s: Parameter %r has no default ' raise ConfigError('Parameter %s.%s has no default '
'value and was not given in config!' % 'value and was not given in config!' %
(self.name, pname)) (self.name, pname))
# we do not want to call the setter for this parameter for now, # we do not want to call the setter for this parameter for now,
@ -243,9 +370,10 @@ class Module(HasProperties, metaclass=ModuleMeta):
except BadValueError as e: except BadValueError as e:
raise ProgrammingError('bad default for %s.%s: %s' raise ProgrammingError('bad default for %s.%s: %s'
% (name, pname, e)) % (name, pname, e))
if pobj.initwrite: if pobj.initwrite and not pobj.readonly:
# we will need to call write_<pname> # we will need to call write_<pname>
# if this is not desired, the default must not be given # if this is not desired, the default must not be given
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
pobj.value = value pobj.value = value
self.writeDict[pname] = value self.writeDict[pname] = value
else: else:
@ -312,7 +440,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
for cb in cblist: for cb in cblist:
try: try:
cb(arg) cb(arg)
except Exception as e: except Exception:
# print(formatExtendedTraceback()) # print(formatExtendedTraceback())
pass pass
@ -358,38 +486,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
modobj.announceUpdate(p, value) modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb) self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None): def isBusy(self, status=None):
"""helper function for treating substates of BUSY correctly""" """helper function for treating substates of BUSY correctly"""
# defined even for non drivable (used for dynamic polling) # defined even for non drivable (used for dynamic polling)
return False return False
def earlyInit(self): def earlyInit(self):
"""may be overriden in derived classes to init stuff # may be overriden in derived classes to init stuff
after creating the module (no super call needed)
"""
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__) self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
def initModule(self): def initModule(self):
"""may be overriden to do stuff after all modules are intiialized
no super call needed
"""
self.log.debug('empty %s.initModule()' % self.__class__.__name__) self.log.debug('empty %s.initModule()' % self.__class__.__name__)
def startModule(self, started_callback):
"""runs after init of all modules
:param started_callback: argument less function to be called when the thread
spawned by startModule has finished its initial work
:return: None or a timeout value, if different from default (30 sec)
override this method for doing stuff during startup, after all modules are
initialized. do not forget the super call
"""
mkthread(self.writeInitParams, started_callback)
def pollOneParam(self, pname): def pollOneParam(self, pname):
"""poll parameter <pname> with proper error handling""" """poll parameter <pname> with proper error handling"""
try: try:
@ -421,33 +529,33 @@ class Module(HasProperties, metaclass=ModuleMeta):
if started_callback: if started_callback:
started_callback() started_callback()
def startModule(self, started_callback):
"""runs after init of all modules
started_callback to be called when the thread spawned by startModule
has finished its initial work
might return a timeout value, if different from default
"""
mkthread(self.writeInitParams, started_callback)
class Readable(Module): class Readable(Module):
"""basic readable module""" """basic readable module"""
# pylint: disable=invalid-name # pylint: disable=invalid-name
Status = Enum('Status', Status = Enum('Status',
IDLE = 100, IDLE=100,
WARN = 200, WARN=200,
UNSTABLE = 270, UNSTABLE=270,
ERROR = 400, ERROR=400,
DISABLED = 0, DISABLED=0,
UNKNOWN = 401, UNKNOWN=401,
) #: status codes ) #: status codes
parameters = {
'value': Parameter('current value of the Module', readonly=True, value = Parameter('current value of the module', FloatRange(), poll=True)
datatype=FloatRange(), status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
poll=True, default=(Status.IDLE, ''), poll=True)
), pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
'pollinterval': Parameter('sleeptime between polls', default=5, default=5, readonly=False)
readonly=False,
datatype=FloatRange(0.1, 120),
),
'status': Parameter('*(rd, tuple of (Readable.Status, str))* current status of the Module',
default=(Status.IDLE, ''),
datatype=TupleOf(EnumType(Status), StringType()),
readonly=True, poll=True,
),
}
def startModule(self, started_callback): def startModule(self, started_callback):
"""start basic polling thread""" """start basic polling thread"""
@ -496,30 +604,17 @@ class Readable(Module):
class Writable(Readable): class Writable(Readable):
"""basic writable module""" """basic writable module"""
parameters = {
'target': Parameter('target value of the Module', target = Parameter('target value of the module',
default=0, readonly=False, datatype=FloatRange(), default=0, readonly=False, datatype=FloatRange())
),
}
class Drivable(Writable): class Drivable(Writable):
"""basic drivable module""" """basic drivable module"""
Status = Enum(Readable.Status, BUSY=300) #: Status codes Status = Enum(Readable.Status, BUSY=300) #: status codes
commands = { status = Parameter(datatype=StatusType(Status)) # override Readable.status
'stop': Command(
'cease driving, go to IDLE state',
argument=None,
result=None
),
}
overrides = {
'status': Override('*(rd, tuple of (Drivable.Status, str))* current status of the Module',
datatype=StatusType(Status)),
}
def isBusy(self, status=None): def isBusy(self, status=None):
"""check for busy, treating substates correctly """check for busy, treating substates correctly
@ -533,7 +628,6 @@ class Drivable(Writable):
returns True when busy, but not finalizing returns True when busy, but not finalizing
""" """
""""""
return 300 <= (status or self.status)[0] < 390 return 300 <= (status or self.status)[0] < 390
# improved polling: may poll faster if module is BUSY # improved polling: may poll faster if module is BUSY
@ -554,26 +648,16 @@ class Drivable(Writable):
self.pollOneParam(pname) self.pollOneParam(pname)
return fastpoll return fastpoll
def do_stop(self): @Command(None, result=None)
# default implementation of the stop command def stop(self):
# by default does nothing """cease driving, go to IDLE state"""
pass
class Communicator(Module): class Communicator(Module):
"""basic communication module """basic abstract communication module"""
providing no parameters, but a 'communicate' command. @Command(StringType(), result=StringType())
""" def communicate(self, command):
commands = {
"communicate": Command("provides the simplest mean to communication",
argument=StringType(),
result=StringType()
),
}
def do_communicate(self, command):
"""communicate command """communicate command
:param command: the command to be sent :param command: the command to be sent
@ -583,7 +667,7 @@ class Communicator(Module):
class Attached(Property): class Attached(Property):
"""a special property, defining an attached module """a special property, defining an attached modle
assign a module name to this property in the cfg file, assign a module name to this property in the cfg file,
and the server will create an attribute with this module and the server will create an attribute with this module
@ -594,7 +678,8 @@ class Attached(Property):
# we can not put this to properties.py, as it needs datatypes # we can not put this to properties.py, as it needs datatypes
def __init__(self, attrname=None): def __init__(self, attrname=None):
self.attrname = attrname self.attrname = attrname
super().__init__('attached module', StringType()) # we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
super().__init__('attached module', StringType(), mandatory=False)
def __repr__(self): def __repr__(self):
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '') return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')

View File

@ -23,154 +23,243 @@
"""Define classes for Parameters/Commands and Overriding them""" """Define classes for Parameters/Commands and Overriding them"""
from collections import OrderedDict import inspect
from inspect import cleandoc
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \ from secop.datatypes import BoolType, CommandType, DataType, \
NoneOr, TextType, IntRange DataTypeType, EnumType, IntRange, NoneOr, OrType, \
from secop.errors import ProgrammingError, BadValueError StringType, StructOf, TextType, TupleOf, ValueType
from secop.errors import BadValueError, ProgrammingError
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
UNSET = object() # an argument not given, not even None
class CountedObj:
ctr = [0]
def __init__(self):
cl = self.__class__.ctr
cl[0] += 1
self.ctr = cl[0]
class Accessible(HasProperties, CountedObj): class Accessible(HasProperties):
'''base class for Parameter and Command''' """base class for Parameter and Command"""
properties = {} kwds = None # is a dict if it might be used as Override
def __init__(self, **kwds): def __init__(self, **kwds):
super(Accessible, self).__init__() super().__init__()
# do not use self.properties.update here, as no invalid values should be self.init(kwds)
def init(self, kwds):
# do not use self.propertyValues.update here, as no invalid values should be
# assigned to properties, even not before checkProperties # assigned to properties, even not before checkProperties
for k,v in kwds.items(): for k, v in kwds.items():
self.setProperty(k, v) self.setProperty(k, v)
def __repr__(self): def inherit(self, cls, owner):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join( for base in owner.__bases__:
['%s=%r' % (k, self.properties.get(k, v.default)) for k, v in sorted(self.__class__.properties.items())])) if hasattr(base, self.name):
aobj = getattr(base, 'accessibles', {}).get(self.name)
if aobj:
if not isinstance(aobj, cls):
raise ProgrammingError('%s %s.%s can not inherit from a %s' %
(cls.__name__, owner.__name__, self.name, aobj.__class__.__name__))
# inherit from aobj
for pname, value in aobj.propertyValues.items():
if pname not in self.propertyValues:
self.propertyValues[pname] = value
break
def as_dict(self):
return self.propertyValues
def override(self, value=UNSET, **kwds):
"""return a copy, overridden by a bare attribute
and/or some properties"""
raise NotImplementedError
def copy(self): def copy(self):
# return a copy of ourselfs """return a (deep) copy of ourselfs"""
props = dict(self.properties, ctr=self.ctr) raise NotImplementedError
# deep copy, as datatype might be altered from config
props['datatype'] = props['datatype'].copy()
return type(self)(**props)
def for_export(self): def for_export(self):
"""prepare for serialisation""" """prepare for serialisation"""
return self.exportProperties() raise NotImplementedError
def __repr__(self):
props = []
for k, prop in sorted(self.propertyDict.items()):
v = self.propertyValues.get(k, prop.default)
if v != prop.default:
props.append('%s=%r' % (k, v))
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
class Parameter(Accessible): class Parameter(Accessible):
"""storage for parameter settings + value + qualifiers""" """defines a parameter
# poll: meaning for the basicPoller:
# - True or 1 (poll this every pollinterval)
# - positive int (poll every N(th) pollinterval)
# - negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval)
# note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
properties = { :param description: description
'description': Property('mandatory description of the parameter', TextType(), :param datatype: the datatype
extname='description', mandatory=True), :param inherit: whether properties not given should be inherited
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(), :param kwds: optional properties
extname='datainfo', mandatory=True), """
'readonly': Property('not changeable via SECoP (default True)', BoolType(), # storage for Parameter settings + value + qualifiers
extname='readonly', mandatory=True),
'group': Property('optional parameter group this parameter belongs to', StringType(),
extname='group', default=''),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', default=1),
'constant': Property('optional constant value for constant parameters', ValueType(),
extname='constant', default=None, mandatory=False),
'default': Property('[internal] default (startup) value of this parameter '
'if it can not be read from the hardware.',
ValueType(), export=False, default=None, mandatory=False),
'export': Property('''
[internal] export settings
* False: not accessible via SECoP.
* True: exported, name automatic.
* a string: exported with custom name''',
OrType(BoolType(), StringType()), export=False, default=True),
'poll': Property('''
[internal] polling indicator
may be:
* None (omitted): will be converted to True/False if handler is/is not None
* False or 0 (never poll this parameter)
* True or 1 (AUTO), converted to SLOW (readonly=False)
DYNAMIC (*status* and *value*) or REGULAR (else)
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
* 3 (REGULAR), polled with pollperiod
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
else polled with pollperiod
''',
NoneOr(IntRange()), export=False, default=None),
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
settable=False, default=False),
'handler': Property('[internal] overload the standard read and write functions',
ValueType(), export=False, default=None, mandatory=False, settable=False),
'initwrite': Property('[internal] write this parameter on initialization'
' (default None: write if given in config)',
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False),
}
def __init__(self, description, datatype, *, ctr=None, unit=None, **kwds): description = Property(
'mandatory description of the parameter', TextType(),
extname='description', mandatory=True)
datatype = Property(
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
extname='datainfo', mandatory=True)
readonly = Property(
'not changeable via SECoP (default True)', BoolType(),
extname='readonly', default=True, export='always')
group = Property(
'optional parameter group this parameter belongs to', StringType(),
extname='group', default='')
visibility = Property(
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', default=1)
constant = Property(
'optional constant value for constant parameters', ValueType(),
extname='constant', default=None)
default = Property(
'''[internal] default (startup) value of this parameter
if ctr is not None: if it can not be read from the hardware''', ValueType(),
self.ctr = ctr export=False, default=None)
export = Property(
'''[internal] export settings
if not isinstance(datatype, DataType): * False: not accessible via SECoP.
if issubclass(datatype, DataType): * True: exported, name automatic.
# goodie: make an instance from a class (forgotten ()???) * a string: exported with custom name''', OrType(BoolType(), StringType()),
datatype = datatype() export=False, default=True)
else: poll = Property(
raise ProgrammingError( '''[internal] polling indicator
'datatype MUST be derived from class DataType!')
kwds['description'] = cleandoc(description) may be:
kwds['datatype'] = datatype
kwds['readonly'] = kwds.get('readonly', True) # for frappy optional, for SECoP mandatory
if unit is not None: # for legacy code only
datatype.setProperty('unit', unit)
super(Parameter, self).__init__(**kwds)
if self.initwrite and self.readonly: * None (omitted): will be converted to True/False if handler is/is not None
raise ProgrammingError('can not have both readonly and initwrite!') * False or 0 (never poll this parameter)
* True or 1 (AUTO), converted to SLOW (readonly=False)
DYNAMIC (*status* and *value*) or REGULAR (else)
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
* 3 (REGULAR), polled with pollperiod
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
else polled with pollperiod
''', NoneOr(IntRange()),
export=False, default=None)
needscfg = Property(
'[internal] needs value in config', NoneOr(BoolType()),
export=False, default=None)
optional = Property(
'[internal] is this parameter optional?', BoolType(),
export=False, settable=False, default=False)
handler = Property(
'[internal] overload the standard read and write functions', ValueType(),
export=False, default=None, settable=False)
initwrite = Property(
'''[internal] write this parameter on initialization
if self.constant is not None: default None: write if given in config''', NoneOr(BoolType()),
self.properties['readonly'] = True export=False, default=None, settable=False)
# used on the instance copy only
value = None
timestamp = 0
readerror = None
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
super().__init__(**kwds)
if datatype is not None:
if not isinstance(datatype, DataType):
if isinstance(datatype, type) and issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
datatype = datatype()
else:
raise ProgrammingError(
'datatype MUST be derived from class DataType!')
self.datatype = datatype
if 'default' in kwds:
self.default = datatype(kwds['default'])
if description is not None:
self.description = inspect.cleandoc(description)
# save for __set_name__
self._inherit = inherit
self._unit = unit # for legacy code only
self._constant = constant
def __get__(self, instance, owner):
# not used yet
if instance is None:
return self
return instance.parameters[self.name].value
def __set__(self, obj, value):
obj.announceUpdate(self.name, value)
def __set_name__(self, owner, name):
self.name = name
if self._inherit:
self.inherit(Parameter, owner)
# check for completeness
missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues]
if missing_properties:
raise ProgrammingError('Parameter %s.%s needs a %s' %
(owner.__name__, name, ' and a '.join(missing_properties)))
if self._unit is not None:
self.datatype.setProperty('unit', self._unit)
if self._constant is not None:
constant = self.datatype(self._constant)
# The value of the `constant` property should be the # The value of the `constant` property should be the
# serialised version of the constant, or unset # serialised version of the constant, or unset
constant = self.datatype(kwds['constant']) self.constant = self.datatype.export_value(constant)
self.properties['constant'] = self.datatype.export_value(constant) self.readonly = True
# internal caching: value and timestamp of last change... if 'default' in self.propertyValues:
self.value = self.default # fixes in case datatype has changed
self.timestamp = 0 try:
self.readerror = None # if not None, indicates that last read was not successful self.datatype(self.default)
except BadValueError:
# clear default, if it does not match datatype
self.propertyValues.pop('default')
if self.export is True:
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
self.export = name
else:
self.export = '_' + name
def copy(self):
# deep copy, as datatype might be altered from config
res = Parameter()
res.name = self.name
res.init(self.propertyValues)
res.datatype = res.datatype.copy()
return res
def override(self, value=UNSET, **kwds):
res = self.copy()
res.init(kwds)
if value is not UNSET:
res.value = res.datatype(value)
return res
def export_value(self): def export_value(self):
return self.datatype.export_value(self.value) return self.datatype.export_value(self.value)
def for_export(self):
return dict(self.exportProperties(), readonly=self.readonly)
def getProperties(self): def getProperties(self):
"""get also properties of datatype""" """get also properties of datatype"""
superProp = super().getProperties().copy() super_prop = super().getProperties().copy()
superProp.update(self.datatype.getProperties()) super_prop.update(self.datatype.getProperties())
return superProp return super_prop
def setProperty(self, key, value): def setProperty(self, key, value):
"""set also properties of datatype""" """set also properties of datatype"""
if key in self.__class__.properties: if key in self.propertyDict:
super().setProperty(key, value) super().setProperty(key, value)
else: else:
self.datatype.setProperty(key, value) self.datatype.setProperty(key, value)
@ -179,158 +268,168 @@ class Parameter(Accessible):
super().checkProperties() super().checkProperties()
self.datatype.checkProperties() self.datatype.checkProperties()
def for_export(self):
"""prepare for serialisation
readonly is mandatory for serialisation, but not for declaration in classes
"""
r = super().for_export()
if 'readonly' not in r:
r['readonly'] = self.__class__.properties['readonly'].default
return r
class UnusedClass:
# do not derive anything from this!
pass
class Parameters(OrderedDict):
"""class storage for Parameters"""
def __init__(self, *args, **kwds):
self.exported = {} # only for lookups!
super(Parameters, self).__init__(*args, **kwds)
def __setitem__(self, key, value):
if value.export:
if isinstance(value, PREDEFINED_ACCESSIBLES.get(key, UnusedClass)):
value.properties['export'] = key
else:
value.properties['export'] = '_' + key
self.exported[value.export] = key
super(Parameters, self).__setitem__(key, value)
def __getitem__(self, item):
return super(Parameters, self).__getitem__(self.exported.get(item, item))
class ParamValue:
__slots__ = ['value', 'timestamp']
def __init__(self, value, timestamp=0):
self.value = value
self.timestamp = timestamp
class Commands(Parameters):
"""class storage for Commands"""
class Override(CountedObj):
"""Stores the overrides to be applied to a Parameter or Command
note: overrides are applied by the metaclass during class creating
reorder=True: use position of Override instead of inherited for the order
"""
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
super(Override, self).__init__()
self.kwds = kwds
self.reorder = reorder
# allow to override description and datatype without keyword
if description:
self.kwds['description'] = cleandoc(description)
if datatype is not None:
self.kwds['datatype'] = datatype
# for now, do not use the Override ctr
# self.kwds['ctr'] = self.ctr
def __repr__(self):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
def apply(self, obj):
if isinstance(obj, Accessible):
props = obj.properties.copy()
props['datatype'] = props['datatype'].copy()
if isinstance(obj, Parameter):
if 'constant' in self.kwds:
constant = obj.datatype(self.kwds.pop('constant'))
self.kwds['constant'] = obj.datatype.export_value(constant)
self.kwds['readonly'] = True
if 'datatype' in self.kwds and 'default' not in self.kwds:
try:
self.kwds['datatype'](obj.default)
except BadValueError:
# clear default, if it does not match datatype
props['default'] = None
props.update(self.kwds)
if self.reorder:
return type(obj)(**props)
return type(obj)(ctr=self.ctr, **props)
raise ProgrammingError(
"Overrides can only be applied to Accessibles, %r is none!" % obj)
class Command(Accessible): class Command(Accessible):
"""storage for Commands settings (description + call signature...) """decorator to turn a method into a command
:param argument: the datatype of the argument or None
:param result: the datatype of the result or None
:param inherit: whether properties not given should be inherited
:param kwds: optional properties
""" """
properties = {
'description': Property('description of the command', TextType(),
extname='description', export=True, mandatory=True),
'group': Property('optional command group of the command.', StringType(),
extname='group', export=True, default=''),
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', export=True, default=1),
'export': Property('''
[internal] export settings
- False: not accessible via SECoP.
- True: exported, name automatic.
- a string: exported with custom name''',
OrType(BoolType(), StringType()), export=False, default=True),
'optional': Property('[internal] is the command optional to implement? (vs. mandatory)',
BoolType(), export=False, default=False, settable=False),
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
DataTypeType(), extname='datainfo', mandatory=True),
'argument': Property('datatype of the argument to the command, or None.',
NoneOr(DataTypeType()), export=False, mandatory=True),
'result': Property('datatype of the result from the command, or None.',
NoneOr(DataTypeType()), export=False, mandatory=True),
}
def __init__(self, description, ctr=None, **kwds): description = Property(
kwds['description'] = cleandoc(description) 'description of the Command', TextType(),
kwds['datatype'] = CommandType(kwds.get('argument', None), kwds.get('result', None)) extname='description', export=True, mandatory=True)
super(Command, self).__init__(**kwds) group = Property(
if ctr is not None: 'optional command group of the command.', StringType(),
self.ctr = ctr extname='group', export=True, default='')
visibility = Property(
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
extname='visibility', export=True, default=1)
export = Property(
'''[internal] export settings
@property * False: not accessible via SECoP.
def argument(self): * True: exported, name automatic.
return self.datatype.argument * a string: exported with custom name''', OrType(BoolType(), StringType()),
export=False, default=True)
optional = Property(
'[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
export=False, default=False, settable=False)
datatype = Property(
"datatype of the command, auto generated from 'argument' and 'result'",
DataTypeType(), extname='datainfo', export='always')
argument = Property(
'datatype of the argument to the command, or None', NoneOr(DataTypeType()),
export=False, mandatory=True)
result = Property(
'datatype of the result from the command, or None', NoneOr(DataTypeType()),
export=False, mandatory=True)
@property func = None
def result(self):
return self.datatype.result def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
super().__init__(**kwds)
if result or kwds or isinstance(argument, DataType) or not callable(argument):
# normal case
if argument is False and result:
argument = None
if argument is not False:
if isinstance(argument, (tuple, list)):
# goodie: treat as TupleOf
argument = TupleOf(*argument)
self.argument = argument
self.result = result
else:
# goodie: allow @Command instead of @Command()
self.func = argument # this is the wrapped method!
if argument.__doc__:
self.description = inspect.cleandoc(argument.__doc__)
self.name = self.func.__name__
self._inherit = inherit # save for __set_name__
def __set_name__(self, owner, name):
self.name = name
if self.func is None:
raise ProgrammingError('Command %s.%s must be used as a method decorator' %
(owner.__name__, name))
if self._inherit:
self.inherit(Command, owner)
self.datatype = CommandType(self.argument, self.result)
if self.export is True:
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
self.export = name
else:
self.export = '_' + name
def __get__(self, obj, owner=None):
if obj is None:
return self
if not self.func:
raise ProgrammingError('Command %s not properly configured' % self.name)
return self.func.__get__(obj, owner)
def __call__(self, func):
if 'description' not in self.propertyValues and func.__doc__:
self.description = inspect.cleandoc(func.__doc__)
self.func = func
return self
def copy(self):
res = Command()
res.name = self.name
res.func = self.func
res.init(self.propertyValues)
if res.argument:
res.argument = res.argument.copy()
if res.result:
res.result = res.result.copy()
res.datatype = CommandType(res.argument, res.result)
return res
def override(self, value=UNSET, **kwds):
res = self.copy()
res.init(kwds)
if value is not UNSET:
res.func = value
return res
def do(self, module_obj, argument):
"""perform function call
:param module_obj: the module on which the command is to be executed
:param argument: the argument from the do command
:returns: the return value converted to the result type
- when the argument type is TupleOf, the function is called with multiple arguments
- when the argument type is StructOf, the function is called with keyworded arguments
- the validity of the argument/s is/are checked
"""
func = self.__get__(module_obj)
if self.argument:
# validate
argument = self.argument(argument)
if isinstance(self.argument, TupleOf):
res = func(*argument)
elif isinstance(self.argument, StructOf):
res = func(**argument)
else:
res = func(argument)
else:
if argument is not None:
raise BadValueError('%s.%s takes no arguments' % (module_obj.__class__.__name__, self.name))
res = func()
if self.result:
return self.result(res)
return None # silently ignore the result from the method
def for_export(self):
return self.exportProperties()
def __repr__(self):
result = super().__repr__()
return result[:-1] + ', %r)' % self.func if self.func else result
# list of predefined accessibles with their type # list of predefined accessibles with their type
PREDEFINED_ACCESSIBLES = dict( PREDEFINED_ACCESSIBLES = dict(
value = Parameter, value=Parameter,
status = Parameter, status=Parameter,
target = Parameter, target=Parameter,
pollinterval = Parameter, pollinterval=Parameter,
ramp = Parameter, ramp=Parameter,
user_ramp = Parameter, user_ramp=Parameter,
setpoint = Parameter, setpoint=Parameter,
time_to_target = Parameter, time_to_target=Parameter,
unit = Parameter, # reserved name unit=Parameter, # reserved name
loglevel = Parameter, # reserved name loglevel=Parameter, # reserved name
mode = Parameter, # reserved name mode=Parameter, # reserved name
stop = Command, stop=Command,
reset = Command, reset=Command,
go = Command, go=Command,
abort = Command, abort=Command,
shutdown = Command, shutdown=Command,
communicate = Command, communicate=Command,
) )

View File

@ -34,10 +34,11 @@ Usage examples:
""" """
import time import time
from threading import Event
from heapq import heapify, heapreplace from heapq import heapify, heapreplace
from secop.lib import mkthread from threading import Event
from secop.errors import ProgrammingError from secop.errors import ProgrammingError
from secop.lib import mkthread
# poll types: # poll types:
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
@ -166,8 +167,8 @@ class Poller(PollerBase):
continue # only one poller per handler continue # only one poller per handler
handlers.add(pobj.handler) handlers.add(pobj.handler)
# placeholders 0 are used for due, lastdue and idx # placeholders 0 are used for due, lastdue and idx
self.queues[polltype].append((0, 0, self.queues[polltype].append(
(0, module, pobj, pname, factors[polltype]))) (0, 0, (0, module, pobj, pname, factors[polltype])))
def poll_next(self, polltype): def poll_next(self, polltype):
"""try to poll next item """try to poll next item

View File

@ -23,11 +23,44 @@
"""Define validated data types.""" """Define validated data types."""
from collections import OrderedDict import inspect
from inspect import cleandoc import sys
from secop.errors import ProgrammingError, ConfigError, BadValueError from secop.errors import BadValueError, ConfigError, ProgrammingError
from secop.lib.classdoc import append_to_doc, indent_description
class HasDescriptorMeta(type):
def __new__(cls, name, bases, attrs):
newtype = type.__new__(cls, name, bases, attrs)
if sys.version_info < (3, 6):
# support older python versions
for key, attr in attrs.items():
if hasattr(attr, '__set_name__'):
attr.__set_name__(newtype, key)
newtype.__init_subclass__()
return newtype
class HasDescriptors(metaclass=HasDescriptorMeta):
@classmethod
def __init_subclass__(cls):
# when migrating old style declarations, sometimes the trailing comma is not removed
bad = [k for k, v in cls.__dict__.items()
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
if bad:
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
@classmethod
def filterDescriptors(cls, filter_type):
res = {}
for name in dir(cls):
desc = getattr(cls, name, None)
if isinstance(desc, filter_type):
res[name] = desc
return res
UNSET = object() # an unset value, not even None
# storage for 'properties of a property' # storage for 'properties of a property'
@ -35,175 +68,148 @@ class Property:
"""base class holding info about a property """base class holding info about a property
:param description: mandatory :param description: mandatory
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed, :param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed!
also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc. also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc.
:param default: a default value. SECoP properties are normally not sent to the ECS, :param default: a default value. SECoP properties are normally not sent to the ECS,
when they match the default when they match the default
:param extname: external name :param extname: external name
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given :param export: sent to the ECS when True. defaults to True, when ``extname`` is given.
special value 'always': export also when matching the default
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value :param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute) assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
:param settable: settable from the cfg file :param settable: settable from the cfg file
""" """
# note: this is intended to be used on base classes. # note: this is intended to be used on base classes.
# the VALUES of the properties are on the instances! # the VALUES of the properties are on the instances!
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True): def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None,
settable=True, value=UNSET, name=''):
if not callable(datatype): if not callable(datatype):
raise ValueError('datatype MUST be a valid DataType or a basic_validator') raise ValueError('datatype MUST be a valid DataType or a basic_validator')
self.description = cleandoc(description) self.description = inspect.cleandoc(description)
self.default = datatype.default if default is None else datatype(default) self.default = datatype.default if default is UNSET else datatype(default)
self.datatype = datatype self.datatype = datatype
self.extname = extname self.extname = extname
self.export = export or bool(extname) self.export = export or bool(extname)
if mandatory is None: if mandatory is None:
mandatory = default is None mandatory = default is UNSET
self.mandatory = mandatory self.mandatory = mandatory
self.settable = settable or mandatory # settable means settable from the cfg file self.settable = settable or mandatory # settable means settable from the cfg file
self.value = UNSET if value is UNSET else datatype(value)
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.propertyValues.get(self.name, self.default)
def __set__(self, instance, value):
instance.propertyValues[self.name] = self.datatype(value)
def __set_name__(self, owner, name):
self.name = name
if self.export and not self.extname:
self.extname = '_' + name
if self.description == '_':
# the programmer indicates, that the name is already speaking for itself
self.description = name.replace('_', ' ')
def __repr__(self): def __repr__(self):
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % ( extras = ['default=%s' % repr(self.default)]
self.description, self.datatype, self.default, self.extname, self.export, if self.export:
self.mandatory, self.settable) extras.append('extname=%r' % self.extname)
extras.append('export=%r' % self.export)
if self.mandatory:
extras.append('mandatory=True')
if not self.settable:
extras.append('settable=False')
if self.value is not UNSET:
extras.append('value=%s' % repr(self.value))
if not self.name:
extras.append('name=%r' % self.name)
return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras))
class Properties(OrderedDict): class HasProperties(HasDescriptors):
"""a collection of `Property` objects propertyValues = None
checks values upon assignment.
You can either assign a Property object, or a value
(which must pass the validator of the already existing Property)
"""
def __setitem__(self, key, value):
if not isinstance(value, Property):
raise ProgrammingError('setting property %r on classes is not supported!' % key)
# make sure, extname is valid if export is True
if not value.extname and value.export:
value.extname = '_%s' % key # generate custom key
elif value.extname and not value.export:
value.export = True
OrderedDict.__setitem__(self, key, value)
def __delitem__(self, key):
raise ProgrammingError('deleting Properties is not supported!')
class PropertyMeta(type):
"""Metaclass for HasProperties
joining the class's properties with those of base classes.
"""
def __new__(cls, name, bases, attrs):
newtype = type.__new__(cls, name, bases, attrs)
if '__constructed__' in attrs:
return newtype
newtype = cls.__join_properties__(newtype, name, bases, attrs)
attrs['__constructed__'] = True
return newtype
@classmethod
def __join_properties__(cls, newtype, name, bases, attrs):
# merge properties from all sub-classes
properties = Properties()
for base in reversed(bases):
properties.update(getattr(base, "properties", {}))
# update with properties from new class
properties.update(attrs.get('properties', {}))
newtype.properties = properties
# generate getters
for k, po in properties.items():
def getter(self, pname=k):
val = self.__class__.properties[pname].default
return self.properties.get(pname, val)
if k in attrs and not isinstance(attrs[k], property):
if callable(attrs[k]):
raise ProgrammingError('%r: property %r collides with method'
% (newtype, k))
# store the attribute value for putting on the instance later
try:
# for inheritance reasons, it seems best to store it as a renamed attribute
setattr(newtype, '_initProp_' + k, po.datatype(attrs[k]))
except BadValueError:
raise ProgrammingError('%r: property %r can not be set to %r'
% (newtype, k, attrs[k]))
setattr(newtype, k, property(getter))
# add property information to the doc string
def fmt_property(name, prop):
desc = indent_description(prop)
if '(' in desc[0:2]:
dtinfo = ''
else:
dtinfo = [prop.datatype.short_doc(), None if prop.export else 'hidden']
dtinfo = ', '.join(filter(None, dtinfo))
if dtinfo:
dtinfo = '*(%s)* ' % dtinfo
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
append_to_doc(newtype, 'properties', 'SECOP Properties',
'properties', attrs.get("properties", {}), fmt_property)
return newtype
class HasProperties(metaclass=PropertyMeta):
properties = {}
def __init__(self): def __init__(self):
super(HasProperties, self).__init__() super(HasProperties, self).__init__()
self.initProperties()
def initProperties(self):
# store property values in the instance, keep descriptors on the class # store property values in the instance, keep descriptors on the class
self.properties = {} self.propertyValues = {}
# pre-init with properties default value (if any) # pre-init
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
value = getattr(self, '_initProp_' + pn, self) if po.value is not UNSET:
if value is not self: # property value was given as attribute self.setProperty(pn, po.value)
self.properties[pn] = value
elif not po.mandatory: @classmethod
self.properties[pn] = po.default def __init_subclass__(cls):
super().__init_subclass__()
# raise an error when an attribute is a tuple with one single descriptor as element
# when migrating old style declarations, sometimes the trailing comma is not removed
bad = [k for k, v in cls.__dict__.items()
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
if bad:
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
properties = {}
for base in cls.__bases__:
properties.update(getattr(base, 'propertyDict', {}))
properties.update(cls.filterDescriptors(Property))
cls.propertyDict = properties
# treat overriding properties with bare values
for pn, po in properties.items():
value = cls.__dict__.get(pn, po)
if not isinstance(value, Property): # attribute is a bare value
po = Property(**po.__dict__)
try:
po.value = po.datatype(value)
except BadValueError:
for base in cls.__bases__:
if pn in getattr(base, 'propertyDict', {}):
if callable(value):
raise ProgrammingError('method %s.%s collides with property of %s' %
(cls.__name__, pn, base.__name__))
raise ProgrammingError('can not set property %s.%s to %r' %
(cls.__name__, pn, value))
cls.propertyDict[pn] = po
def checkProperties(self): def checkProperties(self):
"""validates properties and checks for min... <= max...""" """validates properties and checks for min... <= max..."""
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
if po.export and po.mandatory: if po.mandatory:
if pn not in self.properties: if pn not in self.propertyDict:
name = getattr(self, 'name', repr(self)) name = getattr(self, 'name', self.__class__.__name__)
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype)) raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
# apply validator (which may complain further) # apply validator (which may complain further)
self.properties[pn] = po.datatype(self.properties[pn]) self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
if pn.startswith('min'): if pn.startswith('min'):
maxname = 'max' + pn[3:] maxname = 'max' + pn[3:]
minval = self.properties[pn] minval = self.propertyValues.get(pn, po.default)
maxval = self.properties.get(maxname, minval) maxval = self.propertyValues.get(maxname, minval)
if minval > maxval: if minval > maxval:
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self)) raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
def getProperties(self): def getProperties(self):
return self.__class__.properties return self.propertyDict
def exportProperties(self): def exportProperties(self):
# export properties which have # export properties which have
# export=True and # export=True and
# mandatory=True or non_default=True # mandatory=True or non_default=True
res = {} res = {}
for pn, po in self.__class__.properties.items(): for pn, po in self.propertyDict.items():
val = self.properties.get(pn, None) val = self.propertyValues.get(pn, po.default)
if po.export and (po.mandatory or val != po.default): if po.export and (po.export == 'always' or val != po.default):
try: try:
val = po.datatype.export_value(val) val = po.datatype.export_value(val)
except AttributeError: except AttributeError:
pass # for properties, accept simple datatypes without export_value pass # for properties, accept simple datatypes without export_value
res[po.extname] = val res[po.extname] = val
return res return res
def setProperty(self, key, value): def setProperty(self, key, value):
self.properties[key] = self.__class__.properties[key].datatype(value) # this is overwritten by Param.setProperty and DataType.setProperty
# in oder to extend setting to inner properties
# otherwise direct setting of self.<key> = value is preferred
self.propertyValues[key] = self.propertyDict[key].datatype(value)

View File

@ -42,8 +42,8 @@ import threading
from collections import OrderedDict from collections import OrderedDict
from time import time as currenttime from time import time as currenttime
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \ from secop.errors import NoSuchCommandError, NoSuchModuleError, \
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError, fmt_error NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
from secop.params import Parameter from secop.params import Parameter
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \ from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \ DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
@ -54,9 +54,9 @@ def make_update(modulename, pobj):
if pobj.readerror: if pobj.readerror:
return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export), return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export),
# error-report ! # error-report !
[pobj.readerror.name, fmt_error(pobj.readerror), dict(t=pobj.timestamp)]) [pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)])
return (EVENTREPLY, '%s:%s' % (modulename, pobj.export), return (EVENTREPLY, '%s:%s' % (modulename, pobj.export),
[pobj.export_value(), dict(t=pobj.timestamp)]) [pobj.export_value(), dict(t=pobj.timestamp)])
class Dispatcher: class Dispatcher:
@ -109,7 +109,7 @@ class Dispatcher:
self._subscriptions.setdefault(eventname, set()).add(conn) self._subscriptions.setdefault(eventname, set()).add(conn)
def unsubscribe(self, conn, eventname): def unsubscribe(self, conn, eventname):
if not ':' in eventname: if ':' not in eventname:
# also remove 'more specific' subscriptions # also remove 'more specific' subscriptions
for k, v in self._subscriptions.items(): for k, v in self._subscriptions.items():
if k.startswith('%s:' % eventname): if k.startswith('%s:' % eventname):
@ -177,7 +177,7 @@ class Dispatcher:
result = {'modules': OrderedDict()} result = {'modules': OrderedDict()}
for modulename in self._export: for modulename in self._export:
module = self.get_module(modulename) module = self.get_module(modulename)
if not module.properties.get('export', False): if not module.export:
continue continue
# some of these need rework ! # some of these need rework !
mod_desc = {'accessibles': self.export_accessibles(modulename)} mod_desc = {'accessibles': self.export_accessibles(modulename)}
@ -186,7 +186,7 @@ class Dispatcher:
result['modules'][modulename] = mod_desc result['modules'][modulename] = mod_desc
result['equipment_id'] = self.equipment_id result['equipment_id'] = self.equipment_id
result['firmware'] = 'FRAPPY - The Python Framework for SECoP' result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
result['version'] = '2019.08' result['version'] = '2021.02'
result.update(self.nodeprops) result.update(self.nodeprops)
return result return result
@ -195,40 +195,24 @@ class Dispatcher:
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module %r does not exist' % modulename) raise NoSuchModuleError('Module %r does not exist' % modulename)
cmdname = moduleobj.commands.exported.get(exportedname, None) cname = moduleobj.accessiblename2attr.get(exportedname)
if cmdname is None: cobj = moduleobj.commands.get(cname)
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname)) if cobj is None:
cmdspec = moduleobj.commands[cmdname] raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
if argument is None and cmdspec.datatype.argument is not None:
raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname))
if argument is not None and cmdspec.datatype.argument is None:
raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname))
if cmdspec.datatype.argument:
# validate!
argument = cmdspec.datatype(argument)
# now call func # now call func
# note: exceptions are handled in handle_request, not here! # note: exceptions are handled in handle_request, not here!
func = getattr(moduleobj, 'do_' + cmdname) return cobj.do(moduleobj, argument), dict(t=currenttime())
res = func() if argument is None else func(argument)
# pipe through cmdspec.datatype.result
if cmdspec.datatype.result:
res = cmdspec.datatype.result(res)
return res, dict(t=currenttime())
def _setParameterValue(self, modulename, exportedname, value): def _setParameterValue(self, modulename, exportedname, value):
moduleobj = self.get_module(modulename) moduleobj = self.get_module(modulename)
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module %r does not exist' % modulename) raise NoSuchModuleError('Module %r does not exist' % modulename)
pname = moduleobj.parameters.exported.get(exportedname, None) pname = moduleobj.accessiblename2attr.get(exportedname)
if pname is None: pobj = moduleobj.parameters.get(pname)
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname)) if pobj is None:
pobj = moduleobj.parameters[pname] raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
if pobj.constant is not None: if pobj.constant is not None:
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely" raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
% (modulename, pname)) % (modulename, pname))
@ -252,10 +236,10 @@ class Dispatcher:
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module %r does not exist' % modulename) raise NoSuchModuleError('Module %r does not exist' % modulename)
pname = moduleobj.parameters.exported.get(exportedname, None) pname = moduleobj.accessiblename2attr.get(exportedname)
if pname is None: pobj = moduleobj.parameters.get(pname)
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname)) if pobj is None:
pobj = moduleobj.parameters[pname] raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
if pobj.constant is not None: if pobj.constant is not None:
# really needed? we could just construct a readreply instead.... # really needed? we could just construct a readreply instead....
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.') # raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
@ -321,15 +305,13 @@ class Dispatcher:
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data))) return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
def handle_do(self, conn, specifier, data): def handle_do(self, conn, specifier, data):
# XXX: should this be done asyncron? we could just return the reply in
# that case
modulename, cmd = specifier.split(':', 1) modulename, cmd = specifier.split(':', 1)
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data))) return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
def handle_ping(self, conn, specifier, data): def handle_ping(self, conn, specifier, data):
if data: if data:
raise ProtocolError('ping requests don\'t take data!') raise ProtocolError('ping requests don\'t take data!')
return (HEARTBEATREPLY, specifier, [None, {'t':currenttime()}]) return (HEARTBEATREPLY, specifier, [None, {'t': currenttime()}])
def handle_activate(self, conn, specifier, data): def handle_activate(self, conn, specifier, data):
if data: if data:

View File

@ -25,6 +25,7 @@ import json
EOL = b'\n' EOL = b'\n'
def encode_msg_frame(action, specifier=None, data=None): def encode_msg_frame(action, specifier=None, data=None):
""" encode a msg_triple into an msg_frame, ready to be sent """ encode a msg_triple into an msg_frame, ready to be sent

View File

@ -21,13 +21,12 @@
# ***************************************************************************** # *****************************************************************************
"""provides tcp interface to the SECoP Server""" """provides tcp interface to the SECoP Server"""
import sys
import socket import socket
import socketserver import socketserver
import sys
import threading import threading
import time
from secop.datatypes import StringType, BoolType from secop.datatypes import BoolType, StringType
from secop.errors import SECoPError from secop.errors import SECoPError
from secop.lib import formatException, \ from secop.lib import formatException, \
formatExtendedStack, formatExtendedTraceback formatExtendedStack, formatExtendedTraceback
@ -36,7 +35,6 @@ from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
from secop.protocol.messages import ERRORPREFIX, \ from secop.protocol.messages import ERRORPREFIX, \
HELPREPLY, HELPREQUEST, HelpMessage HELPREPLY, HELPREQUEST, HelpMessage
DEF_PORT = 10767 DEF_PORT = 10767
MESSAGE_READ_SIZE = 1024 MESSAGE_READ_SIZE = 1024
HELP = HELPREQUEST.encode() HELP = HELPREQUEST.encode()
@ -134,7 +132,6 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
if result[0].startswith(ERRORPREFIX) and not detailed_errors: if result[0].startswith(ERRORPREFIX) and not detailed_errors:
# strip extra information # strip extra information
result[2][2].clear() result[2][2].clear()
result[2][2]['t'] = time.time()
self.send_reply(result) self.send_reply(result)
def send_reply(self, data): def send_reply(self, data):

View File

@ -80,7 +80,6 @@ REQUEST2REPLY = {
} }
HelpMessage = """Try one of the following: HelpMessage = """Try one of the following:
'%s' to query protocol version '%s' to query protocol version
'%s' to read the description '%s' to read the description

View File

@ -34,12 +34,12 @@ simplifications:
import time import time
import secop.protocol.dispatcher
import secop.errors
from secop.protocol.messages import DESCRIPTIONREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY,\
READREQUEST, WRITEREQUEST, COMMANDREQUEST
import secop.client import secop.client
import secop.errors
import secop.protocol.dispatcher
from secop.lib.multievent import MultiEvent from secop.lib.multievent import MultiEvent
from secop.protocol.messages import COMMANDREQUEST, DESCRIPTIONREPLY, \
ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, READREQUEST, WRITEREQUEST
class SecopClient(secop.client.SecopClient): class SecopClient(secop.client.SecopClient):

View File

@ -21,21 +21,19 @@
# ***************************************************************************** # *****************************************************************************
"""SECoP proxy modules""" """SECoP proxy modules"""
from secop.params import Parameter, Command from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.modules import Module, Writable, Readable, Drivable
from secop.datatypes import StringType from secop.datatypes import StringType
from secop.errors import BadValueError, \
CommunicationFailedError, ConfigError, make_secop_error
from secop.lib import get_class
from secop.modules import Drivable, Module, Readable, Writable
from secop.params import Command, Parameter
from secop.properties import Property from secop.properties import Property
from secop.stringio import HasIodev from secop.stringio import HasIodev
from secop.lib import get_class
from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError
class ProxyModule(HasIodev, Module): class ProxyModule(HasIodev, Module):
properties = { module = Property('remote module name', datatype=StringType(), default='')
'module':
Property('remote module name', datatype=StringType(), default=''),
}
pollerClass = None pollerClass = None
_consistency_check_done = False _consistency_check_done = False
@ -55,7 +53,7 @@ class ProxyModule(HasIodev, Module):
def initModule(self): def initModule(self):
if not self.module: if not self.module:
self.properties['module'] = self.name self.module = self.name
self._secnode = self._iodev.secnode self._secnode = self._iodev.secnode
self._secnode.register_callback(self.module, self.updateEvent, self._secnode.register_callback(self.module, self.updateEvent,
self.descriptiveDataChange, self.nodeStateChange) self.descriptiveDataChange, self.nodeStateChange)
@ -90,10 +88,10 @@ class ProxyModule(HasIodev, Module):
dt.compatible(pobj.datatype) dt.compatible(pobj.datatype)
except Exception: except Exception:
self.log.warning('remote parameter %s:%s is not fully compatible: %r != %r' self.log.warning('remote parameter %s:%s is not fully compatible: %r != %r'
% (self.module, pname, pobj.datatype, dt)) % (self.module, pname, pobj.datatype, dt))
except Exception: except Exception:
self.log.warning('remote parameter %s:%s has an incompatible datatype: %r != %r' self.log.warning('remote parameter %s:%s has an incompatible datatype: %r != %r'
% (self.module, pname, pobj.datatype, dt)) % (self.module, pname, pobj.datatype, dt))
while cmds: while cmds:
cname, cobj = cmds.popitem() cname, cobj = cmds.popitem()
props = remotecmds.get(cname) props = remotecmds.get(cname)
@ -103,9 +101,9 @@ class ProxyModule(HasIodev, Module):
dt = props['datatype'] dt = props['datatype']
try: try:
cobj.datatype.compatible(dt) cobj.datatype.compatible(dt)
except Exception: except BadValueError:
self.log.warning('remote command %s:%s is not compatible: %r != %r' self.log.warning('remote command %s:%s is not compatible: %r != %r'
% (self.module, pname, pobj.datatype, dt)) % (self.module, cname, cobj.datatype, dt))
# what to do if descriptive data does not match? # what to do if descriptive data does not match?
# we might raise an exception, but this would lead to a reconnection, # we might raise an exception, but this would lead to a reconnection,
# which might not help. # which might not help.
@ -141,14 +139,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
class SecNode(Module): class SecNode(Module):
properties = { uri = Property('uri of a SEC node', datatype=StringType())
'uri':
Property('uri of a SEC node', datatype=StringType()),
}
commands = {
'request':
Command('send a request', argument=StringType(), result=StringType())
}
def earlyInit(self): def earlyInit(self):
self.secnode = SecopClient(self.uri, self.log) self.secnode = SecopClient(self.uri, self.log)
@ -156,8 +147,9 @@ class SecNode(Module):
def startModule(self, started_callback): def startModule(self, started_callback):
self.secnode.spawn_connect(started_callback) self.secnode.spawn_connect(started_callback)
def do_request(self, msg): @Command(StringType(), result=StringType())
"""for test purposes""" def request(self, msg):
"""send a request, for debugging purposes"""
reply = self.secnode.request(*decode_msg(msg.encode('utf-8'))) reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
return encode_msg_frame(*reply).decode('utf-8') return encode_msg_frame(*reply).decode('utf-8')
@ -184,17 +176,12 @@ def proxy_class(remote_class, name=None):
else: else:
raise ConfigError('%r is no SECoP module class' % remote_class) raise ConfigError('%r is no SECoP module class' % remote_class)
parameters = {} attrs = rcls.propertyDict.copy()
commands = {}
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
for aname, aobj in rcls.accessibles.items(): for aname, aobj in rcls.accessibles.items():
if isinstance(aobj, Parameter): if isinstance(aobj, Parameter):
pobj = aobj.copy() pobj = aobj.override(poll=False, handler=None, needscfg=False)
parameters[aname] = pobj attrs[aname] = pobj
pobj.properties['poll'] = False
pobj.properties['handler'] = None
pobj.properties['needscfg'] = False
def rfunc(self, pname=aname): def rfunc(self, pname=aname):
value, _, readerror = self._secnode.getParameter(self.name, pname) value, _, readerror = self._secnode.getParameter(self.name, pname)
@ -216,12 +203,11 @@ def proxy_class(remote_class, name=None):
elif isinstance(aobj, Command): elif isinstance(aobj, Command):
cobj = aobj.copy() cobj = aobj.copy()
commands[aname] = cobj
def cfunc(self, arg=None, cname=aname): def cfunc(self, arg=None, cname=aname):
return self._secnode.execCommand(self.name, cname, arg) return self._secnode.execCommand(self.name, cname, arg)
attrs['do_' + aname] = cfunc attrs[aname] = cobj(cfunc)
else: else:
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class)) raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))

View File

@ -23,13 +23,17 @@
# ***************************************************************************** # *****************************************************************************
"""Define helpers""" """Define helpers"""
import os
from os.path import join, exists, dirname, isdir
import ast import ast
import time
import threading
import configparser import configparser
import os
import threading
import time
from collections import OrderedDict from collections import OrderedDict
from secop.errors import ConfigError
from secop.lib import formatException, get_class, getGeneralConfig
from secop.modules import Attached
try: try:
from daemon import DaemonContext from daemon import DaemonContext
try: try:
@ -39,10 +43,6 @@ try:
except ImportError: except ImportError:
DaemonContext = None DaemonContext = None
from secop.errors import ConfigError
from secop.lib import formatException, get_class, getGeneralConfig
from secop.modules import Attached
from secop.params import PREDEFINED_ACCESSIBLES
try: try:
import systemd.daemon import systemd.daemon
@ -95,7 +95,11 @@ class Server:
merged_cfg = OrderedDict() merged_cfg = OrderedDict()
ambiguous_sections = set() ambiguous_sections = set()
for cfgfile in cfgfiles.split(','): for cfgfile in cfgfiles.split(','):
cfgdict = self.loadCfgFile(cfgfile) if cfgfile.endswith('.cfg') and os.path.exists(cfgfile):
filename = cfgfile
else:
filename = os.path.join(cfg['confdir'], cfgfile + '.cfg')
cfgdict = self.loadCfgFile(filename)
ambiguous_sections |= set(merged_cfg) & set(cfgdict) ambiguous_sections |= set(merged_cfg) & set(cfgdict)
merged_cfg.update(cfgdict) merged_cfg.update(cfgdict)
self.node_cfg = merged_cfg.pop('NODE', {}) self.node_cfg = merged_cfg.pop('NODE', {})
@ -112,22 +116,9 @@ class Server:
if ambiguous_sections: if ambiguous_sections:
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections))) self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
self._cfgfiles = cfgfiles self._cfgfiles = cfgfiles
self._pidfile = join(cfg['piddir'], name + '.pid') self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
def loadCfgFile(self, cfgfile): def loadCfgFile(self, filename):
if not cfgfile.endswith('.cfg'):
cfgfile += '.cfg'
if '/' in cfgfile: # specified as full path
filename = cfgfile if exists(cfgfile) else None
else:
cfg = getGeneralConfig()
for filename in [join(d, cfgfile) for d in cfg['confdir'].split(':')]:
if exists(filename):
break
else:
filename = None
if filename is None:
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, cfg['confdir']))
self.log.debug('Parse config file %s ...' % filename) self.log.debug('Parse config file %s ...' % filename)
result = OrderedDict() result = OrderedDict()
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
@ -165,8 +156,8 @@ class Server:
def start(self): def start(self):
if not DaemonContext: if not DaemonContext:
raise ConfigError('can not daemonize, as python-daemon is not installed') raise ConfigError('can not daemonize, as python-daemon is not installed')
piddir = dirname(self._pidfile) piddir = os.path.dirname(self._pidfile)
if not isdir(piddir): if not os.path.isdir(piddir):
os.makedirs(piddir) os.makedirs(piddir)
pidfile = pidlockfile.TimeoutPIDLockFile(self._pidfile) pidfile = pidlockfile.TimeoutPIDLockFile(self._pidfile)
@ -238,7 +229,7 @@ class Server:
# all objs created, now start them up and interconnect # all objs created, now start them up and interconnect
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
self.log.info('registering module %r' % modname) self.log.info('registering module %r' % modname)
self.dispatcher.register_module(modobj, modname, modobj.properties['export']) self.dispatcher.register_module(modobj, modname, modobj.export)
if modobj.pollerClass is not None: if modobj.pollerClass is not None:
# a module might be explicitly excluded from polling by setting pollerClass to None # a module might be explicitly excluded from polling by setting pollerClass to None
modobj.pollerClass.add_to_table(poll_table, modobj) modobj.pollerClass.add_to_table(poll_table, modobj)
@ -247,14 +238,16 @@ class Server:
# handle attached modules # handle attached modules
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
for propname, propobj in modobj.__class__.properties.items(): for propname, propobj in modobj.propertyDict.items():
if isinstance(propobj, Attached): if isinstance(propobj, Attached):
setattr(modobj, propobj.attrname or '_' + propname, setattr(modobj, propobj.attrname or '_' + propname,
self.dispatcher.get_module(modobj.properties[propname])) self.dispatcher.get_module(getattr(modobj, propname)))
# call init on each module after registering all # call init on each module after registering all
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
modobj.initModule() modobj.initModule()
if self._testonly:
return
start_events = [] start_events = []
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
event = threading.Event() event = threading.Event()
@ -271,10 +264,3 @@ class Server:
if not event.wait(timeout=max(0, deadline - time.time())): if not event.wait(timeout=max(0, deadline - time.time())):
self.log.info('WARNING: timeout when starting %s' % name) self.log.info('WARNING: timeout when starting %s' % name)
self.log.info('all modules and pollers started') self.log.info('all modules and pollers started')
history_path = os.environ.get('FRAPPY_HISTORY')
if history_path:
from secop.histwriter import HistWriter
writer = HistWriter(history_path, PREDEFINED_ACCESSIBLES.keys(), self.dispatcher)
# treat writer as a connection
self.dispatcher.add_connection(writer)
writer.init(self.dispatcher.handle_describe(writer, None, None))

View File

@ -22,12 +22,15 @@
"""Define Simulation classes""" """Define Simulation classes"""
# TODO: rework after syntax change!
import random import random
from time import sleep from time import sleep
from secop.datatypes import FloatRange from secop.datatypes import FloatRange
from secop.lib import mkthread from secop.lib import mkthread
from secop.modules import Drivable, Module, Parameter, Readable, Writable, BasicPoller from secop.modules import BasicPoller, Drivable, \
Module, Parameter, Readable, Writable
class SimBase: class SimBase:

View File

@ -23,15 +23,18 @@
implements TCP/IP and is be used as a base for SerialIO implements TCP/IP and is be used as a base for SerialIO
""" """
import time
import threading
import re import re
import threading
import time
from secop.datatypes import ArrayOf, BoolType, \
FloatRange, StringType, TupleOf, ValueType
from secop.errors import CommunicationFailedError, \
CommunicationSilentError, ConfigError
from secop.lib.asynconn import AsynConn, ConnectionClosed from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Override from secop.modules import Attached, Command, \
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType Communicator, Done, Module, Parameter, Property
from secop.errors import CommunicationFailedError, CommunicationSilentError
from secop.poller import REGULAR from secop.poller import REGULAR
from secop.metaclass import Done
class StringIO(Communicator): class StringIO(Communicator):
@ -39,48 +42,22 @@ class StringIO(Communicator):
self healing is assured by polling the parameter 'is_connected' self healing is assured by polling the parameter 'is_connected'
""" """
properties = { uri = Property('hostname:portnumber', datatype=StringType())
'uri': end_of_line = Property('end_of_line character', datatype=ValueType(),
Property('hostname:portnumber', datatype=StringType()), default='\n', settable=True)
'end_of_line': encoding = Property('used encoding', datatype=StringType(),
Property('end_of_line character', datatype=ValueType(), default='ascii', settable=True)
default='\n', settable=True), identification = Property('''
'encoding': identification
Property('used encoding', datatype=StringType(),
default='ascii', settable=True),
'identification':
Property('identification\n\n'
'a list of tuples with commands and expected responses as regexp, '
'to be sent on connect',
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
}
parameters = {
'timeout':
Parameter('timeout', datatype=FloatRange(0), default=2),
'wait_before':
Parameter('wait time before sending', datatype=FloatRange(), default=0),
'is_connected':
Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR),
'pollinterval':
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
}
commands = {
'communicate':
Override('''
send a command and receive a reply
- using end_of_line, encoding and self._lock a list of tuples with commands and expected responses as regexp,
- for commands without reply, the command must be joined with a query command, to be sent on connect''',
- wait_before is respected for end_of_lines within a command datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
'''),
'multicomm': timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
Command(''' wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
execute multiple commands in one go is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
assuring that no other thread calls commands in between
''',
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
}
_reconnectCallbacks = None _reconnectCallbacks = None
@ -115,11 +92,12 @@ class StringIO(Communicator):
self._conn = AsynConn(uri, self._eol_read) self._conn = AsynConn(uri, self._eol_read)
self.is_connected = True self.is_connected = True
for command, regexp in self.identification: for command, regexp in self.identification:
reply = self.do_communicate(command) reply = self.communicate(command)
if not re.match(regexp, reply): if not re.match(regexp, reply):
self.closeConnection() self.closeConnection()
raise CommunicationFailedError('bad response: %s does not match %s' % raise CommunicationFailedError('bad response: %s does not match %s' %
(reply, regexp)) (reply, regexp))
def closeConnection(self): def closeConnection(self):
"""close connection """close connection
@ -135,7 +113,7 @@ class StringIO(Communicator):
self.is_connected is changed only by self.connectStart or self.closeConnection self.is_connected is changed only by self.connectStart or self.closeConnection
""" """
if self.is_connected: if self.is_connected:
return Done # no need for intermediate updates return Done # no need for intermediate updates
try: try:
self.connectStart() self.connectStart()
if self._last_error: if self._last_error:
@ -180,9 +158,17 @@ class StringIO(Communicator):
if removeme: if removeme:
self._reconnectCallbacks.pop(key) self._reconnectCallbacks.pop(key)
def do_communicate(self, command): def communicate(self, command):
"""send a command and receive a reply
using end_of_line, encoding and self._lock
for commands without reply, the command must be joined with a query command,
wait_before is respected for end_of_lines within a command.
"""
if not self.is_connected: if not self.is_connected:
self.read_is_connected() # try to reconnect self.read_is_connected() # try to reconnect
if not self._conn:
raise CommunicationSilentError('can not connect to %r' % self.uri)
try: try:
with self._lock: with self._lock:
# read garbage and wait before send # read garbage and wait before send
@ -214,11 +200,13 @@ class StringIO(Communicator):
self.log.error(self._last_error) self.log.error(self._last_error)
raise raise
def do_multicomm(self, commands): @Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
def multicomm(self, commands):
"""communicate multiple request/replies in one row"""
replies = [] replies = []
with self._lock: with self._lock:
for cmd in commands: for cmd in commands:
replies.append(self.do_communicate(cmd)) replies.append(self.communicate(cmd))
return replies return replies
@ -227,16 +215,15 @@ class HasIodev(Module):
not only StringIO ! not only StringIO !
""" """
properties = { iodev = Attached()
'iodev': Attached(), uri = Property('uri for automatic creation of the attached communication module',
'uri': Property('uri for automatic creation of the attached communication module', StringType(), default=''), StringType(), default='')
}
iodevDict = {} iodevDict = {}
def __init__(self, name, logger, opts, srv): def __init__(self, name, logger, opts, srv):
iodev = opts.get('iodev') iodev = opts.get('iodev')
super().__init__(name, logger, opts, srv) Module.__init__(self, name, logger, opts, srv)
if self.uri: if self.uri:
opts = {'uri': self.uri, 'description': 'communication device for %s' % name, opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
'export': False} 'export': False}
@ -246,7 +233,9 @@ class HasIodev(Module):
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv) iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
srv.modules[ioname] = iodev srv.modules[ioname] = iodev
self.iodevDict[self.uri] = ioname self.iodevDict[self.uri] = ioname
self.setProperty('iodev', ioname) self.iodev = ioname
elif not self.iodev:
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
def initModule(self): def initModule(self):
try: try:
@ -257,4 +246,4 @@ class HasIodev(Module):
super().initModule() super().initModule()
def sendRecv(self, command): def sendRecv(self, command):
return self._iodev.do_communicate(command) return self._iodev.communicate(command)

View File

@ -47,7 +47,7 @@ def get_git_version(abbrev=4, cwd=None):
# mangle version to comply with pep440 # mangle version to comply with pep440
if version.count('-'): if version.count('-'):
version, patchcount, githash = version.split('-') version, patchcount, githash = version.split('-')
version += '.post%s+%s' %(patchcount, githash) version += '.post%s+%s' % (patchcount, githash)
return version return version
except Exception: except Exception:
return None return None

View File

@ -25,20 +25,17 @@ import random
import time import time
from math import atan from math import atan
from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType from secop.datatypes import BoolType, EnumType, FloatRange, StringType, TupleOf
from secop.lib import clamp, mkthread from secop.lib import clamp, mkthread
from secop.modules import Drivable, Override, Parameter from secop.modules import Command, Drivable, Parameter
# test custom property (value.test can be changed in config file) # test custom property (value.test can be changed in config file)
from secop.properties import Property from secop.properties import Property
Parameter.properties['test'] = Property('A Property for testing purposes', StringType(), default='', export=True) Parameter.propertyDict['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
class CryoBase(Drivable): class CryoBase(Drivable):
properties = { is_cryo = Property('private Flag if this is a cryostat', BoolType(), default=True, export=True)
'is_cryo': Property('private Flag if this is a cryostat', BoolType(), default=True, export=True),
}
class Cryostat(CryoBase): class Cryostat(CryoBase):
@ -49,93 +46,88 @@ class Cryostat(CryoBase):
- thermal transfer between regulation and samplen - thermal transfer between regulation and samplen
""" """
parameters = dict( jitter = Parameter("amount of random noise on readout values",
jitter=Parameter("amount of random noise on readout values", datatype=FloatRange(0, 1), unit="K",
datatype=FloatRange(0, 1), unit="K", default=0.1, readonly=False, export=False,
default=0.1, readonly=False, export=False,
),
T_start=Parameter("starting temperature for simulation",
datatype=FloatRange(0), default=10,
export=False,
),
looptime=Parameter("timestep for simulation",
datatype=FloatRange(0.01, 10), unit="s", default=1,
readonly=False, export=False,
), ),
ramp=Parameter("ramping speed of the setpoint", T_start = Parameter("starting temperature for simulation",
datatype=FloatRange(0, 1e3), unit="K/min", default=1, datatype=FloatRange(0), default=10,
readonly=False, export=False,
), ),
setpoint=Parameter("current setpoint during ramping else target", looptime = Parameter("timestep for simulation",
datatype=FloatRange(), default=1, unit='K', datatype=FloatRange(0.01, 10), unit="s", default=1,
), readonly=False, export=False,
maxpower=Parameter("Maximum heater power", ),
datatype=FloatRange(0), default=1, unit="W", ramp = Parameter("ramping speed of the setpoint",
readonly=False, datatype=FloatRange(0, 1e3), unit="K/min", default=1,
group='heater_settings',
),
heater=Parameter("current heater setting",
datatype=FloatRange(0, 100), default=0, unit="%",
group='heater_settings',
),
heaterpower=Parameter("current heater power",
datatype=FloatRange(0), default=0, unit="W",
group='heater_settings',
),
target=Override("target temperature",
datatype=FloatRange(0), default=0, unit="K",
readonly=False, readonly=False,
), ),
value=Override("regulation temperature", setpoint = Parameter("current setpoint during ramping else target",
datatype=FloatRange(0), default=0, unit="K", datatype=FloatRange(), default=1, unit='K',
test='TEST', ),
maxpower = Parameter("Maximum heater power",
datatype=FloatRange(0), default=1, unit="W",
readonly=False,
group='heater_settings',
),
heater = Parameter("current heater setting",
datatype=FloatRange(0, 100), default=0, unit="%",
group='heater_settings',
),
heaterpower = Parameter("current heater power",
datatype=FloatRange(0), default=0, unit="W",
group='heater_settings',
),
target = Parameter("target temperature",
datatype=FloatRange(0), default=0, unit="K",
readonly=False,
),
value = Parameter("regulation temperature",
datatype=FloatRange(0), default=0, unit="K",
test='TEST',
),
pid = Parameter("regulation coefficients",
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
FloatRange(0, 100)),
default=(40, 10, 2), readonly=False,
group='pid',
), ),
pid=Parameter("regulation coefficients", # pylint: disable=invalid-name
datatype=TupleOf(FloatRange(0), FloatRange(0, 100), p = Parameter("regulation coefficient 'p'",
FloatRange(0, 100)), datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
default=(40, 10, 2), readonly=False,
group='pid', group='pid',
), ),
p=Parameter("regulation coefficient 'p'", i = Parameter("regulation coefficient 'i'",
datatype=FloatRange(0), default=40, unit="%/K", readonly=False, datatype=FloatRange(0, 100), default=10, readonly=False,
group='pid', group='pid',
), ),
i=Parameter("regulation coefficient 'i'", d = Parameter("regulation coefficient 'd'",
datatype=FloatRange(0, 100), default=10, readonly=False, datatype=FloatRange(0, 100), default=2, readonly=False,
group='pid', group='pid',
), ),
d=Parameter("regulation coefficient 'd'", mode = Parameter("mode of regulation",
datatype=FloatRange(0, 100), default=2, readonly=False, datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
group='pid', default='ramp',
), readonly=False,
mode=Parameter("mode of regulation", ),
datatype=EnumType('mode', ramp=None, pid=None, openloop=None), pollinterval = Parameter("polling interval",
default='ramp', datatype=FloatRange(0), default=5,
readonly=False, ),
), tolerance = Parameter("temperature range for stability checking",
pollinterval=Override("polling interval", datatype=FloatRange(0, 100), default=0.1, unit='K',
datatype=FloatRange(0), default=5, readonly=False,
), group='stability',
tolerance=Parameter("temperature range for stability checking", ),
datatype=FloatRange(0, 100), default=0.1, unit='K', window = Parameter("time window for stability checking",
datatype=FloatRange(1, 900), default=30, unit='s',
readonly=False,
group='stability',
),
timeout = Parameter("max waiting time for stabilisation check",
datatype=FloatRange(1, 36000), default=900, unit='s',
readonly=False, readonly=False,
group='stability', group='stability',
), ),
window=Parameter("time window for stability checking",
datatype=FloatRange(1, 900), default=30, unit='s',
readonly=False,
group='stability',
),
timeout=Parameter("max waiting time for stabilisation check",
datatype=FloatRange(1, 36000), default=900, unit='s',
readonly=False,
group='stability',
),
)
commands = dict(
stop=Override(
"Stop ramping the setpoint\n\nby setting the current setpoint as new target"),
)
def initModule(self): def initModule(self):
self._stopflag = False self._stopflag = False
@ -180,11 +172,11 @@ class Cryostat(CryoBase):
def read_pid(self): def read_pid(self):
return (self.p, self.i, self.d) return (self.p, self.i, self.d)
def do_stop(self): @Command()
""""stop the ramp def stop(self):
"""Stop ramping the setpoint
by setting current setpoint as target by setting the current setpoint as new target"""
"""
# XXX: discussion: take setpoint or current value ??? # XXX: discussion: take setpoint or current value ???
self.write_target(self.setpoint) self.write_target(self.setpoint)

View File

@ -28,42 +28,41 @@ import time
from secop.datatypes import ArrayOf, BoolType, EnumType, \ from secop.datatypes import ArrayOf, BoolType, EnumType, \
FloatRange, IntRange, StringType, StructOf, TupleOf FloatRange, IntRange, StringType, StructOf, TupleOf
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Drivable, Override, Parameter as SECoP_Parameter, Readable from secop.modules import Drivable
from secop.modules import Parameter as SECoP_Parameter
from secop.modules import Readable
from secop.properties import Property from secop.properties import Property
class Parameter(SECoP_Parameter): class Parameter(SECoP_Parameter):
properties = { test = Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test')
'test' : Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test'),
}
PERSIST = 101 PERSIST = 101
class Switch(Drivable): class Switch(Drivable):
"""switch it on or off.... """switch it on or off....
""" """
parameters = {
'value': Override('current state (on or off)', value = Parameter('current state (on or off)',
datatype=EnumType(on=1, off=0), default=0,
)
target = Parameter('wanted state (on or off)',
datatype=EnumType(on=1, off=0), default=0, datatype=EnumType(on=1, off=0), default=0,
), readonly=False,
'target': Override('wanted state (on or off)', )
datatype=EnumType(on=1, off=0), default=0, switch_on_time = Parameter('seconds to wait after activating the switch',
readonly=False, datatype=FloatRange(0, 60), unit='s',
), default=10, export=False,
'switch_on_time': Parameter('seconds to wait after activating the switch', )
switch_off_time = Parameter('cool-down time in seconds',
datatype=FloatRange(0, 60), unit='s', datatype=FloatRange(0, 60), unit='s',
default=10, export=False, default=10, export=False,
), )
'switch_off_time': Parameter('cool-down time in seconds',
datatype=FloatRange(0, 60), unit='s',
default=10, export=False,
),
}
properties = { description = Property('The description of the Module', StringType(),
'description' : Property('The description of the Module', StringType(), default='no description', mandatory=False, extname='description')
default='no description', mandatory=False, extname='description'),
}
def read_value(self): def read_value(self):
# could ask HW # could ask HW
@ -109,30 +108,29 @@ class Switch(Drivable):
class MagneticField(Drivable): class MagneticField(Drivable):
"""a liquid magnet """a liquid magnet
""" """
parameters = {
'value': Override('current field in T', value = Parameter('current field in T',
unit='T', datatype=FloatRange(-15, 15), default=0,
)
target = Parameter('target field in T',
unit='T', datatype=FloatRange(-15, 15), default=0, unit='T', datatype=FloatRange(-15, 15), default=0,
), readonly=False,
'target': Override('target field in T', )
unit='T', datatype=FloatRange(-15, 15), default=0, ramp = Parameter('ramping speed',
readonly=False, unit='T/min', datatype=FloatRange(0, 1), default=0.1,
), readonly=False,
'ramp': Parameter('ramping speed', )
unit='T/min', datatype=FloatRange(0, 1), default=0.1, mode = Parameter('what to do after changing field',
readonly=False, default=1, datatype=EnumType(persistent=1, hold=0),
), readonly=False,
'mode': Parameter('what to do after changing field', )
default=1, datatype=EnumType(persistent=1, hold=0), heatswitch = Parameter('name of heat switch device',
readonly=False, datatype=StringType(), export=False,
), )
'heatswitch': Parameter('name of heat switch device',
datatype=StringType(), export=False,
),
}
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303) Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
overrides = {
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())), status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
}
def initModule(self): def initModule(self):
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
@ -202,21 +200,20 @@ class MagneticField(Drivable):
time.sleep(max(0.01, ts + loopdelay - time.time())) time.sleep(max(0.01, ts + loopdelay - time.time()))
self.log.error(self, 'main thread exited unexpectedly!') self.log.error(self, 'main thread exited unexpectedly!')
def do_stop(self): def stop(self):
self.write_target(self.read_value()) self.write_target(self.read_value())
class CoilTemp(Readable): class CoilTemp(Readable):
"""a coil temperature """a coil temperature
""" """
parameters = {
'value': Override('Coil temperatur', value = Parameter('Coil temperatur',
unit='K', datatype=FloatRange(), default=0, unit='K', datatype=FloatRange(), default=0,
), )
'sensor': Parameter("Sensor number or calibration id", sensor = Parameter("Sensor number or calibration id",
datatype=StringType(), readonly=True, datatype=StringType(), readonly=True,
), )
}
def read_value(self): def read_value(self):
return round(2.3 + random.random(), 3) return round(2.3 + random.random(), 3)
@ -225,18 +222,17 @@ class CoilTemp(Readable):
class SampleTemp(Drivable): class SampleTemp(Drivable):
"""a sample temperature """a sample temperature
""" """
parameters = {
'value': Override('Sample temperature', value = Parameter('Sample temperature',
unit='K', datatype=FloatRange(), default=10, unit='K', datatype=FloatRange(), default=10,
), )
'sensor': Parameter("Sensor number or calibration id", sensor = Parameter("Sensor number or calibration id",
datatype=StringType(), readonly=True, datatype=StringType(), readonly=True,
), )
'ramp': Parameter('moving speed in K/min', ramp = Parameter('moving speed in K/min',
datatype=FloatRange(0, 100), unit='K/min', default=0.1, datatype=FloatRange(0, 100), unit='K/min', default=0.1,
readonly=False, readonly=False,
), )
}
def initModule(self): def initModule(self):
_thread = threading.Thread(target=self._thread) _thread = threading.Thread(target=self._thread)
@ -272,20 +268,19 @@ class Label(Readable):
of several subdevices. used for demoing connections between of several subdevices. used for demoing connections between
modules. modules.
""" """
parameters = {
'system': Parameter("Name of the magnet system", system = Parameter("Name of the magnet system",
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
'subdev_mf': Parameter("name of subdevice for magnet status", subdev_mf = Parameter("name of subdevice for magnet status",
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
'subdev_ts': Parameter("name of subdevice for sample temp", subdev_ts = Parameter("name of subdevice for sample temp",
datatype=StringType(), export=False, datatype=StringType(), export=False,
), )
'value': Override("final value of label string", default='', value = Parameter("final value of label string", default='',
datatype=StringType(), datatype=StringType(),
), )
}
def read_value(self): def read_value(self):
strings = [self.system] strings = [self.system]
@ -317,29 +312,25 @@ class Label(Readable):
class DatatypesTest(Readable): class DatatypesTest(Readable):
"""for demoing all datatypes """for demoing all datatypes
""" """
parameters = {
'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9), enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
readonly=False, default=1), readonly=False, default=1)
'tupleof': Parameter('tuple of int, float and str', tupleof = Parameter('tuple of int, float and str',
datatype=TupleOf(IntRange(), FloatRange(), datatype=TupleOf(IntRange(), FloatRange(),
StringType()), StringType()),
readonly=False, default=(1, 2.3, 'a')), readonly=False, default=(1, 2.3, 'a'))
'arrayof': Parameter('array: 2..3 times bool', arrayof = Parameter('array: 2..3 times bool',
datatype=ArrayOf(BoolType(), 2, 3), datatype=ArrayOf(BoolType(), 2, 3),
readonly=False, default=[1, 0, 1]), readonly=False, default=[1, 0, 1])
'intrange': Parameter('intrange', datatype=IntRange(2, 9), intrange = Parameter('intrange', datatype=IntRange(2, 9),
readonly=False, default=4), readonly=False, default=4)
'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1), floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
readonly=False, default=0, ), readonly=False, default=0)
'struct': Parameter('struct(a=str, b=int, c=bool)', struct = Parameter('struct(a=str, b=int, c=bool)',
datatype=StructOf(a=StringType(), b=IntRange(), datatype=StructOf(a=StringType(), b=IntRange(),
c=BoolType()), c=BoolType()))
),
}
class ArrayTest(Readable): class ArrayTest(Readable):
parameters = { x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000), default=100000 * [0])
default = 100000 * [0]),
}

View File

@ -24,7 +24,7 @@
import random import random
from secop.datatypes import FloatRange, StringType from secop.datatypes import FloatRange, StringType
from secop.modules import Communicator, Drivable, Parameter, Readable, Override from secop.modules import Communicator, Drivable, Parameter, Readable
from secop.params import Command from secop.params import Command
@ -45,11 +45,10 @@ class Heater(Drivable):
class name indicates it to be some heating element, class name indicates it to be some heating element,
but the implementation may do anything but the implementation may do anything
""" """
parameters = {
'maxheaterpower': Parameter('maximum allowed heater power', maxheaterpower = Parameter('maximum allowed heater power',
datatype=FloatRange(0, 100), unit='W', datatype=FloatRange(0, 100), unit='W',
), )
}
def read_value(self): def read_value(self):
return round(100 * random.random(), 1) return round(100 * random.random(), 1)
@ -64,22 +63,21 @@ class Temp(Drivable):
class name indicates it to be some temperature controller, class name indicates it to be some temperature controller,
but the implementation may do anything but the implementation may do anything
""" """
parameters = {
'sensor': Parameter( sensor = Parameter(
"Sensor number or calibration id", "Sensor number or calibration id",
datatype=StringType( datatype=StringType(
8, 8,
16), 16),
readonly=True, readonly=True,
), )
'target': Override( target = Parameter(
"Target temperature", "Target temperature",
default=300.0, default=300.0,
datatype=FloatRange(0), datatype=FloatRange(0),
readonly=False, readonly=False,
unit='K', unit='K',
), )
}
def read_value(self): def read_value(self):
return round(100 * random.random(), 1) return round(100 * random.random(), 1)
@ -90,8 +88,8 @@ class Temp(Drivable):
class Lower(Communicator): class Lower(Communicator):
"""Communicator returning a lowercase version of the request""" """Communicator returning a lowercase version of the request"""
command = {
'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'), @Command(argument=StringType(), result=StringType(), export='communicate')
} def communicate(self, command):
def do_communicate(self, request): """lowercase a string"""
return str(request).lower() return str(command).lower()

View File

@ -58,20 +58,20 @@ except ImportError:
class EpicsReadable(Readable): class EpicsReadable(Readable):
"""EpicsDrivable handles a Drivable interfacing to EPICS v4""" """EpicsDrivable handles a Drivable interfacing to EPICS v4"""
# Commmon parameter for all EPICS devices # Commmon parameter for all EPICS devices
parameters = {
'value': Parameter('EPICS generic value', # parameters
datatype=FloatRange(), value = Parameter('EPICS generic value',
default=300.0,), datatype=FloatRange(),
'epics_version': Parameter("EPICS version used, v3 or v4", default=300.0,)
datatype=EnumType(v3=3, v4=4),), epics_version = Parameter("EPICS version used, v3 or v4",
# 'private' parameters: not remotely accessible datatype=EnumType(v3=3, v4=4),)
'value_pv': Parameter('EPICS pv_name of value', value_pv = Parameter('EPICS pv_name of value',
datatype=StringType(), datatype=StringType(),
default="unset", export=False), default="unset", export=False)
'status_pv': Parameter('EPICS pv_name of status', status_pv = Parameter('EPICS pv_name of status',
datatype=StringType(), datatype=StringType(),
default="unset", export=False), default="unset", export=False)
}
# Generic read and write functions # Generic read and write functions
def _read_pv(self, pv_name): def _read_pv(self, pv_name):
@ -118,21 +118,21 @@ class EpicsReadable(Readable):
class EpicsDrivable(Drivable): class EpicsDrivable(Drivable):
"""EpicsDrivable handles a Drivable interfacing to EPICS v4""" """EpicsDrivable handles a Drivable interfacing to EPICS v4"""
# Commmon parameter for all EPICS devices # Commmon parameter for all EPICS devices
parameters = {
'target': Parameter('EPICS generic target', datatype=FloatRange(), # parameters
default=300.0, readonly=False), target = Parameter('EPICS generic target', datatype=FloatRange(),
'value': Parameter('EPICS generic value', datatype=FloatRange(), default=300.0, readonly=False)
default=300.0,), value = Parameter('EPICS generic value', datatype=FloatRange(),
'epics_version': Parameter("EPICS version used, v3 or v4", default=300.0,)
datatype=StringType(),), epics_version = Parameter("EPICS version used, v3 or v4",
# 'private' parameters: not remotely accessible datatype=StringType(),)
'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(), target_pv = Parameter('EPICS pv_name of target', datatype=StringType(),
default="unset", export=False), default="unset", export=False)
'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(), value_pv = Parameter('EPICS pv_name of value', datatype=StringType(),
default="unset", export=False), default="unset", export=False)
'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(), status_pv = Parameter('EPICS pv_name of status', datatype=StringType(),
default="unset", export=False), default="unset", export=False)
}
# Generic read and write functions # Generic read and write functions
def _read_pv(self, pv_name): def _read_pv(self, pv_name):
@ -191,17 +191,16 @@ class EpicsDrivable(Drivable):
class EpicsTempCtrl(EpicsDrivable): class EpicsTempCtrl(EpicsDrivable):
parameters = {
# TODO: restrict possible values with oneof datatype # parameters
'heaterrange': Parameter('Heater range', datatype=StringType(), heaterrange = Parameter('Heater range', datatype=StringType(),
default='Off', readonly=False,), default='Off', readonly=False,)
'tolerance': Parameter('allowed deviation between value and target', tolerance = Parameter('allowed deviation between value and target',
datatype=FloatRange(1e-6, 1e6), default=0.1, datatype=FloatRange(1e-6, 1e6), default=0.1,
readonly=False,), readonly=False,)
# 'private' parameters: not remotely accessible heaterrange_pv = Parameter('EPICS pv_name of heater range',
'heaterrange_pv': Parameter('EPICS pv_name of heater range', datatype=StringType(), default="unset", export=False,)
datatype=StringType(), default="unset", export=False,),
}
def read_target(self): def read_target(self):
return self._read_pv(self.target_pv) return self._read_pv(self.target_pv)

View File

@ -31,7 +31,7 @@ import math
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
from secop.errors import ConfigError, DisabledError from secop.errors import ConfigError, DisabledError
from secop.lib.sequence import SequencerMixin, Step from secop.lib.sequence import SequencerMixin, Step
from secop.modules import Drivable, Parameter, BasicPoller from secop.modules import BasicPoller, Drivable, Parameter
class GarfieldMagnet(SequencerMixin, Drivable): class GarfieldMagnet(SequencerMixin, Drivable):
@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable):
pollerClass = BasicPoller pollerClass = BasicPoller
parameters = {
'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False), # parameters
'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False), subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False), subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
'subdev_symmetry': Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False), subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False)
'userlimits': Parameter('User defined limits of device value', subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), userlimits = Parameter('User defined limits of device value',
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10), datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
'abslimits': Parameter('Absolute limits of device value', default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), abslimits = Parameter('Absolute limits of device value',
default=(-0.5, 0.5), poll=True, datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
), default=(-0.5, 0.5), poll=True,
'precision': Parameter('Precision of the device value (allowed deviation ' )
'of stable values from target)', precision = Parameter('Precision of the device value (allowed deviation '
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False, 'of stable values from target)',
), datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
'ramp': Parameter('Target rate of field change per minute', readonly=False, )
datatype=FloatRange(unit='$/min'), default=1.0), ramp = Parameter('Target rate of field change per minute', readonly=False,
'calibration': Parameter('Coefficients for calibration ' datatype=FloatRange(unit='$/min'), default=1.0)
'function: [c0, c1, c2, c3, c4] calculates ' calibration = Parameter('Coefficients for calibration '
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' 'function: [c0, c1, c2, c3, c4] calculates '
' in T', poll=1, 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
datatype=ArrayOf(FloatRange(), 5, 5), ' in T', poll=1,
default=(1.0, 0.0, 0.0, 0.0, 0.0)), datatype=ArrayOf(FloatRange(), 5, 5),
'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting', default=(1.0, 0.0, 0.0, 0.0, 0.0))
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5), calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
short=ArrayOf( datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
FloatRange(), 5, 5), short=ArrayOf(
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False), FloatRange(), 5, 5),
} asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False)
def _current2field(self, current, *coefficients): def _current2field(self, current, *coefficients):
"""Return field in T for given current in A. """Return field in T for given current in A.
@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
return self._currentsource.read_status()[0] == 'BUSY' return self._currentsource.read_status()[0] == 'BUSY'
if self._currentsource.status[0] != 'BUSY': if self._currentsource.status[0] != 'BUSY':
if self._enable.status[0] == 'ERROR': if self._enable.status[0] == 'ERROR':
self._enable.do_reset() self._enable.reset()
self._enable.read_status() self._enable.read_status()
self._enable.write_target('On') self._enable.write_target('On')
self._enable._hw_wait() self._enable._hw_wait()

View File

@ -30,18 +30,17 @@ MLZ TANGO interface for the respective device classes.
import re import re
import threading import threading
from time import time as currenttime
from time import sleep from time import sleep
from time import time as currenttime
import PyTango import PyTango
from secop.datatypes import ArrayOf, EnumType, FloatRange, \
from secop.datatypes import ArrayOf, EnumType, \ IntRange, LimitsType, StringType, TupleOf
FloatRange, IntRange, StringType, TupleOf, LimitsType
from secop.errors import CommunicationFailedError, \ from secop.errors import CommunicationFailedError, \
ConfigError, HardwareError, ProgrammingError ConfigError, HardwareError, ProgrammingError
from secop.lib import lazy_property from secop.lib import lazy_property
from secop.modules import Command, Drivable, \ from secop.modules import BasicPoller, Command, \
Module, Override, Parameter, Readable, BasicPoller Drivable, Module, Parameter, Readable
##### #####
@ -160,24 +159,18 @@ class PyTangoDevice(Module):
pollerClass = BasicPoller pollerClass = BasicPoller
parameters = { # parameters
'comtries': Parameter('Maximum retries for communication', comtries = Parameter('Maximum retries for communication',
datatype=IntRange(1, 100), default=3, readonly=False, datatype=IntRange(1, 100), default=3, readonly=False,
group='communication'), group='communication')
'comdelay': Parameter('Delay between retries', datatype=FloatRange(0), comdelay = Parameter('Delay between retries', datatype=FloatRange(0),
unit='s', default=0.1, readonly=False, unit='s', default=0.1, readonly=False,
group='communication'), group='communication')
tangodevice = Parameter('Tango device name',
'tangodevice': Parameter('Tango device name', datatype=StringType(), readonly=True,
datatype=StringType(), readonly=True, # export=True, # for testing only
# export=True, # for testing only export=False,
export=False, )
),
}
commands = {
'reset': Command('Tango reset command', argument=None, result=None),
}
tango_status_mapping = { tango_status_mapping = {
PyTango.DevState.ON: Drivable.Status.IDLE, PyTango.DevState.ON: Drivable.Status.IDLE,
@ -372,7 +365,9 @@ class PyTangoDevice(Module):
return (myState, tangoStatus) return (myState, tangoStatus)
def do_reset(self): @Command(argument=None, result=None)
def reset(self):
"""Tango reset command"""
self._dev.Reset() self._dev.Reset()
@ -405,13 +400,9 @@ class Sensor(AnalogInput):
# note: we don't transport the formula to secop.... # note: we don't transport the formula to secop....
# we support the adjust method # we support the adjust method
commands = { @Command(argument=FloatRange(), result=None)
'setposition': Command('Set the position to the given value.', def setposition(self, value):
argument=FloatRange(), result=None, """Set the position to the given value."""
),
}
def do_setposition(self, value):
self._dev.Adjust(value) self._dev.Adjust(value)
@ -427,29 +418,29 @@ class AnalogOutput(PyTangoDevice, Drivable):
controllers, ... controllers, ...
""" """
parameters = { # parameters
'userlimits': Parameter('User defined limits of device value', userlimits = Parameter('User defined limits of device value',
datatype=LimitsType(FloatRange(unit='$')),
default=(float('-Inf'), float('+Inf')),
readonly=False, poll=10,
),
'abslimits': Parameter('Absolute limits of device value',
datatype=LimitsType(FloatRange(unit='$')), datatype=LimitsType(FloatRange(unit='$')),
), default=(float('-Inf'), float('+Inf')),
'precision': Parameter('Precision of the device value (allowed deviation ' readonly=False, poll=10,
'of stable values from target)', )
datatype=FloatRange(1e-38, unit='$'), abslimits = Parameter('Absolute limits of device value',
readonly=False, group='stability', datatype=LimitsType(FloatRange(unit='$')),
), )
'window': Parameter('Time window for checking stabilization if > 0', precision = Parameter('Precision of the device value (allowed deviation '
default=60.0, readonly=False, 'of stable values from target)',
datatype=FloatRange(0, 900, unit='s'), group='stability', datatype=FloatRange(1e-38, unit='$'),
), readonly=False, group='stability',
'timeout': Parameter('Timeout for waiting for a stable value (if > 0)', )
default=60.0, readonly=False, window = Parameter('Time window for checking stabilization if > 0',
datatype=FloatRange(0, 900, unit='s'), group='stability', default=60.0, readonly=False,
), datatype=FloatRange(0, 900, unit='s'), group='stability',
} )
timeout = Parameter('Timeout for waiting for a stable value (if > 0)',
default=60.0, readonly=False,
datatype=FloatRange(0, 900, unit='s'), group='stability',
)
_history = () _history = ()
_timeout = None _timeout = None
_moving = False _moving = False
@ -566,7 +557,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
if self.status[0] == self.Status.BUSY: if self.status[0] == self.Status.BUSY:
# changing target value during movement is not allowed by the # changing target value during movement is not allowed by the
# Tango base class state machine. If we are moving, stop first. # Tango base class state machine. If we are moving, stop first.
self.do_stop() self.stop()
self._hw_wait() self._hw_wait()
self._dev.value = value self._dev.value = value
# set meaningful timeout # set meaningful timeout
@ -587,7 +578,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY: while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
sleep(0.3) sleep(0.3)
def do_stop(self): def stop(self):
self._dev.Stop() self._dev.Stop()
@ -601,21 +592,14 @@ class Actuator(AnalogOutput):
""" """
# for secop: support the speed and ramp parameters # for secop: support the speed and ramp parameters
parameters = { # parameters
'speed': Parameter('The speed of changing the value', speed = Parameter('The speed of changing the value',
readonly=False, datatype=FloatRange(0, unit='$/s'), readonly=False, datatype=FloatRange(0, unit='$/s'),
), )
'ramp': Parameter('The speed of changing the value', ramp = Parameter('The speed of changing the value',
readonly=False, datatype=FloatRange(0, unit='$/s'), readonly=False, datatype=FloatRange(0, unit='$/s'),
poll=30, poll=30,
), )
}
commands = {
'setposition': Command('Set the position to the given value.',
argument=FloatRange(), result=None,
),
}
def read_speed(self): def read_speed(self):
return self._dev.speed return self._dev.speed
@ -630,7 +614,9 @@ class Actuator(AnalogOutput):
self.write_speed(value / 60.) self.write_speed(value / 60.)
return self.read_speed() * 60 return self.read_speed() * 60
def do_setposition(self, value=FloatRange()): @Command(FloatRange(), result=None)
def setposition(self, value=FloatRange()):
"""Set the position to the given value."""
self._dev.Adjust(value) self._dev.Adjust(value)
@ -641,21 +627,16 @@ class Motor(Actuator):
It has the ability to move a real object from one place to another place. It has the ability to move a real object from one place to another place.
""" """
parameters = { # parameters
'refpos': Parameter('Reference position', refpos = Parameter('Reference position',
datatype=FloatRange(unit='$'), datatype=FloatRange(unit='$'),
), )
'accel': Parameter('Acceleration', accel = Parameter('Acceleration',
datatype=FloatRange(unit='$/s^2'), readonly=False, datatype=FloatRange(unit='$/s^2'), readonly=False,
), )
'decel': Parameter('Deceleration', decel = Parameter('Deceleration',
datatype=FloatRange(unit='$/s^2'), readonly=False, datatype=FloatRange(unit='$/s^2'), readonly=False,
), )
}
commands = {
'reference': Command('Do a reference run', argument=None, result=None),
}
def read_refpos(self): def read_refpos(self):
return float(self._getProperty('refpos')) return float(self._getProperty('refpos'))
@ -672,7 +653,9 @@ class Motor(Actuator):
def write_decel(self, value): def write_decel(self, value):
self._dev.decel = value self._dev.decel = value
def do_reference(self): @Command()
def reference(self):
"""Do a reference run"""
self._dev.Reference() self._dev.Reference()
return self.read_value() return self.read_value()
@ -681,32 +664,29 @@ class TemperatureController(Actuator):
"""A temperature control loop device. """A temperature control loop device.
""" """
parameters = { # parameters
'p': Parameter('Proportional control Parameter', datatype=FloatRange(), # pylint: disable=invalid-name
readonly=False, group='pid', p = Parameter('Proportional control Parameter', datatype=FloatRange(),
), readonly=False, group='pid',
'i': Parameter('Integral control Parameter', datatype=FloatRange(), )
readonly=False, group='pid', i = Parameter('Integral control Parameter', datatype=FloatRange(),
), readonly=False, group='pid',
'd': Parameter('Derivative control Parameter', datatype=FloatRange(), )
readonly=False, group='pid', d = Parameter('Derivative control Parameter', datatype=FloatRange(),
), readonly=False, group='pid',
'pid': Parameter('pid control Parameters', )
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()), pid = Parameter('pid control Parameters',
readonly=False, group='pid', poll=30, datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
), readonly=False, group='pid', poll=30,
'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1, )
), setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1, )
), heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
} )
overrides = { # overrides
# We want this to be freely user-settable, and not produce a warning precision = Parameter(default=0.1)
# on startup, so select a usually sensible default. ramp = Parameter(description='Temperature ramp')
'precision': Override(default=0.1),
'ramp': Override(description='Temperature ramp'),
}
def read_ramp(self): def read_ramp(self):
return self._dev.ramp return self._dev.ramp
@ -755,15 +735,14 @@ class PowerSupply(Actuator):
"""A power supply (voltage and current) device. """A power supply (voltage and current) device.
""" """
parameters = { # parameters
'voltage': Parameter('Actual voltage', voltage = Parameter('Actual voltage',
datatype=FloatRange(unit='V'), poll=-5), datatype=FloatRange(unit='V'), poll=-5)
'current': Parameter('Actual current', current = Parameter('Actual current',
datatype=FloatRange(unit='A'), poll=-5), datatype=FloatRange(unit='A'), poll=-5)
}
overrides = { # overrides
'ramp': Override(description='Current/voltage ramp'), ramp = Parameter(description='Current/voltage ramp')
}
def read_ramp(self): def read_ramp(self):
return self._dev.ramp return self._dev.ramp
@ -782,9 +761,8 @@ class DigitalInput(PyTangoDevice, Readable):
"""A device reading a bitfield. """A device reading a bitfield.
""" """
overrides = { # overrides
'value': Override(datatype=IntRange()), value = Parameter(datatype=IntRange())
}
def read_value(self): def read_value(self):
return self._dev.value return self._dev.value
@ -794,10 +772,9 @@ class NamedDigitalInput(DigitalInput):
"""A DigitalInput with numeric values mapped to names. """A DigitalInput with numeric values mapped to names.
""" """
parameters = { # parameters
'mapping': Parameter('A dictionary mapping state names to integers', mapping = Parameter('A dictionary mapping state names to integers',
datatype=StringType(), export=False), # XXX:!!! datatype=StringType(), export=False) # XXX:!!!
}
def initModule(self): def initModule(self):
super(NamedDigitalInput, self).initModule() super(NamedDigitalInput, self).initModule()
@ -821,12 +798,11 @@ class PartialDigitalInput(NamedDigitalInput):
bit width accessed. bit width accessed.
""" """
parameters = { # parameters
'startbit': Parameter('Number of the first bit', startbit = Parameter('Number of the first bit',
datatype=IntRange(0), default=0), datatype=IntRange(0), default=0)
'bitwidth': Parameter('Number of bits', bitwidth = Parameter('Number of bits',
datatype=IntRange(0), default=1), datatype=IntRange(0), default=1)
}
def initModule(self): def initModule(self):
super(PartialDigitalInput, self).initModule() super(PartialDigitalInput, self).initModule()
@ -844,10 +820,9 @@ class DigitalOutput(PyTangoDevice, Drivable):
bitfield. bitfield.
""" """
overrides = { # overrides
'value': Override(datatype=IntRange()), value = Parameter(datatype=IntRange())
'target': Override(datatype=IntRange()), target = Parameter(datatype=IntRange())
}
def read_value(self): def read_value(self):
return self._dev.value # mapping is done by datatype upon export() return self._dev.value # mapping is done by datatype upon export()
@ -865,10 +840,9 @@ class NamedDigitalOutput(DigitalOutput):
"""A DigitalOutput with numeric values mapped to names. """A DigitalOutput with numeric values mapped to names.
""" """
parameters = { # parameters
'mapping': Parameter('A dictionary mapping state names to integers', mapping = Parameter('A dictionary mapping state names to integers',
datatype=StringType(), export=False), datatype=StringType(), export=False)
}
def initModule(self): def initModule(self):
super(NamedDigitalOutput, self).initModule() super(NamedDigitalOutput, self).initModule()
@ -894,12 +868,11 @@ class PartialDigitalOutput(NamedDigitalOutput):
bit width accessed. bit width accessed.
""" """
parameters = { # parameters
'startbit': Parameter('Number of the first bit', startbit = Parameter('Number of the first bit',
datatype=IntRange(0), default=0), datatype=IntRange(0), default=0)
'bitwidth': Parameter('Number of bits', bitwidth = Parameter('Number of bits',
datatype=IntRange(0), default=1), datatype=IntRange(0), default=1)
}
def initModule(self): def initModule(self):
super(PartialDigitalOutput, self).initModule() super(PartialDigitalOutput, self).initModule()
@ -925,17 +898,16 @@ class StringIO(PyTangoDevice, Module):
receives strings. receives strings.
""" """
parameters = { # parameters
'bustimeout': Parameter('Communication timeout', bustimeout = Parameter('Communication timeout',
datatype=FloatRange(unit='s'), readonly=False, datatype=FloatRange(unit='s'), readonly=False,
group='communication'), group='communication')
'endofline': Parameter('End of line', endofline = Parameter('End of line',
datatype=StringType(), readonly=False, datatype=StringType(), readonly=False,
group='communication'), group='communication')
'startofline': Parameter('Start of line', startofline = Parameter('Start of line',
datatype=StringType(), readonly=False, datatype=StringType(), readonly=False,
group='communication'), group='communication')
}
def read_bustimeout(self): def read_bustimeout(self):
return self._dev.communicationTimeout return self._dev.communicationTimeout
@ -955,53 +927,48 @@ class StringIO(PyTangoDevice, Module):
def write_startofline(self, value): def write_startofline(self, value):
self._dev.startOfLine = value self._dev.startOfLine = value
commands = { @Command(argument=StringType(), result=StringType())
'communicate': Command('Send a string and return the reply', def communicate(self, value=StringType()):
argument=StringType(), """Send a string and return the reply"""
result=StringType()),
'flush': Command('Flush output buffer',
argument=None, result=None),
'read': Command('read some characters from input buffer',
argument=IntRange(0), result=StringType()),
'write': Command('write some chars to output',
argument=StringType(), result=None),
'readLine': Command('Read sol - a whole line - eol',
argument=None, result=StringType()),
'writeLine': Command('write sol + a whole line + eol',
argument=StringType(), result=None),
'availableChars': Command('return number of chars in input buffer',
argument=None, result=IntRange(0)),
'availableLines': Command('return number of lines in input buffer',
argument=None, result=IntRange(0)),
'multiCommunicate': Command('perform a sequence of communications',
argument=ArrayOf(
TupleOf(StringType(), IntRange()), 100),
result=ArrayOf(StringType(), 100)),
}
def do_communicate(self, value=StringType()):
return self._dev.Communicate(value) return self._dev.Communicate(value)
def do_flush(self): @Command(argument=None, result=None)
def flush(self):
"""Flush output buffer"""
self._dev.Flush() self._dev.Flush()
def do_read(self, value): @Command(argument=IntRange(0), result=StringType())
def read(self, value):
"""read some characters from input buffer"""
return self._dev.Read(value) return self._dev.Read(value)
def do_write(self, value): @Command(argument=StringType(), result=None)
def write(self, value):
"""write some chars to output"""
return self._dev.Write(value) return self._dev.Write(value)
def do_readLine(self): @Command(argument=None, result=StringType())
def readLine(self):
"""Read sol - a whole line - eol"""
return self._dev.ReadLine() return self._dev.ReadLine()
def do_writeLine(self, value): @Command(argument=StringType(), result=None)
def writeLine(self, value):
"""write sol + a whole line + eol"""
return self._dev.WriteLine(value) return self._dev.WriteLine(value)
def do_multiCommunicate(self, value): @Command(argument=ArrayOf(TupleOf(StringType(), IntRange()), 100),
result=ArrayOf(StringType(), 100))
def multiCommunicate(self, value):
"""perform a sequence of communications"""
return self._dev.MultiCommunicate(value) return self._dev.MultiCommunicate(value)
def do_availableChars(self): @Command(argument=None, result=IntRange(0))
def availableChars(self):
"""return number of chars in input buffer"""
return self._dev.availableChars return self._dev.availableChars
def do_availableLines(self): @Command(argument=None, result=IntRange(0))
def availableLines(self):
"""return number of lines in input buffer"""
return self._dev.availableLines return self._dev.availableLines

View File

@ -20,65 +20,53 @@
# ***************************************************************************** # *****************************************************************************
"""WAVE FUNCTION LECROY XX: SIGNAL GENERATOR""" """WAVE FUNCTION LECROY XX: SIGNAL GENERATOR"""
from secop.core import Readable, Parameter, Override, Command, FloatRange, TupleOf, \ from secop.core import Readable, Parameter, FloatRange, \
HasIodev, StringIO, Done, Attached, IntRange, BoolType, EnumType, StringType, Module, \ HasIodev, IntRange, BoolType, EnumType, Module, Property
Property
class Channel(Module): class Channel(HasIodev, Module):
properties = { channel = Property('choose channel to manipulate', IntRange(1, 2))
'channel':Property('choose channel to manipulate',IntRange(1,2)),
} freq = Parameter('frequency', FloatRange(1e-6, 20e6, unit='Hz'),
parameters = { poll=True, initwrite=True, default=1000)
'freq': amp = Parameter('exc_volt_int', FloatRange(0.00, 5, unit='Vrms'),
Parameter('frequency', FloatRange(1e-6,20e6,unit='Hz'), poll=True, readonly=False, initwrite=True, default=0.1)
poll=True, initwrite=True, default=1000), offset = Parameter('offset_volt_int', FloatRange(0.00, 10, unit='V'),
'amp': poll=True, readonly=False, initwrite=True, default=0.0)
Parameter('exc_volt_int', FloatRange(0.00,5,unit='Vrms'), wave = Parameter('type of wavefunction',
poll=True, readonly=False, initwrite=True, default=0.1), EnumType('WaveFunction', SINE=1, SQUARE=2, RAMP=3, PULSE=4, NOISE=5, ARB=6, DC=7),
'offset': poll=True, readonly=False, default='SINE'),
Parameter('offset_volt_int', FloatRange(0.00,10,unit='V'), phase = Parameter('signal phase', FloatRange(0, 360, unit='deg'),
poll = True, readonly = False, initwrite = True, default = 0.0), poll=True, readonly=False, initwrite=True, default=0)
'wave': enabled = Parameter('enable output channel', datatype=EnumType('OnOff', OFF=0, ON=1),
Parameter ('type of wavefunction', readonly=False, default='OFF')
EnumType('WaveFunction', SINE=1, SQUARE=2, RAMP=3, PULSE=4, NOISE=5, ARB=6, DC=7), symm = Parameter('wavefunction symmetry', FloatRange(0, 100, unit=''),
poll=True, readonly=False, default='SINE'), poll=True, readonly=False, default=0)
'phase':
Parameter('signal phase', FloatRange(0,360,unit='deg'),
poll=True, readonly=False, initwrite=True, default=0),
'enabled':
Parameter('enable output channel', datatype=EnumType('OnOff', OFF=0, ON=1),
readonly=False, default='OFF'),
'symm':
Parameter('wavefunction symmetry', FloatRange(0,100, unit=''),
poll=True, readonly =False, default=0),
}
def read_value(self): def read_value(self):
return self.sendRecv('C%d:BSWV FRQ?' % self.channel) return self.sendRecv('C%d:BSWV FRQ?' % self.channel)
def write_target(self, value):
def write_target(self,value):
self.sendRecv('C%d:BSWV FRQ, %g' % (self.channel, str(value)+'Hz')) self.sendRecv('C%d:BSWV FRQ, %g' % (self.channel, str(value)+'Hz'))
return value return value
#signal wavefunction parameter # signal wavefunction parameter
def read_wave(self): def read_wave(self):
return self.sendRecv('C%d:BSWV WVTP?' % self.channel) return self.sendRecv('C%d:BSWV WVTP?' % self.channel)
def write_wave(self,value): #string value def write_wave(self, value): # string value
self.sendRecv('C%d:BSWV WVTP, %s' % (self.channel, value.name)) self.sendRecv('C%d:BSWV WVTP, %s' % (self.channel, value.name))
return value return value
#signal amplitude parameter # signal amplitude parameter
def read_amp(self): def read_amp(self):
return self.sendRecv('C%d:BSWV AMP?' % self.channel) return self.sendRecv('C%d:BSWV AMP?' % self.channel)
def write_amp(self,value): def write_amp(self, value):
self.sendRecv('C%d:BSWV AMP, %g' % (self.channel, value)) self.sendRecv('C%d:BSWV AMP, %g' % (self.channel, value))
return value return value
#offset value parameter # offset value parameter
def read_offset(self): def read_offset(self):
return self.sendRecv('C%d:BSWV OFST?' % self.channel) return self.sendRecv('C%d:BSWV OFST?' % self.channel)
@ -86,44 +74,41 @@ class Channel(Module):
self.sendRecv('C%d:BSWV OFST %g' % (self.channel, value)) self.sendRecv('C%d:BSWV OFST %g' % (self.channel, value))
return value return value
# channel symmetry
# channel symmetry
def read_symm(self): def read_symm(self):
return self.sendRecv('C%d:BSWV SYM?' % self.channel) return self.sendRecv('C%d:BSWV SYM?' % self.channel)
def write_symm(self, value): def write_symm(self, value):
self.comm('C%d:BSWV SYM %g' % (self.channel, value)) self.sendRecv('C%d:BSWV SYM %g' % (self.channel, value))
return value return value
# wave phase parameter # wave phase parameter
def read_phase(self): def read_phase(self):
return self.sendRecv('C%d:BSWV PHSE?' % self.channel) return self.sendRecv('C%d:BSWV PHSE?' % self.channel)
def write_phase(self, value): def write_phase(self, value):
self.sendRecv('C%d:BSWV PHSE %g' % (self.channel, str(value))) self.sendRecv('C%d:BSWV PHSE %g' % (self.channel, str(value)))
return value return value
# dis/enable output channel
# dis/enable output channel
def read_enabled(self): def read_enabled(self):
return self.sendRecv('C%d: OUTP?' % self.channel) return self.sendRecv('C%d: OUTP?' % self.channel)
def write_enabled(self, value): def write_enabled(self, value):
self.sendRecv('C%d: OUTP %s' % (self.channel, value.name)) self.sendRecv('C%d: OUTP %s' % (self.channel, value.name))
return value return value
# devices are defined as arg less output enable what is defined as arg2 # devices are defined as arg less output enable what is defined as arg2
class arg(Readable): class arg(Readable):
pollerClass = None pollerClass = None
parameters = {
'value': Override(datatype=FloatRange(unit='')), value = Parameter(datatype=FloatRange(unit=''))
}
class arg2(Readable): class arg2(Readable):
pollerClass = None pollerClass = None
parameters = {
'value': Override(datatype=BoolType(unit='')), value = Parameter(datatype=BoolType())
}

View File

@ -20,262 +20,29 @@
# ***************************************************************************** # *****************************************************************************
"""SIGNAL RECOVERY SR7270: lOCKIN AMPLIFIER FOR AC SUSCEPTIBILITY""" """SIGNAL RECOVERY SR7270: lOCKIN AMPLIFIER FOR AC SUSCEPTIBILITY"""
from secop.core import Readable, Parameter, Override, Command, FloatRange, TupleOf, \ from secop.core import FloatRange, HasIodev, \
HasIodev, StringIO, Done, Attached, IntRange, BoolType, EnumType Parameter, Readable, StringIO, TupleOf
class SR7270(StringIO): class SR7270(StringIO):
end_of_line = b'\x00' # end_of_line = '\x00' #termination line from maanual page 6.8
end_of_line = '\n'
def do_communicate(self, command): #remove dash from terminator
reply = StringIO.do_communicate(self, command)
status = self._conn.readbytes(2, 0.1) # get the 2 status bytes class XY(HasIodev, Readable):
# print('comm=',command,'reply=',reply,'status=',status) value = Parameter('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V')))
return reply + ';%d;%d' % tuple(status) freq = Parameter('exc_freq_int', FloatRange(0.001,250e3,unit='Hz'), readonly=False, default=100)
class XY(HasIodev, Readable):
properties = {
'x': Attached(),
'y': Attached(),
'freq_arg': Attached(),
'amp_arg': Attached(),
'tc_arg': Attached(),
'phase_arg': Attached(),
'dac_arg': Attached(),
}#parameters required an initial value but initwrite write the default value for polled parameters
parameters = {
'value': Override('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V'))),
'freq': Parameter('exc_freq_int',
FloatRange(0.001,250e3,unit='Hz'),
poll=True, readonly=False, initwrite=True, default=1000),
'amp': Parameter('exc_volt_int',
FloatRange(0.00,5,unit='Vrms'),
poll=True, readonly=False, initwrite=True, default=0.1),
'range': Parameter('sensitivity value', FloatRange(0.00,1,unit='V'), poll=True, default=1),
'irange': Parameter('sensitivity index', IntRange(0,27), poll=True, readonly=False, default=25),
'autorange': Parameter('autorange_on', EnumType('autorange', off=0, soft=1, hard=2), readonly=False, default=0, initwrite=True),
'tc': Parameter('time constant value', FloatRange(10e-6,100,unit='s'), poll=True, default=0.1),
'itc': Parameter('time constant index', IntRange(0,30), poll=True, readonly=False, initwrite=True, default=14),
'nm': Parameter ('noise mode',BoolType(), readonly=False, default=0),
'phase': Parameter('Reference phase control', FloatRange(-360,360,unit='deg'), poll=True, readonly=False, initwrite=True, default=0),
'vmode' : Parameter('Voltage input configuration', IntRange(0,3), readonly=False, default=3),
# 'dac': Parameter ('output DAC channel value', datatype=TupleOf(IntRange(1,4), FloatRange(0.00,5000,unit='mV')), poll=True, readonly=False, initwrite=True, default=(3,0)),
'dac': Parameter ('output DAC channel value', FloatRange(-10000,10000,unit='mV'), poll=True, readonly=False, initwrite=True, default=0),
}
commands = {
'aphase': Command('auto phase'),
}
iodevClass = SR7270 iodevClass = SR7270
def comm(self, command):
reply, status, overload = self.sendRecv(command).split(';')
if overload != '0':
self.status = self.Status.WARN, 'overload %s' % overload
else:
self.status = self.Status.IDLE, ''
return reply
def read_value(self): def read_value(self):
reply = self.comm('XY.').split(',') reply = self.sendRecv('XY.').split('\x00')[-1]
x = float(reply[0]) return reply.split(',')
y = float(reply[1])
if self.autorange == 1: # soft
if max(abs(x), abs(y)) >= 0.9*self.range and self.irange < 27:
self.write_irange(self.irange+1)
elif max(abs(x), abs(y)) <= 0.3*self.range and self.irange > 1:
self.write_irange(self.irange-1)
self._x.value = x # to update X,Y classes which will be the collected data.
self._y.value = y
# print(x,y)
self._freq_arg.value = self.freq
self._amp_arg.value = self.amp
self._tc_arg.value = self.tc
self._phase_arg.value = self.phase
self._dac_arg.value = self.dac
return x,y
def read_freq(self): def read_freq(self):
reply = self.comm('OF.') reply = self.sendRecv('OF.').split('\x00')[-1]
return reply return reply
def write_freq(self,value): def write_freq(self,value):
self.comm('OF. %g' % value) self.sendRecv('OF. %g' % value)
return value return value
def write_autorange(self, value):
if value == 2: # hard
self.comm('AS') # put hardware autorange on
self.comm('AUTOMATIC. 1')
else:
self.comm('AUTOMATIC. 0')
return value
def read_autorange(self):
reply=self.comm('AUTOMATIC')
# determine hardware autorange
if reply == 1: #"hardware auto range is on":
return 2 # hard
if self.autorange == 0: # soft
return self.autorange() #read autorange
return reply # off
#oscillator amplitude module
def read_amp(self):
reply = self.comm('OA.')
return reply
def write_amp(self,value):
self.comm('OA. %g' % value)
return value
#external output DAC
def read_dac(self):
# reply = self.comm('DAC %g' % channel) # failed to add the DAC channel you want to control
reply = self.comm('DAC 3') #stack to channel 3
return reply
def write_dac(self,value):
#self.comm('DAC %g %g' % channel % value)
self.comm('DAC 3 %g' % value)
return value
#sensitivity module
def read_range(self):
reply = self.comm('SEN.')
return reply
def write_irange(self,value):
self.comm('SEN %g' % value)
self.read_range()
return value
def read_irange(self):
reply = self.comm('SEN')
return reply
#time constant module/ noisemode off or 0 allows to use all the time constant range
def read_nm(self):
reply = self.comm('NOISEMODE')
return reply
def write_nm(self,value):
self.comm('NOISEMODE %d' % int(value))
self.read_nm()
return value
def read_tc(self):
reply = self.comm('TC.')
return reply
def write_itc(self,value):
self.comm('TC %g' % value)
self.read_tc()
return value
def read_itc(self):
reply = self.comm('TC')
return reply
#phase and autophase
def read_phase(self):
reply = self.comm('REFP.')
return reply
def write_phase(self,value):
self.comm('REFP %d' % round(1000*value,0))
self.read_phase()
return value
def do_aphase(self):
self.read_phase()
reply = self.comm('AQN')
self.read_phase()
#voltage input configuration 0:grounded,1=A,2=B,3=A-B
# def read_vmode(self):
# reply = self.comm('VMODE')
# return reply
def write_vmode(self,value):
self.comm('VMODE %d' % value)
# self.read_vmode()
return value
class Comp(Readable):
pollerClass = None
parameters = {
'value': Override(datatype=FloatRange(unit='V')),
}
class arg(Readable):
pollerClass = None
parameters = {
'value': Override(datatype=FloatRange(unit='')),
}
# parameters = {
# 'valueX': Override('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V'))),
#}
#iodevClass = SR7270
# def read_valueX(self):
# reply = self.sendRecv('XY.')
# return reply.split(',')[0]
# def read_valueY(self):
# reply = self.sendRecv('XY.')
# return reply.split(',')[1]
#class aphase(self):
# reply = self.sendRecv('ASM')
# return reply
# def asens(self):
# reply = self.sendRecv('AS')
# return reply
# def write_Fstart(self,value):
# self.sendRecv('FSTART. %g' % value)
# return value
# def write_Fstop(self,value):
# self.sendRecv('FSTOP. %g' % value)
# return value
# def write_Fstep(self,value):
# self.sendRecv('FSTEP. %g' % value)
# return value
# def write_Astart(self,value):
# self.sendRecv('ASTART. %g' % value')
# return value
# def write_Astop(self,value):
# self.sendRecv('ASTOP. %g' % value)
# return value
# def write_Astep(self,value):
# self.sendRecv('ASTEP. %g' % value)
# return value

View File

@ -20,7 +20,7 @@
# ***************************************************************************** # *****************************************************************************
"""Andeen Hagerling capacitance bridge""" """Andeen Hagerling capacitance bridge"""
from secop.core import Readable, Parameter, Override, FloatRange, HasIodev, StringIO, Done from secop.core import Done, FloatRange, HasIodev, Parameter, Readable, StringIO
class Ah2700IO(StringIO): class Ah2700IO(StringIO):
@ -29,12 +29,12 @@ class Ah2700IO(StringIO):
class Capacitance(HasIodev, Readable): class Capacitance(HasIodev, Readable):
parameters = {
'value': Override('capacitance', FloatRange(unit='pF'), poll=True), value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0), freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0), voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
'loss': Parameter('loss', FloatRange(unit='deg'), default=0), loss = Parameter('loss', FloatRange(unit='deg'), default=0)
}
iodevClass = Ah2700IO iodevClass = Ah2700IO
def parse_reply(self, reply): def parse_reply(self, reply):

View File

@ -20,7 +20,7 @@
# ***************************************************************************** # *****************************************************************************
"""Delay generator stanford 645""" """Delay generator stanford 645"""
from secop.core import Module, Parameter, Override, FloatRange, HasIodev, StringIO, Done from secop.core import FloatRange, HasIodev, Module, Parameter, StringIO
class DG645(StringIO): class DG645(StringIO):
@ -28,12 +28,12 @@ class DG645(StringIO):
class Delay(HasIodev, Module): class Delay(HasIodev, Module):
parameters = {
'on1': Parameter('on delay 1', FloatRange(unit='sec'), readonly=False, default=0), on1 = Parameter('on delay 1', FloatRange(unit='sec'), readonly=False, default=0)
'off1': Parameter('off delay 1', FloatRange(unit='sec'), readonly=False, default=60e-9), off1 = Parameter('off delay 1', FloatRange(unit='sec'), readonly=False, default=60e-9)
'on2': Parameter('on delay 2', FloatRange(unit='sec'), readonly=False, default=0), on2 = Parameter('on delay 2', FloatRange(unit='sec'), readonly=False, default=0)
'off2': Parameter('off delay 2', FloatRange(unit='sec'), readonly=False, default=150e-9), off2 = Parameter('off delay 2', FloatRange(unit='sec'), readonly=False, default=150e-9)
}
iodevClass = DG645 iodevClass = DG645
def read_on1(self): def read_on1(self):

View File

@ -22,8 +22,8 @@
not tested yet""" not tested yet"""
from secop.core import Writable, Module, Parameter, Override, Attached,\ from secop.core import Attached, BoolType, EnumType, FloatRange, \
BoolType, FloatRange, EnumType, HasIodev, StringIO HasIodev, Module, Parameter, StringIO, Writable
class K2601bIO(StringIO): class K2601bIO(StringIO):
@ -42,13 +42,13 @@ SOURCECMDS = {
class SourceMeter(HasIodev, Module): class SourceMeter(HasIodev, Module):
parameters = {
'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True), resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
'power': Parameter('readback power', FloatRange(unit='W'), poll=True), power = Parameter('readback power', FloatRange(unit='W'), poll=True)
'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
readonly=False, default=0), readonly=False, default=0)
'active': Parameter('output enable', BoolType(), readonly=False, poll=True), active = Parameter('output enable', BoolType(), readonly=False, poll=True)
}
iodevClass = K2601bIO iodevClass = K2601bIO
def read_resistivity(self): def read_resistivity(self):
@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module):
class Current(HasIodev, Writable): class Current(HasIodev, Writable):
properties = { sourcemeter = Attached()
'sourcemeter': Attached(),
} value = Parameter('measured current', FloatRange(unit='A'), poll=True)
parameters = { target = Parameter('set current', FloatRange(unit='A'), poll=True)
'value': Override('measured current', FloatRange(unit='A'), poll=True), active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage
'target': Override('set current', FloatRange(unit='A'), poll=True), limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
'active': Parameter('current is controlled', BoolType(), default=False), # polled from Current/Voltage
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True),
}
def read_value(self): def read_value(self):
return self.sendRecv('print(smua.measure.i())') return self.sendRecv('print(smua.measure.i())')
@ -120,15 +117,12 @@ class Current(HasIodev, Writable):
class Voltage(HasIodev, Writable): class Voltage(HasIodev, Writable):
properties = { sourcemeter = Attached()
'sourcemeter': Attached(),
} value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
parameters = { target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
'value': Override('measured voltage', FloatRange(unit='V'), poll=True), active = Parameter('voltage is controlled', BoolType(), poll=True)
'target': Override('set voltage', FloatRange(unit='V'), poll=True), limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
'active': Parameter('voltage is controlled', BoolType(), poll=True),
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True),
}
def read_value(self): def read_value(self):
return self.sendRecv('print(smua.measure.v())') return self.sendRecv('print(smua.measure.v())')
@ -159,7 +153,7 @@ class Voltage(HasIodev, Writable):
def write_active(self, value): def write_active(self, value):
if self._sourcemeter.mode != 2: if self._sourcemeter.mode != 2:
if value: if value:
self._sourcemeter.write_mode(2) # switch to voltage self._sourcemeter.write_mode(2) # switch to voltage
else: else:
return 0 return 0
return self._sourcemeter.write_active(value) return self._sourcemeter.write_active(value)

View File

@ -22,13 +22,13 @@
import time import time
from secop.modules import Readable, Drivable, Parameter, Override, Property, Attached
from secop.metaclass import Done
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
from secop.stringio import HasIodev
from secop.poller import Poller, REGULAR
from secop.lib import formatStatusBits
import secop.iohandler import secop.iohandler
from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
from secop.lib import formatStatusBits
from secop.modules import Attached, Done, \
Drivable, Parameter, Property, Readable
from secop.poller import REGULAR, Poller
from secop.stringio import HasIodev
Status = Drivable.Status Status = Drivable.Status
@ -59,19 +59,18 @@ class StringIO(secop.stringio.StringIO):
class Main(HasIodev, Drivable): class Main(HasIodev, Drivable):
parameters = {
'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)), value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
'target': Override('channel to select', datatype=IntRange(0, 17)), target = Parameter('channel to select', datatype=IntRange(0, 17))
'autoscan': autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False), pollinterval = Parameter('sleeptime between polls', default=1)
'pollinterval': Override('sleeptime between polls', default=1),
}
pollerClass = Poller pollerClass = Poller
iodevClass = StringIO iodevClass = StringIO
_channel_changed = 0 # time of last channel change
_channels = None # dict <channel no> of <module object>
def earlyInit(self): def earlyInit(self):
self._channel_changed = 0
self._channels = {} self._channels = {}
def register_channel(self, modobj): def register_channel(self, modobj):
@ -85,10 +84,8 @@ class Main(HasIodev, Drivable):
def read_value(self): def read_value(self):
channel, auto = scan.send_command(self) channel, auto = scan.send_command(self)
# response = self.sendRecv('SCAN?').strip().split(',')
# channel, auto = (int(s) for s in response)
if channel not in self._channels: if channel not in self._channels:
return channel return channel
if not self._channels[channel].enabled: if not self._channels[channel].enabled:
# channel was disabled recently, but still selected # channel was disabled recently, but still selected
nextchannel = 0 nextchannel = 0
@ -129,61 +126,42 @@ class ResChannel(HasIodev, Readable):
RES_RANGE = {key: i+1 for i, key in list( RES_RANGE = {key: i+1 for i, key in list(
enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm'] enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm']
for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]} for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]}
RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7, 16)] # RES_SCALE[0] is not used RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7, 16)] # RES_SCALE[0] is not used
CUR_RANGE = {key: i + 1 for i, key in list( CUR_RANGE = {key: i + 1 for i, key in list(
enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA'] enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA']
for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]} for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]}
VOLT_RANGE = {key: i + 1 for i, key in list( VOLT_RANGE = {key: i + 1 for i, key in list(
enumerate(mag % val for mag in ['%guV', '%gmV'] enumerate(mag % val for mag in ['%guV', '%gmV']
for val in [2, 6.32, 20, 63.2, 200, 632]))} for val in [2, 6.32, 20, 63.2, 200, 632]))}
pollerClass = Poller pollerClass = Poller
iodevClass = StringIO iodevClass = StringIO
_main = None # main module
_last_range_change = 0 # time of last range change
properties = { channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
'channel': main = Attached()
Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False),
'main':
Attached()
}
parameters = { value = Parameter(datatype=FloatRange(unit='Ohm'))
'value': pollinterval = Parameter(visibility=3)
Override(datatype=FloatRange(unit='Ohm')), range = Parameter('reading range', readonly=False,
'pollinterval': datatype=EnumType(**RES_RANGE), handler=rdgrng)
Override(visibility=3), minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
'range': datatype=EnumType(**RES_RANGE))
Parameter('reading range', readonly=False, autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
datatype=EnumType(**RES_RANGE), handler=rdgrng), readonly=False, handler=rdgrng, default=2)
'minrange': iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng)
Parameter('minimum range for software autorange', readonly=False, default=1, vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng)
datatype=EnumType(**RES_RANGE)), enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset)
'autorange': pause = Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset)
Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2), dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset)
readonly=False, handler=rdgrng, default=2), filter = Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl)
'iexc':
Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng),
'vexc':
Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng),
'enabled':
Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset),
'pause':
Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset),
'dwell':
Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset),
'filter':
Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl),
}
def initModule(self): def initModule(self):
self._main = self.DISPATCHER.get_module(self.main) self._main = self.DISPATCHER.get_module(self.main)
self._main.register_channel(self) self._main.register_channel(self)
def startModule(self, started_callback):
self._last_range_change = 0
super().startModule(started_callback)
def read_value(self): def read_value(self):
if self.channel != self._main.value: if self.channel != self._main.value:
return Done return Done
@ -195,7 +173,7 @@ class ResChannel(HasIodev, Readable):
if self.autorange == 'soft': if self.autorange == 'soft':
now = time.time() now = time.time()
if now > self._last_range_change + self.pause: if now > self._last_range_change + self.pause:
rng = int(max(self.minrange, self.range)) # convert from enum to int rng = int(max(self.minrange, self.range)) # convert from enum to int
if self.status[1] == '': if self.status[1] == '':
if abs(result) > self.RES_SCALE[rng]: if abs(result) > self.RES_SCALE[rng]:
if rng < 22: if rng < 22:
@ -236,8 +214,6 @@ class ResChannel(HasIodev, Readable):
result = dict(range=rng) result = dict(range=rng)
if autorange: if autorange:
result['autorange'] = 'hard' result['autorange'] = 'hard'
#elif self.autorange == 'hard':
# result['autorange'] = 'soft'
# else: do not change autorange # else: do not change autorange
self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange)) self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
if excoff: if excoff:

View File

@ -22,6 +22,7 @@
from secop.modules import Communicator from secop.modules import Communicator
class Ls370Sim(Communicator): class Ls370Sim(Communicator):
CHANNEL_COMMANDS = [ CHANNEL_COMMANDS = [
('RDGR?%d', '1.0'), ('RDGR?%d', '1.0'),
@ -32,9 +33,8 @@ class Ls370Sim(Communicator):
] ]
OTHER_COMMANDS = [ OTHER_COMMANDS = [
('*IDN?', 'LSCI,MODEL370,370184,05302003'), ('*IDN?', 'LSCI,MODEL370,370184,05302003'),
('SCAN?', '1,1'), ('SCAN?', '3,1'),
] ]
channel = [None]
def earlyInit(self): def earlyInit(self):
self._data = dict(self.OTHER_COMMANDS) self._data = dict(self.OTHER_COMMANDS)
@ -43,7 +43,7 @@ class Ls370Sim(Communicator):
self._data[fmt % chan] = v self._data[fmt % chan] = v
# mkthread(self.run) # mkthread(self.run)
def do_communicate(self, command): def communicate(self, command):
# simulation part, time independent # simulation part, time independent
for channel in range(1,17): for channel in range(1,17):
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')

View File

@ -31,20 +31,19 @@ Polling of value and status is done commonly for all modules. For each registere
<module>.update_value_status() is called in order to update their value and status. <module>.update_value_status() is called in order to update their value and status.
""" """
import time
import threading import threading
import time
from secop.modules import Module, Readable, Drivable, Parameter, Override,\
Communicator, Property, Attached
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
BoolType, StatusType
from secop.lib.enum import Enum
from secop.lib import clamp
from secop.errors import HardwareError
from secop.poller import Poller
import secop.iohandler import secop.iohandler
from secop.datatypes import BoolType, EnumType, \
FloatRange, IntRange, StatusType, StringType
from secop.errors import HardwareError
from secop.lib import clamp
from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, Done, \
Drivable, Parameter, Property, Readable
from secop.poller import Poller
from secop.stringio import HasIodev from secop.stringio import HasIodev
from secop.metaclass import Done
try: try:
import secop_psi.ppmswindows as ppmshw import secop_psi.ppmswindows as ppmshw
@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler):
class Main(Communicator): class Main(Communicator):
"""ppms communicator module""" """ppms communicator module"""
parameters = { pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
'pollinterval': Parameter('poll interval', readonly=False, data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
datatype=FloatRange(), default=2), default="", readonly=True)
'communicate': Override('GBIP command'),
'data': Parameter('internal', poll=True, export=True, # export for test only
default="", readonly=True, datatype=StringType()),
}
properties = {
'class_id': Property('Quantum Design class id', export=False,
datatype=StringType()),
}
_channel_names = ['packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2', class_id = Property('Quantum Design class id', StringType(), export=False)
_channel_names = [
'packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2',
'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2', 'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2',
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29'] 'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
assert len(_channel_names) == 30 assert len(_channel_names) == 30
@ -102,7 +96,8 @@ class Main(Communicator):
def register(self, other): def register(self, other):
self.modules[other.channel] = other self.modules[other.channel] = other
def do_communicate(self, command): def communicate(self, command):
"""GPIB command"""
with self.lock: with self.lock:
reply = self._ppms_device.send(command) reply = self._ppms_device.send(command)
self.log.debug("%s|%s", command, reply) self.log.debug("%s|%s", command, reply)
@ -114,7 +109,7 @@ class Main(Communicator):
if channel.enabled: if channel.enabled:
mask |= 1 << self._channel_to_index.get(channelname, 0) mask |= 1 << self._channel_to_index.get(channelname, 0)
# send, read and convert to floats and ints # send, read and convert to floats and ints
data = self.do_communicate('GETDAT? %d' % mask) data = self.communicate('GETDAT? %d' % mask)
reply = data.split(',') reply = data.split(',')
mask = int(reply.pop(0)) mask = int(reply.pop(0))
reply.pop(0) # pop timestamp reply.pop(0) # pop timestamp
@ -133,23 +128,23 @@ class Main(Communicator):
return data # return data as string return data # return data as string
class PpmsMixin(HasIodev, Module): class PpmsBase(HasIodev, Readable):
"""common methods for ppms modules""" """common base for all ppms modules"""
iodev = Attached()
parameters = {
'pollinterval': None,
}
pollerClass = Poller pollerClass = Poller
enabled = True # default, if no parameter enable is defined enabled = True # default, if no parameter enable is defined
_last_settings = None # used by several modules _last_settings = None # used by several modules
slow_pollfactor = 1 slow_pollfactor = 1
# as this pollinterval affects only the polling of settings
# it would be confusing to export it.
pollinterval = Parameter(export=False)
def initModule(self): def initModule(self):
self._iodev.register(self) self._iodev.register(self)
def startModule(self, started_callback): def startModule(self, started_callback):
""""""
# no polls except on main module # no polls except on main module
started_callback() started_callback()
@ -160,8 +155,8 @@ class PpmsMixin(HasIodev, Module):
def read_status(self): def read_status(self):
# polling is done by the main module # polling is done by the main module
# and PPMS does not deliver really fresh status values anyway: # and PPMS does not deliver really fresh status values anyway: the status is not
# e.g. the status is not changed immediately after a target change! # changed immediately after a target change!
return Done return Done
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
@ -177,29 +172,22 @@ class PpmsMixin(HasIodev, Module):
self.status = (self.Status.IDLE, '') self.status = (self.Status.IDLE, '')
class Channel(PpmsMixin, Readable): class Channel(PpmsBase):
"""channel base class""" """channel base class"""
parameters = { value = Parameter('main value of channels', poll=True)
'value': enabled = Parameter('is this channel used?', readonly=False, poll=False,
Override('main value of channels', poll=True), datatype=BoolType(), default=False)
'enabled':
Parameter('is this channel used?', readonly=False, poll=False, channel = Property('channel name',
datatype=BoolType(), default=False), datatype=StringType(), export=False, default='')
} no = Property('channel number',
properties = { datatype=IntRange(1, 4), export=False)
'channel':
Property('channel name',
datatype=StringType(), export=False, default=''),
'no':
Property('channel number',
datatype=IntRange(1, 4), export=False),
}
def earlyInit(self): def earlyInit(self):
Readable.earlyInit(self) Readable.earlyInit(self)
if not self.channel: if not self.channel:
self.properties['channel'] = self.name self.channel = self.name
def get_settings(self, pname): def get_settings(self, pname):
return '' return ''
@ -208,15 +196,12 @@ class Channel(PpmsMixin, Readable):
class UserChannel(Channel): class UserChannel(Channel):
"""user channel""" """user channel"""
properties = { # pollinterval = Parameter(visibility=3)
'no':
Property('*(unused)*',
datatype=IntRange(0, 0), export=False, default=0),
'linkenable':
Property('name of linked channel for enabling',
datatype=StringType(), export=False, default=''),
} no = Property('channel number',
datatype=IntRange(0, 0), export=False, default=0)
linkenable = Property('name of linked channel for enabling',
datatype=StringType(), export=False, default='')
def write_enabled(self, enabled): def write_enabled(self, enabled):
other = self._iodev.modules.get(self.linkenable, None) other = self._iodev.modules.get(self.linkenable, None)
@ -230,14 +215,11 @@ class DriverChannel(Channel):
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
parameters = { current = Parameter('driver current', readonly=False, handler=drvout,
'current': datatype=FloatRange(0., 5000., unit='uA'))
Parameter('driver current', readonly=False, handler=drvout, powerlimit = Parameter('power limit', readonly=False, handler=drvout,
datatype=FloatRange(0., 5000., unit='uA')), datatype=FloatRange(0., 1000., unit='uW'))
'powerlimit': # pollinterval = Parameter(visibility=3)
Parameter('power limit', readonly=False, handler=drvout,
datatype=FloatRange(0., 1000., unit='uW')),
}
def analyze_drvout(self, no, current, powerlimit): def analyze_drvout(self, no, current, powerlimit):
if self.no != no: if self.no != no:
@ -255,25 +237,19 @@ class BridgeChannel(Channel):
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
# pylint: disable=invalid-name # pylint: disable=invalid-name
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
parameters = {
'enabled': enabled = Parameter(handler=bridge)
Override(handler=bridge), excitation = Parameter('excitation current', readonly=False, handler=bridge,
'excitation': datatype=FloatRange(0.01, 5000., unit='uA'))
Parameter('excitation current', readonly=False, handler=bridge, powerlimit = Parameter('power limit', readonly=False, handler=bridge,
datatype=FloatRange(0.01, 5000., unit='uA')), datatype=FloatRange(0.001, 1000., unit='uW'))
'powerlimit': dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
Parameter('power limit', readonly=False, handler=bridge, datatype=BoolType())
datatype=FloatRange(0.001, 1000., unit='uW')), readingmode = Parameter('reading mode', readonly=False, handler=bridge,
'dcflag': datatype=EnumType(ReadingMode))
Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
datatype=BoolType()), datatype=FloatRange(0.0001, 100., unit='mV'))
'readingmode': # pollinterval = Parameter(visibility=3)
Parameter('reading mode', readonly=False, handler=bridge,
datatype=EnumType(ReadingMode)),
'voltagelimit':
Parameter('voltage limit', readonly=False, handler=bridge,
datatype=FloatRange(0.0001, 100., unit='mV')),
}
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit): def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
if self.no != no: if self.no != no:
@ -294,23 +270,22 @@ class BridgeChannel(Channel):
return self.no, 0, 0, change.dcflag, change.readingmode, 0 return self.no, 0, 0, change.dcflag, change.readingmode, 0
class Level(PpmsMixin, Readable): class Level(PpmsBase):
"""helium level""" """helium level"""
level = IOHandler('level', 'LEVEL?', '%g,%d') level = IOHandler('level', 'LEVEL?', '%g,%d')
parameters = { value = Parameter(datatype=FloatRange(unit='%'), handler=level)
'value': Override(datatype=FloatRange(unit='%'), handler=level), status = Parameter(handler=level)
'status': Override(handler=level), # pollinterval = Parameter(visibility=3)
}
channel = 'level' channel = 'level'
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
pass
# must be a no-op # must be a no-op
# when called from Main.read_data, value is always None # when called from Main.read_data, value is always None
# value and status is polled via settings # value and status is polled via settings
pass
def analyze_level(self, level, status): def analyze_level(self, level, status):
# ignore 'old reading' state of the flag, as this happens only for a short time # ignore 'old reading' state of the flag, as this happens only for a short time
@ -318,7 +293,7 @@ class Level(PpmsMixin, Readable):
return dict(value=level, status=(self.Status.IDLE, '')) return dict(value=level, status=(self.Status.IDLE, ''))
class Chamber(PpmsMixin, Drivable): class Chamber(PpmsBase, Drivable):
"""sample chamber handling """sample chamber handling
value is an Enum, which is redundant with the status text value is an Enum, which is redundant with the status text
@ -351,14 +326,13 @@ class Chamber(PpmsMixin, Drivable):
venting_continuously=9, venting_continuously=9,
general_failure=15, general_failure=15,
) )
parameters = {
'value': value = Parameter(description='chamber state', handler=chamber,
Override(description='chamber state', handler=chamber, datatype=EnumType(StatusCode))
datatype=EnumType(StatusCode)), target = Parameter(description='chamber command', handler=chamber,
'target': datatype=EnumType(Operation))
Override(description='chamber command', handler=chamber, # pollinterval = Parameter(visibility=3)
datatype=EnumType(Operation)),
}
STATUS_MAP = { STATUS_MAP = {
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'), StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'), StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
@ -387,44 +361,40 @@ class Chamber(PpmsMixin, Drivable):
return dict(target=target) return dict(target=target)
def change_chamber(self, change): def change_chamber(self, change):
# write settings, combining <pname>=<value> and current attributes
# and request updated settings
if change.target == self.Operation.noop: if change.target == self.Operation.noop:
return None return None
return (change.target,) return (change.target,)
class Temp(PpmsMixin, Drivable): class Temp(PpmsBase, Drivable):
"""temperature""" """temperature"""
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d') temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
Status = Enum(Drivable.Status, Status = Enum(
RAMPING = 370, Drivable.Status,
STABILIZING = 380, RAMPING=370,
STABILIZING=380,
) )
# pylint: disable=invalid-name # pylint: disable=invalid-name
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
parameters = {
'value': value = Parameter(datatype=FloatRange(unit='K'), poll=True)
Override(datatype=FloatRange(unit='K'), poll=True), status = Parameter(datatype=StatusType(Status), poll=True)
'status': target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
Override(datatype=StatusType(Status), poll=True), setpoint = Parameter('intermediate set point',
'target': datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False), ramp = Parameter('ramping speed', readonly=False, default=0,
'setpoint': datatype=FloatRange(0, 20, unit='K/min'))
Parameter('intermediate set point', workingramp = Parameter('intermediate ramp value',
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp), datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
'ramp': approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
Parameter('ramping speed', readonly=False, default=0, datatype=EnumType(ApproachMode))
datatype=FloatRange(0, 20, unit='K/min')), # pollinterval = Parameter(visibility=3)
'workingramp': timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
Parameter('intermediate ramp value', datatype=FloatRange(0, unit='sec'), default=3600)
datatype=FloatRange(0, 20, unit='K/min'), handler=temp),
'approachmode':
Parameter('how to approach target!', readonly=False, handler=temp,
datatype=EnumType(ApproachMode)),
'timeout':
Parameter('drive timeout, in addition to ramp time', readonly=False,
datatype=FloatRange(0, unit='sec'), default=3600),
}
# pylint: disable=invalid-name # pylint: disable=invalid-name
TempStatus = Enum( TempStatus = Enum(
'TempStatus', 'TempStatus',
@ -449,17 +419,14 @@ class Temp(PpmsMixin, Drivable):
14: (Status.ERROR, 'can not complete'), 14: (Status.ERROR, 'can not complete'),
15: (Status.ERROR, 'general failure'), 15: (Status.ERROR, 'general failure'),
} }
properties = { general_stop = Property('respect general stop', datatype=BoolType(),
'general_stop': Property('respect general stop', datatype=BoolType(), default=True, value=False)
export=True, default=True)
}
channel = 'temp' channel = 'temp'
_stopped = False _stopped = False
_expected_target_time = 0 _expected_target_time = 0
_last_change = 0 # 0 means no target change is pending _last_change = 0 # 0 means no target change is pending
_last_target = None # last reached target _last_target = None # last reached target
general_stop = False
_cool_deadline = 0 _cool_deadline = 0
_wait_at10 = False _wait_at10 = False
_ramp_at_limit = False _ramp_at_limit = False
@ -573,7 +540,7 @@ class Temp(PpmsMixin, Drivable):
def calc_expected(self, target, ramp): def calc_expected(self, target, ramp):
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
def do_stop(self): def stop(self):
if not self.isDriving(): if not self.isDriving():
return return
if self.status[0] != self.Status.STABILIZING: if self.status[0] != self.Status.STABILIZING:
@ -586,37 +553,31 @@ class Temp(PpmsMixin, Drivable):
self._stopped = True self._stopped = True
class Field(PpmsMixin, Drivable): class Field(PpmsBase, Drivable):
"""magnetic field""" """magnetic field"""
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d') field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
Status = Enum(Drivable.Status, Status = Enum(
PREPARED = 150, Drivable.Status,
PREPARING = 340, PREPARED=150,
RAMPING = 370, PREPARING=340,
FINALIZING = 390, RAMPING=370,
FINALIZING=390,
) )
# pylint: disable=invalid-name # pylint: disable=invalid-name
PersistentMode = Enum('PersistentMode', persistent=0, driven=1) PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2) ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
parameters = { value = Parameter(datatype=FloatRange(unit='T'), poll=True)
'value': status = Parameter(datatype=StatusType(Status), poll=True)
Override(datatype=FloatRange(unit='T'), poll=True), target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
'status': ramp = Parameter('ramping speed', readonly=False, handler=field,
Override(datatype=StatusType(Status), poll=True), datatype=FloatRange(0.064, 1.19, unit='T/min'))
'target': approachmode = Parameter('how to approach target', readonly=False, handler=field,
Override(datatype=FloatRange(-15, 15, unit='T'), handler=field), datatype=EnumType(ApproachMode))
'ramp': persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
Parameter('ramping speed', readonly=False, handler=field, datatype=EnumType(PersistentMode))
datatype=FloatRange(0.064, 1.19, unit='T/min')), # pollinterval = Parameter(visibility=3)
'approachmode':
Parameter('how to approach target', readonly=False, handler=field,
datatype=EnumType(ApproachMode)),
'persistentmode':
Parameter('what to do after changing field', readonly=False, handler=field,
datatype=EnumType(PersistentMode)),
}
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'persistent mode'), 1: (Status.IDLE, 'persistent mode'),
@ -652,7 +613,7 @@ class Field(PpmsMixin, Drivable):
else: else:
status = (self.Status.WARN, 'timeout when ramping leads') status = (self.Status.WARN, 'timeout when ramping leads')
elif now > self._last_change + 5: elif now > self._last_change + 5:
self._last_change = 0 # give up waiting for driving self._last_change = 0 # give up waiting for driving
elif self.isDriving(status) and status != self._status_before_change: elif self.isDriving(status) and status != self._status_before_change:
self._last_change = 0 self._last_change = 0
self.log.debug('time needed to change to busy: %.3g', now - self._last_change) self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
@ -718,7 +679,7 @@ class Field(PpmsMixin, Drivable):
return Done return Done
return None # do not execute FIELD command, as this would trigger a ramp up of leads current return None # do not execute FIELD command, as this would trigger a ramp up of leads current
def do_stop(self): def stop(self):
if not self.isDriving(): if not self.isDriving():
return return
newtarget = clamp(self._last_target, self.value, self.target) newtarget = clamp(self._last_target, self.value, self.target)
@ -729,23 +690,20 @@ class Field(PpmsMixin, Drivable):
self._stopped = True self._stopped = True
class Position(PpmsMixin, Drivable): class Position(PpmsBase, Drivable):
"""rotator position""" """rotator position"""
move = IOHandler('move', 'MOVE?', '%g,%g,%g') move = IOHandler('move', 'MOVE?', '%g,%g,%g')
Status = Drivable.Status Status = Drivable.Status
parameters = {
'value': value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
Override(datatype=FloatRange(unit='deg'), poll=True), target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
'target': enabled = Parameter('is this channel used?', readonly=False, poll=False,
Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move), datatype=BoolType(), default=True)
'enabled': speed = Parameter('motor speed', readonly=False, handler=move,
Parameter('is this channel used?', readonly=False, poll=False, datatype=FloatRange(0.8, 12, unit='deg/sec'))
datatype=BoolType(), default=True), # pollinterval = Parameter(visibility=3)
'speed':
Parameter('motor speed', readonly=False, handler=move,
datatype=FloatRange(0.8, 12, unit='deg/sec')),
}
STATUS_MAP = { STATUS_MAP = {
1: (Status.IDLE, 'at target'), 1: (Status.IDLE, 'at target'),
5: (Status.BUSY, 'moving'), 5: (Status.BUSY, 'moving'),
@ -824,7 +782,7 @@ class Position(PpmsMixin, Drivable):
self.speed = value self.speed = value
return None # do not execute MOVE command, as this would trigger an unnecessary move return None # do not execute MOVE command, as this would trigger an unnecessary move
def do_stop(self): def stop(self):
if not self.isDriving(): if not self.isDriving():
return return
newtarget = clamp(self._last_target, self.value, self.target) newtarget = clamp(self._last_target, self.value, self.target)

View File

@ -20,9 +20,9 @@
# ***************************************************************************** # *****************************************************************************
"""PPMS mf proxy""" """PPMS mf proxy"""
from secop.core import Enum, FloatRange, EnumType, Override, Parameter, Drivable
from secop.datatypes import StatusType
import secop_psi.ppms import secop_psi.ppms
from secop.core import Drivable, Enum, EnumType, FloatRange, Override, Parameter
from secop.datatypes import StatusType
from secop.proxy import proxy_class from secop.proxy import proxy_class

View File

@ -18,9 +18,10 @@
# Module authors: # Module authors:
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# ***************************************************************************** # *****************************************************************************
import time
import json import json
import math import math
import time
def num(string): def num(string):
return json.loads(string) return json.loads(string)

View File

@ -32,21 +32,21 @@ t1:raw tt t1/raw /tt/t1/raw tt t1 raw /tt/t1
rx:bla rx bla /some/rx_a/bla rx bla /some/rx_a rx:bla rx bla /some/rx_a/bla rx bla /some/rx_a
""" """
import json
import threading import threading
import time import time
import json from os.path import expanduser, join
from os.path import join, expanduser
from secop.modules import Module, Parameter, Command, Override, Drivable, Readable, Writable, Property, Attached
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, IntRange, EnumType
from secop.lib import mkthread, getGeneralConfig
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.metaclass import ModuleMeta, Done
from secop.errors import HardwareError, secop_error, ConfigError
from secop.client import ProxyClient from secop.client import ProxyClient
from secop.datatypes import ArrayOf, BoolType, \
EnumType, FloatRange, IntRange, StringType
from secop.errors import ConfigError, HardwareError, secop_error
from secop.lib import getGeneralConfig, mkthread
from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Attached, Command, Done, Drivable, \
Module, Parameter, Property, Readable, Writable
from secop.protocol.dispatcher import make_update from secop.protocol.dispatcher import make_update
CFG_HEADER = """[NODE] CFG_HEADER = """[NODE]
id = %(samenv)s.psi.ch id = %(samenv)s.psi.ch
description = %(samenv)s over SEA description = %(samenv)s over SEA
@ -76,7 +76,7 @@ def get_sea_port(instance):
for line in f: for line in f:
linesplit = line.split() linesplit = line.split()
if len(linesplit) == 3: if len(linesplit) == 3:
cmd, var, value = line.split() _, var, value = line.split()
if var == 'serverport': if var == 'serverport':
return value return value
except FileNotFoundError: except FileNotFoundError:
@ -87,23 +87,10 @@ def get_sea_port(instance):
class SeaClient(ProxyClient, Module): class SeaClient(ProxyClient, Module):
"""connection to SEA""" """connection to SEA"""
properties = { json_path = Property('path to SEA json descriptors', StringType())
'json_path': Property('path to SEA json descriptors',
datatype=StringType(), uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000')
default=join(expanduser('~'), 'sea/tcl/json')) timeout = Parameter('timeout', datatype=FloatRange(0), default=10)
}
parameters = {
'uri':
Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000'),
'timeout':
Parameter('timeout', datatype=FloatRange(0), default=10),
}
commands = {
'communicate':
Command('send a command to SEA', argument=StringType(), result=StringType()),
'describe':
Command('save objects (and sub-objects) description', result=StringType()),
}
def __init__(self, name, log, opts, srv): def __init__(self, name, log, opts, srv):
instance = srv.node_cfg['name'].rsplit('_', 1)[0] instance = srv.node_cfg['name'].rsplit('_', 1)[0]
@ -198,7 +185,7 @@ class SeaClient(ProxyClient, Module):
if msg.startswith('_E '): if msg.startswith('_E '):
try: try:
_, path, readerror = msg.split(None, 2) _, path, readerror = msg.split(None, 2)
except Exception as e: except ValueError:
continue continue
else: else:
continue continue
@ -241,11 +228,15 @@ class SeaClient(ProxyClient, Module):
# do not update unchanged values within 0.1 sec # do not update unchanged values within 0.1 sec
self.updateValue(module, param, value, now, readerror) self.updateValue(module, param, value, now, readerror)
def do_communicate(self, command): @Command
def communicate(self, command):
"""send a command to SEA"""
reply = self.request(command) reply = self.request(command)
return reply return reply
def do_describe(self): @Command(result=StringType())
def describe(self):
"""save objects (and sub-objects) description"""
reply = self.request('describe_all') reply = self.request('describe_all')
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n')) reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
samenv, reply = json.loads(reply) samenv, reply = json.loads(reply)
@ -288,9 +279,7 @@ def get_datatype(paramdesc):
class SeaModule(Module): class SeaModule(Module):
properties = { iodev = Attached()
'iodev': Attached(),
}
# pollerClass=None # pollerClass=None
path2param = None path2param = None
@ -329,8 +318,7 @@ class SeaModule(Module):
else: # take all else: # take all
main = '' main = ''
path2param = {} path2param = {}
parameters = {} attributes = dict(sea_object=sea_object, path2param=path2param)
attributes = dict(sea_object=sea_object, path2param=path2param, parameters=parameters)
for paramdesc in descr: for paramdesc in descr:
path = paramdesc['path'] path = paramdesc['path']
readonly = paramdesc.get('readonly', True) readonly = paramdesc.get('readonly', True)
@ -351,6 +339,7 @@ class SeaModule(Module):
else: else:
kwds['group'] = pathlist[-2] kwds['group'] = pathlist[-2]
# flatten path to parameter name # flatten path to parameter name
key = None
for i in reversed(range(len(pathlist))): for i in reversed(range(len(pathlist))):
key = '_'.join(pathlist[i:]) key = '_'.join(pathlist[i:])
if not key in cls.accessibles: if not key in cls.accessibles:
@ -361,12 +350,12 @@ class SeaModule(Module):
if key in cls.accessibles: if key in cls.accessibles:
if key == 'target': if key == 'target':
kwds['readonly'] = False kwds['readonly'] = False
pobj = Override(**kwds) pobj = cls.accessibles[key].override(**kwds)
datatype = kwds.get('datatype', cls.accessibles[key].datatype) datatype = kwds.get('datatype', cls.accessibles[key].datatype)
else: else:
pobj = Parameter(**kwds) pobj = Parameter(**kwds)
datatype = pobj.datatype datatype = pobj.datatype
parameters[key] = pobj attributes[key] = pobj
if not hasattr(cls, 'read_' + key): if not hasattr(cls, 'read_' + key):
def rfunc(self, cmd='hval /sics/%s/%s' % (sea_object, path)): def rfunc(self, cmd='hval /sics/%s/%s' % (sea_object, path)):
print('READ', cmd) print('READ', cmd)
@ -395,19 +384,20 @@ class SeaModule(Module):
return Done return Done
attributes['write_' + key] = wfunc attributes['write_' + key] = wfunc
# create standard parameters like value and status, if not yet there # create standard parameters like value and status, if not yet there
for pname, pobj in cls.accessibles.items(): for pname, pobj in cls.accessibles.items():
if pname == 'pollinterval': if pname == 'pollinterval':
parameters[pname] = Override(export=False) attributes[pname] = pobj.override(export=False)
elif pname not in parameters and isinstance(pobj, Parameter): elif pname not in attributes and isinstance(pobj, Parameter):
parameters[pname] = Override(poll=False, needscfg=False) attributes[pname] = pobj.override(poll=False, needscfg=False)
classname = '%s_%s' % (cls.__name__, sea_object) classname = '%s_%s' % (cls.__name__, sea_object)
newcls = ModuleMeta.__new__(ModuleMeta, classname, (cls,), attributes) newcls = type(classname, (cls,), attributes)
return Module.__new__(newcls) return Module.__new__(newcls)
def __init__(self, name, logger, cfgdict, dispatcher): # def __init__(self, name, logger, cfgdict, dispatcher):
Module.__init__(self, name, logger, cfgdict, dispatcher) # Module.__init__(self, name, logger, cfgdict, dispatcher)
def updateEvent(self, module, parameter, value, timestamp, readerror): def updateEvent(self, module, parameter, value, timestamp, readerror):
upd = getattr(self, 'update_' + parameter, None) upd = getattr(self, 'update_' + parameter, None)
@ -442,9 +432,9 @@ class SeaReadable(SeaModule, Readable):
if readerror: if readerror:
value = repr(readerror) value = repr(readerror)
if value == '': if value == '':
self.status = [self.Status.IDLE, ''] self.status = (self.Status.IDLE, '')
else: else:
self.status = [self.Status.ERROR, value] self.status = (self.Status.ERROR, value)
def read_status(self): def read_status(self):
return self.status return self.status
@ -485,11 +475,11 @@ class SeaDrivable(SeaModule, Drivable):
def updateStatus(self): def updateStatus(self):
if self._sea_status: if self._sea_status:
self.status = [self.Status.ERROR, self._sea_status] self.status = (self.Status.ERROR, self._sea_status)
elif self._is_running: elif self._is_running:
self.status = [self.Status.BUSY, 'driving'] self.status = (self.Status.BUSY, 'driving')
else: else:
self.status = [self.Status.IDLE, ''] self.status = (self.Status.IDLE, '')
def updateTarget(self, module, parameter, value, timestamp, readerror): def updateTarget(self, module, parameter, value, timestamp, readerror):
if value is not None: if value is not None:

View File

@ -21,33 +21,33 @@
"""senis hall sensor""" """senis hall sensor"""
import time
import threading import threading
import time
import numpy as np import numpy as np
from serial import Serial from serial import Serial
from secop.core import Property, Parameter, Override, Readable, BoolType, \
FloatRange, TupleOf, StringType, IntRange, Attached from secop.core import Attached, BoolType, FloatRange, IntRange, \
Parameter, Property, Readable, StringType, TupleOf
class Temperature(Readable): class Temperature(Readable):
pollerClass = None pollerClass = None
parameters = {
'value': Override(datatype=FloatRange(unit='degC')), value = Parameter(datatype=FloatRange(unit='degC'))
}
class Bcomp(Readable): class Bcomp(Readable):
pollerClass = None pollerClass = None
parameters = {
'value': Override(datatype=FloatRange(unit='T')), value = Parameter(datatype=FloatRange(unit='T'))
'range': Parameter('working range', FloatRange(unit='T'), default=0), range = Parameter('working range', FloatRange(unit='T'), default=0)
}
class Raw(Readable): class Raw(Readable):
pollerClass = None pollerClass = None
parameters = {
'value': Override(datatype=FloatRange()), value = Parameter(datatype=FloatRange())
}
class TeslameterBase(Readable): class TeslameterBase(Readable):
@ -58,18 +58,15 @@ class TeslameterBase(Readable):
the B components (and temperatures for 3MH6) are implemented as separate modules the B components (and temperatures for 3MH6) are implemented as separate modules
""" """
properties = { x = Attached()
'x': Attached(), y = Attached()
'y': Attached(), z = Attached()
'z': Attached(),
} value = Parameter('B vector', poll=True,
parameters = { datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T')))
'value': Override('B vector', poll=True, usb = Parameter('usb device', StringType(), readonly=False)
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T'))), enabled = Parameter('enable data acq', datatype=BoolType(), readonly=False, default=True)
'usb': Parameter('usb device', StringType(), readonly=False), nsample = Parameter('number of samples for average', datatype=IntRange(1, 1000), readonly=False, default=1)
'enabled': Parameter('enable data acq', datatype=BoolType(), readonly=False, default=True),
'nsample': Parameter('number of samples for average', datatype=IntRange(1, 1000), readonly=False, default=1),
}
def init_serial(self, baud): def init_serial(self, baud):
self._conn = Serial(self.usb, baud, timeout=0.1) self._conn = Serial(self.usb, baud, timeout=0.1)
@ -103,9 +100,7 @@ class Teslameter3MH3(TeslameterBase):
remark: no query for the sample rate is possible, therefore set always to remark: no query for the sample rate is possible, therefore set always to
a default rate (therefore initwrite=True on the rate parameter) a default rate (therefore initwrite=True on the rate parameter)
""" """
properties = { range = Property('full scale', datatype=FloatRange(), default=2)
'range': Property('full scale', datatype=FloatRange(), default=2),
}
def earlyInit(self): def earlyInit(self):
self.init_serial(115200) self.init_serial(115200)
@ -122,7 +117,7 @@ class Teslameter3MH3(TeslameterBase):
s.timeout = 0.1 + 0.02 * self.nsample s.timeout = 0.1 + 0.02 * self.nsample
for _ in range(2): for _ in range(2):
self.write_bytes(b'B') self.write_bytes(b'B')
t = time.time() # t = time.time()
reply = self.read_bytes(8 * self.nsample) reply = self.read_bytes(8 * self.nsample)
s.timeout = 0.1 s.timeout = 0.1
self.stop_reading() self.stop_reading()
@ -147,21 +142,19 @@ class Teslameter3MH3(TeslameterBase):
class Teslameter3MH6(TeslameterBase): class Teslameter3MH6(TeslameterBase):
"""luxury model with probe and box temperature and autorange""" """luxury model with probe and box temperature and autorange"""
properties = { x_direct = Attached()
'x_direct': Attached(), y_direct = Attached()
'y_direct': Attached(), z_direct = Attached()
'z_direct': Attached(), probe_temp = Attached()
'probe_temp': Attached(), box_temp = Attached()
'box_temp': Attached(), probe_temp_direct = Attached()
'probe_temp_direct': Attached(), box_temp_direct = Attached()
'box_temp_direct': Attached(),
} range = Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0)
parameters = { rate = Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
'range': Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0), readonly=False, poll=True)
'rate': Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'), avtime = Parameter('data acquisition time', FloatRange(), default=0)
readonly=False, poll=True),
'avtime': Parameter('data acquisition time', FloatRange(), default=0),
}
SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000, SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000,
0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10} 0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10}
RANGES = dict(zip(b'1234', [0.1, 0.5, 2, 20])) RANGES = dict(zip(b'1234', [0.1, 0.5, 2, 20]))
@ -183,7 +176,7 @@ class Teslameter3MH6(TeslameterBase):
chk = np.frombuffer(reply, dtype='i1,23i1,i1') chk = np.frombuffer(reply, dtype='i1,23i1,i1')
if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0): if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0):
status = 'checksum error' status = 'checksum error'
continue continue
# first byte must be 'B' and last byte must be CR # first byte must be 'B' and last byte must be CR
if np.all(chk['f0'] == ord(b'B')) and np.all(chk['f2'] == 13): if np.all(chk['f0'] == ord(b'B')) and np.all(chk['f2'] == 13):
break break
@ -219,7 +212,7 @@ class Teslameter3MH6(TeslameterBase):
self._z.value = mean['z'] * 0.001 self._z.value = mean['z'] * 0.001
self._probe_temp.value = mean['thc'] self._probe_temp.value = mean['thc']
self._box_temp.value = mean['tec'] self._box_temp.value = mean['tec']
self.write_bytes(b'D') # put into NONcalibrated mode self.write_bytes(b'D') # put into NONcalibrated mode
if self.read_bytes(1) != b'd': if self.read_bytes(1) != b'd':
self.log.error('missing response to D command') self.log.error('missing response to D command')

View File

@ -20,13 +20,14 @@
# ***************************************************************************** # *****************************************************************************
"""Software calibration""" """Software calibration"""
import os
from os.path import join, exists, basename
import math import math
import numpy as np import os
from scipy.interpolate import splrep, splev # pylint: disable=import-error from os.path import basename, exists, join
from secop.core import Readable, Parameter, Override, Attached, StringType, BoolType import numpy as np
from scipy.interpolate import splev, splrep # pylint: disable=import-error
from secop.core import Attached, BoolType, Parameter, Readable, StringType
def linear(x): def linear(x):
@ -102,6 +103,7 @@ class CalCurve:
sensopt = calibspec.split(',') sensopt = calibspec.split(',')
calibname = sensopt.pop(0) calibname = sensopt.pop(0)
_, dot, ext = basename(calibname).rpartition('.') _, dot, ext = basename(calibname).rpartition('.')
kind = None
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','): for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
# first try without adding kind # first try without adding kind
filename = join(path.strip(), calibname) filename = join(path.strip(), calibname)
@ -109,8 +111,8 @@ class CalCurve:
kind = ext if dot else None kind = ext if dot else None
break break
# then try adding all kinds as extension # then try adding all kinds as extension
for kind in KINDS: for nam in calibname, calibname.upper(), calibname.lower():
for nam in {calibname, calibname.upper(), calibname.lower()}: for kind in KINDS:
filename = join(path.strip(), '%s.%s' % (nam, kind)) filename = join(path.strip(), '%s.%s' % (nam, kind))
if exists(filename): if exists(filename):
break break
@ -150,16 +152,14 @@ class CalCurve:
class Sensor(Readable): class Sensor(Readable):
properties = { rawsensor = Attached()
'rawsensor': Attached(),
} calib = Parameter('calibration name', datatype=StringType(), readonly=False)
parameters = { abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True)
'calib': Parameter('calibration name', datatype=StringType(), readonly=False), value = Parameter(unit='K')
'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True), pollinterval = Parameter(export=False)
'value': Override(unit='K'), status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
'pollinterval': Override(export=False),
'status': Override(default=(Readable.Status.ERROR, 'unintialized'))
}
pollerClass = None pollerClass = None
description = 'a calibrated sensor value' description = 'a calibrated sensor value'
_value_error = None _value_error = None
@ -179,7 +179,7 @@ class Sensor(Readable):
self._value_error = None self._value_error = None
def error_update_value(self, err): def error_update_value(self, err):
if self.abs and str(err) == 'R_UNDER': if self.abs and str(err) == 'R_UNDER': # hack: ignore R_UNDER from ls370
self._value_error = None self._value_error = None
return None return None
self._value_error = repr(err) self._value_error = repr(err)

View File

@ -20,40 +20,38 @@
# ***************************************************************************** # *****************************************************************************
"""Test command arguments""" """Test command arguments"""
from secop.core import Module, Parameter, Command, FloatRange, StringType, BoolType, TupleOf, StructOf, ArrayOf from secop.core import ArrayOf, BoolType, Command, FloatRange, \
Module, Parameter, StringType, StructOf, TupleOf
class TestCmd(Module): class TestCmd(Module):
commands = { struct = Parameter('struct', StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
'arg': readonly=False, default=dict(a='', c=True))
Command('5 args', array = Parameter('array', ArrayOf(BoolType()),
argument=TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())), readonly=False, default=[])
result=StringType()), tuple = Parameter('tuple', TupleOf(StringType(), FloatRange(), BoolType(),
'keyed': TupleOf(BoolType()), StructOf(a=StringType())),
Command('keyworded arg', argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']), result=StringType()), readonly=False, default=('', 0, False, (False,), dict(a='')))
'one':
Command('1 arg', argument=FloatRange(), result=StringType()),
'none':
Command('no arg', result=StringType()),
}
parameters = {
'struct': Parameter('struct', StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
readonly=False, default=dict(a='',c=True)),
'array': Parameter('array', ArrayOf(BoolType()),
readonly=False, default=[]),
'tuple': Parameter('tuple', TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())),
readonly=False, default=('',0,False,(False,),dict(a=''))),
}
def do_arg(self, arg): @Command(argument=TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())),
result=StringType())
def arg(self, *arg):
"""5 args"""
return repr(arg) return repr(arg)
def do_keyed(self, arg): @Command(argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
result=StringType())
def keyed(self, **arg):
"""keyworded arg"""
return repr(arg) return repr(arg)
def do_one(self, arg): @Command(argument=FloatRange(), result=StringType())
def one(self, arg):
"""1 arg"""
return repr(arg) return repr(arg)
def do_none(self): @Command(result=StringType())
def none(self):
"""no arg"""
return repr(None) return repr(None)

View File

@ -20,31 +20,23 @@
# ***************************************************************************** # *****************************************************************************
"""Temp""" """Temp"""
from secop.modules import Readable, Drivable, Parameter, Override
from secop.datatypes import FloatRange, IntRange, StringType from secop.datatypes import FloatRange, IntRange, StringType
from secop.modules import Drivable, Parameter, Readable
from secop.stringio import HasIodev from secop.stringio import HasIodev
Status = Drivable.Status Status = Drivable.Status
class TempLoop(HasIodev, Drivable):
'''temperature channel on Lakeshore 336'''
parameters = { class TempLoop(HasIodev, Drivable):
'value': """temperature channel on Lakeshore 336"""
Override(datatype=FloatRange(unit='K'), default=0, poll=True),
'status': value = Parameter(datatype=FloatRange(unit='K'), default=0, poll=True)
Override(poll=False), status = Parameter(poll=False)
'target': target = Parameter(datatype=FloatRange(1.0, 402.0, unit='K'), default=1.3, poll=True)
Override(datatype=FloatRange(1.0, 402.0, unit='K'), default=1.3, poll=True), tolerance = Parameter('the tolerance', FloatRange(-400, 400), default=1, readonly=False)
'tolerance': pollinterval = Parameter(visibility=3)
Parameter('the tolerance', FloatRange(-400,400), default=1, readonly=False), channel = Parameter('the Lakeshore channel', datatype=StringType(), export=False)
'pollinterval': loop = Parameter('the Lakeshore loop number', datatype=IntRange(1, 3), export=False)
Override(visibility=3),
'channel':
Parameter('the Lakeshore channel', datatype=StringType(), export=False),
'loop':
Parameter('the Lakeshore loop number', datatype=IntRange(1,3), export=False),
}
def earlyInit(self): def earlyInit(self):
super(TempLoop, self).earlyInit() super(TempLoop, self).earlyInit()
@ -67,24 +59,18 @@ class TempLoop(HasIodev, Drivable):
float('x') float('x')
return result return result
def do_stop(self): def stop(self):
self.target = self.value self.target = self.value
self.status = [Status.IDLE, 'stopped'] self.status = [Status.IDLE, 'stopped']
class TempChannel(HasIodev, Readable): class TempChannel(HasIodev, Readable):
'''temperature channel on Lakeshore 336''' """temperature channel on Lakeshore 336"""
parameters = { value = Parameter(datatype=FloatRange(unit='K'), default=0, poll=True)
'value': status = Parameter(poll=False, constant=[Status.IDLE, 'idle'])
Override(datatype=FloatRange(unit='K'), default=0, poll=True), pollinterval = Parameter(visibility=3)
'status': channel = Parameter('the Lakeshore channel', datatype=StringType(), export=False)
Override(poll=False, constant=[Status.IDLE, 'idle']),
'pollinterval':
Override(visibility=3),
'channel':
Parameter('the Lakeshore channel', datatype=StringType(), export=False),
}
def read_value(self): def read_value(self):
result = self.sendRecv('KRDG?%s' % self.channel) result = self.sendRecv('KRDG?%s' % self.channel)

View File

@ -20,17 +20,19 @@
# ***************************************************************************** # *****************************************************************************
"""frappy support for ultrasound""" """frappy support for ultrasound"""
import math
#import serial #import serial
import os import os
import math
import time import time
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, StringIO, \
Done, Attached, TupleOf, StringType, IntRange, EnumType, HasIodev, Module
from secop.properties import Property
from adq_mr import Adq
import iqplot
import numpy as np import numpy as np
import iqplot
from adq_mr import Adq
from secop.core import Attached, BoolType, Done, FloatRange, HasIodev, \
IntRange, Module, Parameter, Readable, StringIO, StringType
from secop.properties import Property
def fname_from_time(t, extension): def fname_from_time(t, extension):
tm = time.localtime(t) tm = time.localtime(t)
@ -43,32 +45,27 @@ def fname_from_time(t, extension):
class Roi(Readable): class Roi(Readable):
properties = { main = Attached()
'main': Attached(),
} value = Parameter('amplitude', FloatRange(), default=0)
parameters = { phase = Parameter('phase', FloatRange(unit='deg'), default=0)
'value': Override('amplitude', FloatRange(), default=0), i = Parameter('in phase', FloatRange(), default=0)
'phase': Parameter('phase', FloatRange(unit='deg'), default=0), q = Parameter('out of phase', FloatRange(), default=0)
'i': Parameter('in phase', FloatRange(), default=0), time = Parameter('start time', FloatRange(unit='nsec'), readonly=False)
'q': Parameter('out of phase', FloatRange(), default=0), size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
'time': Parameter('start time', FloatRange(unit='nsec'), enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
readonly=False), #status = Parameter(export=False)
'size': Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), pollinterval = Parameter(export=False)
readonly=False),
'enable': Parameter('calculate this roi', BoolType(), readonly=False, default=True),
#'status': Override(export=False),
'pollinterval': Override(export=False),
}
interval = (0,0) interval = (0,0)
def initModule(self): def initModule(self):
self._main.register_roi(self) self._main.register_roi(self)
self.calc_interval() self.calc_interval()
def calc_interval(self): def calc_interval(self):
self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size) self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size)
def write_time(self, value): def write_time(self, value):
self.time = value self.time = value
self.calc_interval() self.calc_interval()
@ -83,53 +80,49 @@ class Roi(Readable):
class Pars(Module): class Pars(Module):
description = 'relevant parameters from SEA' description = 'relevant parameters from SEA'
parameters = { timestamp = Parameter('unix timestamp', StringType(), default='0', readonly=False)
'timestamp': Parameter('unix timestamp', StringType(), default='0', readonly=False), temperature = Parameter('T', FloatRange(unit='K'), default=0, readonly=False)
'temperature': Parameter('T', FloatRange(unit='K'), default=0, readonly=False), mf = Parameter('field', FloatRange(unit='T'), default=0, readonly=False)
'mf': Parameter('field', FloatRange(unit='T'), default=0, readonly=False), sr = Parameter('rotaion angle', FloatRange(unit='deg'), default=0, readonly=False)
'sr': Parameter('rotaion angle', FloatRange(unit='deg'), default=0, readonly=False),
}
class FreqStringIO(StringIO): class FreqStringIO(StringIO):
end_of_line = '\r' end_of_line = '\r'
class Frequency(HasIodev, Readable): class Frequency(HasIodev, Readable):
properties = { pars = Attached()
'pars': Attached(), sr = Property('samples per record', datatype=IntRange(), default=16384)
'sr': Property('samples per record', datatype=IntRange(), default=16384), maxy = Property('plot y scale', datatype=FloatRange(), default=0.5)
'maxy': Property('plot y scale', datatype=FloatRange(), default=0.5),
} value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0)
parameters = { basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False)
'value': Override('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0), nr = Parameter('number of records', datatype=IntRange(1,10000), default=500)
'basefreq': Parameter('base frequency', FloatRange(unit='Hz'), readonly=False), freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False, poll=True)
'nr': Parameter('number of records', datatype=IntRange(1,10000), default=500), amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False, poll=True)
'freq': Parameter('target frequency', FloatRange(unit='Hz'), readonly=False, poll=True), control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
'amp': Parameter('amplitude', FloatRange(unit='dBm'), readonly=False, poll=True), time = Parameter('pulse start time', FloatRange(unit='nsec'),
'control': Parameter('control loop on?', BoolType(), readonly=False, default=True), readonly=False)
'time': Parameter('pulse start time', FloatRange(unit='nsec'), size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
readonly=False), readonly=False)
'size': Parameter('pulse length (starting from time)', FloatRange(unit='nsec'), pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
readonly=False), maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
'pulselen': Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1), default=10000)
'maxstep': Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False, minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
default=10000), readonly=False, default=4000)
'minstep': Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'), slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
readonly=False, default=4000), default=1e6)
'slope': Parameter('inphase/frequency slope', FloatRange(), readonly=False, plot = Parameter('create plot images', BoolType(), readonly=False, default=True)
default=1e6), save = Parameter('save data', BoolType(), readonly=False, default=True)
'plot': Parameter('create plot images', BoolType(), readonly=False, default=True), pollinterval = Parameter(datatype=FloatRange(0,120))
'save': Parameter('save data', BoolType(), readonly=False, default=True),
'pollinterval': Override(datatype=FloatRange(0,120)),
}
iodevClass = FreqStringIO iodevClass = FreqStringIO
lastfreq = None lastfreq = None
old = None old = None
starttime = None starttime = None
interval = (0,0) interval = (0,0)
def earlyInit(self): def earlyInit(self):
#assert self.iodev.startswith('serial:') #assert self.iodev.startswith('serial:')
#self._iodev = serial.Serial(self.iodev[7:]) #self._iodev = serial.Serial(self.iodev[7:])
@ -142,30 +135,30 @@ class Frequency(HasIodev, Readable):
def calc_interval(self): def calc_interval(self):
self.interval = (self.time, self.time + self.size) self.interval = (self.time, self.time + self.size)
def write_time(self, value): def write_time(self, value):
self.time = value self.time = value
self.calc_interval() self.calc_interval()
return Done return Done
def write_size(self, value): def write_size(self, value):
self.size = value self.size = value
self.calc_interval() self.calc_interval()
return Done return Done
def write_nr(self, value): def write_nr(self, value):
# self.pollinterval = value * 0.0001 # self.pollinterval = value * 0.0001
return value return value
def register_roi(self, roi): def register_roi(self, roi):
self.roilist.append(roi) self.roilist.append(roi)
def set_freq(self): def set_freq(self):
freq = self.freq + self.basefreq freq = self.freq + self.basefreq
reply = self.sendRecv('FREQ %.15g;FREQ?' % freq) self.sendRecv('FREQ %.15g;FREQ?' % freq)
#self._iodev.readline().decode('ascii') #self._iodev.readline().decode('ascii')
return freq return freq
def write_amp(self, amp): def write_amp(self, amp):
reply = self.sendRecv('AMPR %g;AMPR?' % amp) reply = self.sendRecv('AMPR %g;AMPR?' % amp)
return float(reply) return float(reply)
@ -173,11 +166,11 @@ class Frequency(HasIodev, Readable):
def read_amp(self): def read_amp(self):
reply = self.sendRecv('AMPR?') reply = self.sendRecv('AMPR?')
return float(reply) return float(reply)
def write_freq(self, value): def write_freq(self, value):
self.skipctrl = 2 # suppress control for the 2 next steps self.skipctrl = 2 # suppress control for the 2 next steps
return value return value
def read_freq(self): def read_freq(self):
"""used as main polling loop body""" """used as main polling loop body"""
if self.lastfreq is None: if self.lastfreq is None:
@ -197,7 +190,7 @@ class Frequency(HasIodev, Readable):
self.adq.start() # start next acq self.adq.start() # start next acq
times.append(('start',time.time())) times.append(('start',time.time()))
roilist = [r for r in self.roilist if r.enable] roilist = [r for r in self.roilist if r.enable]
gates = self.adq.gates_and_curves(data, freq, self.interval, gates = self.adq.gates_and_curves(data, freq, self.interval,
[r.interval for r in roilist]) [r.interval for r in roilist])
if self.save: if self.save:

View File

@ -24,10 +24,11 @@
# no fixtures needed # no fixtures needed
import pytest import pytest
from secop.basic_validators import FloatProperty, PositiveFloatProperty, \ from secop.basic_validators import BoolProperty, EnumProperty, FloatProperty, \
NonNegativeFloatProperty, IntProperty, PositiveIntProperty, \ FmtStrProperty, IntProperty, NoneOr, NonNegativeFloatProperty, \
NonNegativeIntProperty, BoolProperty, StringProperty, UnitProperty, \ NonNegativeIntProperty, OneOfProperty, PositiveFloatProperty, \
FmtStrProperty, OneOfProperty, NoneOr, EnumProperty, TupleProperty PositiveIntProperty, StringProperty, TupleProperty, UnitProperty
class unprintable: class unprintable:
def __str__(self): def __str__(self):

View File

@ -26,8 +26,9 @@
import pytest import pytest
from secop.datatypes import ArrayOf, BLOBType, BoolType, \ from secop.datatypes import ArrayOf, BLOBType, BoolType, \
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \ CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType IntRange, ProgrammingError, ScaledInteger, StatusType, \
StringType, StructOf, TextType, TupleOf, get_datatype
def copytest(dt): def copytest(dt):
@ -359,6 +360,7 @@ def test_BoolType():
# pylint: disable=unexpected-keyword-arg # pylint: disable=unexpected-keyword-arg
BoolType(unit='K') BoolType(unit='K')
def test_ArrayOf(): def test_ArrayOf():
# test constructor catching illegal arguments # test constructor catching illegal arguments
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -478,6 +480,14 @@ def test_Command():
'result':{'type': 'int', 'min':-3, 'max':3}} 'result':{'type': 'int', 'min':-3, 'max':3}}
def test_StatusType():
status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400)
dt = StatusType(status_codes)
assert dt.IDLE == status_codes.IDLE
assert dt.ERROR == status_codes.ERROR
assert dt._enum == status_codes
def test_get_datatype(): def test_get_datatype():
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(1) get_datatype(1)

View File

@ -23,10 +23,11 @@
import pytest import pytest
from secop.datatypes import FloatRange, IntRange, Property, StringType
from secop.errors import ProgrammingError
from secop.iohandler import CmdParser, IOHandler from secop.iohandler import CmdParser, IOHandler
from secop.modules import Module, Parameter from secop.modules import Module, Parameter
from secop.datatypes import FloatRange, StringType, IntRange, Property
from secop.errors import ProgrammingError
@pytest.mark.parametrize('fmt, text, values, text2', [ @pytest.mark.parametrize('fmt, text, values, text2', [
('%d,%d', '2,3', [2,3], None), ('%d,%d', '2,3', [2,3], None),
@ -107,15 +108,11 @@ def test_IOHandler():
class Module1(Module): class Module1(Module):
properties = { channel = Property('the channel', IntRange(), default=3)
'channel': Property('the channel', IntRange(), default=3), loop = Property('the loop', IntRange(), default=2)
'loop': Property('the loop', IntRange(), default=2), simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
} real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
parameters = { text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
'real': Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False),
'text': Parameter('a string value', StringType(), default='x', handler=group2, readonly=False),
}
def sendRecv(self, command): def sendRecv(self, command):
assert data.pop('command') == command assert data.pop('command') == command
@ -196,6 +193,4 @@ def test_IOHandler():
with pytest.raises(ProgrammingError): # can not use a handler for different modules with pytest.raises(ProgrammingError): # can not use a handler for different modules
# pylint: disable=unused-variable # pylint: disable=unused-variable
class Module2(Module): class Module2(Module):
parameters = { simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
}

View File

@ -22,13 +22,14 @@
# ***************************************************************************** # *****************************************************************************
"""test data types.""" """test data types."""
# no fixtures needed
#import pytest
import threading import threading
import pytest
from secop.datatypes import BoolType, FloatRange, StringType from secop.datatypes import BoolType, FloatRange, StringType
from secop.errors import ProgrammingError
from secop.modules import Communicator, Drivable, Module from secop.modules import Communicator, Drivable, Module
from secop.params import Command, Override, Parameter from secop.params import Command, Parameter
from secop.poller import BasicPoller from secop.poller import BasicPoller
@ -64,30 +65,27 @@ def test_Communicator():
assert event.is_set() # event should be set immediately assert event.is_set() # event should be set immediately
def test_ModuleMeta(): def test_ModuleMagic():
class Newclass1(Drivable): class Newclass1(Drivable):
parameters = { param1 = Parameter('param1', datatype=BoolType(), default=False)
'pollinterval': Override(reorder=True), param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
'param1' : Parameter('param1', datatype=BoolType(), default=False),
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True), @Command(argument=BoolType(), result=BoolType())
"cmd": Command('stuff', argument=BoolType(), result=BoolType()) def cmd(self, arg):
} """stuff"""
commands = { return not arg
# intermixing parameters with commands is not recommended,
# but acceptable for influencing the order a1 = Parameter('a1', datatype=BoolType(), default=False)
'a1': Parameter('a1', datatype=BoolType(), default=False), a2 = Parameter('a2', datatype=BoolType(), default=True)
'a2': Parameter('a2', datatype=BoolType(), default=True), value = Parameter(datatype=StringType(), default='first')
'value': Override(datatype=StringType(), default='first'),
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()), @Command(argument=BoolType(), result=BoolType())
} def cmd2(self, arg):
"""another stuff"""
return not arg
pollerClass = BasicPoller pollerClass = BasicPoller
def do_cmd(self, arg):
return not arg
def do_cmd2(self, arg):
return not arg
def read_param1(self): def read_param1(self):
return True return True
@ -103,19 +101,31 @@ def test_ModuleMeta():
def read_value(self): def read_value(self):
return 'second' return 'second'
with pytest.raises(ProgrammingError):
class Mod1(Module): # pylint: disable=unused-variable
def do_this(self): # old style command
pass
# first inherited accessibles, then Overrides with reorder=True and new accessibles with pytest.raises(ProgrammingError):
sortcheck1 = ['value', 'status', 'target', 'pollinterval', class Mod2(Module): # pylint: disable=unused-variable
param = Parameter(), # pylint: disable=trailing-comma-tuple
# first inherited accessibles
sortcheck1 = ['value', 'status', 'pollinterval', 'target', 'stop',
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2'] 'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
class Newclass2(Newclass1): class Newclass2(Newclass1):
parameters = { paramOrder = 'param1', 'param2', 'cmd', 'value'
'cmd2': Override('another stuff'),
'value': Override(datatype=FloatRange(unit='deg'), reorder=True), @Command(description='another stuff')
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False), def cmd2(self, arg):
'b2': Parameter('<b2>', datatype=BoolType(), default=True, return arg
poll=True, readonly=False, initwrite=True),
} value = Parameter(datatype=FloatRange(unit='deg'))
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
def write_a1(self, value): def write_a1(self, value):
self._a1_written = value self._a1_written = value
@ -128,14 +138,15 @@ def test_ModuleMeta():
def read_value(self): def read_value(self):
return 0 return 0
sortcheck2 = ['value', 'status', 'target', 'pollinterval', # first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'a1', 'b2'] sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
logger = LoggerStub() logger = LoggerStub()
updates = {} updates = {}
srv = ServerStub(updates) srv = ServerStub(updates)
params_found = set() # set of instance accessibles params_found = set() # set of instance accessibles
objects = [] objects = []
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]: for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
@ -143,16 +154,11 @@ def test_ModuleMeta():
o2 = newclass('o2', logger, {'.description':''}, srv) o2 = newclass('o2', logger, {'.description':''}, srv)
for obj in [o1, o2]: for obj in [o1, o2]:
objects.append(obj) objects.append(obj)
ctr_found = set() for o in obj.accessibles.values():
for n, o in obj.accessibles.items():
# check that instance accessibles are unique objects # check that instance accessibles are unique objects
assert o not in params_found assert o not in params_found
params_found.add(o) params_found.add(o)
assert o.ctr not in ctr_found assert list(obj.accessibles) == sortcheck
ctr_found.add(o.ctr)
check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck]
# HACK: atm. disabled to fix all other problems first.
assert check_order + sorted(check_order)
# check for inital updates working properly # check for inital updates working properly
o1 = Newclass1('o1', logger, {'.description':''}, srv) o1 = Newclass1('o1', logger, {'.description':''}, srv)
@ -214,7 +220,7 @@ def test_ModuleMeta():
assert acs is not None assert acs is not None
else: # do not check object or mixin else: # do not check object or mixin
acs = {} acs = {}
for n, o in acs.items(): for o in acs.values():
# check that class accessibles are not reused as instance accessibles # check that class accessibles are not reused as instance accessibles
assert o not in params_found assert o not in params_found

View File

@ -23,8 +23,8 @@
import pytest import pytest
from secop.protocol.interface import encode_msg_frame, decode_msg
import secop.protocol.messages as m import secop.protocol.messages as m
from secop.protocol.interface import decode_msg, encode_msg_frame
# args are: msg tuple, msg bytes # args are: msg tuple, msg bytes
MSG = [ MSG = [

View File

@ -25,66 +25,78 @@
# no fixtures needed # no fixtures needed
import pytest import pytest
from secop.datatypes import BoolType, IntRange from secop.datatypes import BoolType, FloatRange, IntRange
from secop.params import Command, Override, Parameter, Parameters
from secop.errors import ProgrammingError from secop.errors import ProgrammingError
from secop.modules import HasAccessibles
from secop.params import Command, Parameter
def test_Command(): def test_Command():
cmd = Command('do_something') class Mod(HasAccessibles):
assert cmd.description == 'do_something' @Command()
assert cmd.ctr def cmd(self):
assert cmd.argument is None """do something"""
assert cmd.result is None @Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
assert cmd.for_export() == {'datainfo': {'type': 'command'}, def cmd2(self):
'description': 'do_something'} pass
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1)) assert Mod.cmd.description == 'do something'
assert cmd.description assert Mod.cmd.argument is None
assert isinstance(cmd.argument, IntRange) assert Mod.cmd.result is None
assert isinstance(cmd.result, IntRange) assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9}, 'description': 'do something'}
'result': {'type': 'int', 'min':-1, 'max':1}},
'description': 'do_something'} assert Mod.cmd2.description == 'do some other thing'
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9}, assert isinstance(Mod.cmd2.argument, IntRange)
'result': {'type': 'int', 'max': 1, 'min': -1}}, assert isinstance(Mod.cmd2.result, IntRange)
'description': 'do_something'} assert Mod.cmd2.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min': -9, 'max': 9},
'result': {'type': 'int', 'min': -1, 'max': 1}},
'description': 'do some other thing'}
assert Mod.cmd2.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
'result': {'type': 'int', 'max': 1, 'min': -1}},
'description': 'do some other thing'}
def test_Parameter(): def test_Parameter():
p1 = Parameter('description1', datatype=IntRange(), default=0) class Mod(HasAccessibles):
p2 = Parameter('description2', datatype=IntRange(), constant=1) p1 = Parameter('desc1', datatype=FloatRange(), default=0)
assert p1 != p2 p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
assert p1.ctr != p2.ctr p3 = Parameter('desc3', datatype=FloatRange(), default=0, readonly=False)
p4 = Parameter('desc4', datatype=FloatRange(), constant=1)
assert repr(Mod.p1) != repr(Mod.p3)
assert id(Mod.p1.datatype) != id(Mod.p2.datatype)
assert Mod.p1.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc1', 'readonly': True}
assert Mod.p2.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc2', 'readonly': True}
assert Mod.p3.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc3', 'readonly': False}
assert Mod.p4.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc4', 'readonly': True,
'constant': 1.0}
p3 = Mod.p1.copy()
assert id(p3) != id(Mod.p1)
assert repr(Mod.p1) == repr(p3)
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
Parameter(None, datatype=float) Parameter(None, datatype=float, inherit=False)
p3 = p1.copy()
assert p1.ctr != p3.ctr
p3.ctr = p1.ctr # manipulate ctr for next line
assert repr(p1) == repr(p3)
assert p1.datatype != p2.datatype
def test_Override(): def test_Override():
p = Parameter('description1', datatype=BoolType, default=False) class Base(HasAccessibles):
o = Override(default=True, reorder=True) p1 = Parameter('description1', datatype=BoolType, default=False)
assert o.ctr != p.ctr p2 = Parameter('description1', datatype=BoolType, default=False)
q = o.apply(p) p3 = Parameter('description1', datatype=BoolType, default=False)
assert q.ctr != o.ctr # override shall be useable to influence the order, hence copy the ctr value
assert q.ctr != p.ctr
assert o.ctr != p.ctr
assert q != p
p2 = Parameter('description2', datatype=BoolType, default=False) class Mod(Base):
o2 = Override(default=True) p1 = Parameter(default=True)
assert o2.ctr != p2.ctr p2 = Parameter() # override without change
q2 = o2.apply(p2)
assert q2.ctr != o2.ctr
assert q2.ctr != p2.ctr # EVERY override makes a new parameter object -> ctr++
assert o2.ctr != p2.ctr
assert q2 != p2
def test_Parameters(): assert Mod.p1 != Base.p1
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True))) assert Mod.p2 != Base.p2
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True) assert Mod.p3 == Base.p3
assert ps['_p2'].export == '_p2'
assert id(Mod.p2) != id(Base.p2) # must be a new object
assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
def test_Export():
class Mod:
param = Parameter('description1', datatype=BoolType, default=False)
assert Mod.param.export == '_param'

View File

@ -22,8 +22,8 @@
"""test data types.""" """test data types."""
from collections import OrderedDict
from ast import literal_eval from ast import literal_eval
from collections import OrderedDict
import pytest import pytest

View File

@ -23,9 +23,12 @@
import time import time
from collections import OrderedDict from collections import OrderedDict
import pytest import pytest
from secop.modules import Drivable from secop.modules import Drivable
from secop.poller import Poller, REGULAR, DYNAMIC, SLOW from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
Status = Drivable.Status Status = Drivable.Status
class Time: class Time:

View File

@ -23,39 +23,59 @@
import pytest import pytest
from secop.datatypes import IntRange, StringType, FloatRange, ValueType from secop.datatypes import FloatRange, IntRange, StringType, ValueType
from secop.errors import ProgrammingError, ConfigError from secop.errors import BadValueError, ConfigError, ProgrammingError
from secop.properties import Property, Properties, HasProperties from secop.properties import HasProperties, Property
# args are: datatype, default, extname, export, mandatory, settable
def Prop(*args, name=None, **kwds):
# collect the args for Property
return name, args, kwds
# Property(description, datatype, default, ...)
V_test_Property = [ V_test_Property = [
[(StringType(), 'default', 'extname', False, False), [Prop(StringType(), 'default', extname='extname', mandatory=False),
dict(default='default', extname='extname', export=True, mandatory=False)], dict(default='default', extname='extname', export=True, mandatory=False)
[(IntRange(), '42', '_extname', False, True), ],
dict(default=42, extname='_extname', export=True, mandatory=True)], [Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
[(IntRange(), '42', '_extname', True, False), dict(default=42, extname='_custom', export=True, mandatory=True),
dict(default=42, extname='_extname', export=True, mandatory=False)], ],
[(IntRange(), 42, '_extname', True, True), [Prop(IntRange(), '42', export=True, name='name'),
dict(default=42, extname='_extname', export=True, mandatory=True)], dict(default=42, extname='_name', export=True, mandatory=False)
[(IntRange(), 0, '', True, True), ],
dict(default=0, extname='', export=True, mandatory=True)], [Prop(IntRange(), 42, '_extname', mandatory=True),
[(IntRange(), 0, '', True, False), dict(default=42, extname='_extname', export=True, mandatory=True)
dict(default=0, extname='', export=True, mandatory=False)], ],
[(IntRange(), 0, '', False, True), [Prop(IntRange(), 0, export=True, mandatory=True),
dict(default=0, extname='', export=False, mandatory=True)], dict(default=0, extname='', export=True, mandatory=True)
[(IntRange(), 0, '', False, False), ],
dict(default=0, extname='', export=False, mandatory=False)], [Prop(IntRange(), 0, export=True, mandatory=False),
[(IntRange(), None, '', None), dict(default=0, extname='', export=True, mandatory=False)
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory ],
[(ValueType(), 1, '', False), [Prop(IntRange(), 0, export=False, mandatory=True),
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory dict(default=0, extname='', export=False, mandatory=True)
],
[Prop(IntRange(), 0, export=False, mandatory=False),
dict(default=0, extname='', export=False, mandatory=False)
],
[Prop(IntRange()),
dict(default=0, extname='', export=False, mandatory=True) # mandatory not given, no default -> mandatory
],
[Prop(ValueType(), 1),
dict(default=1, extname='', export=False, mandatory=False) # mandatory not given, default given -> NOT mandatory
],
] ]
@pytest.mark.parametrize('args, check', V_test_Property) @pytest.mark.parametrize('propargs, check', V_test_Property)
def test_Property(args, check): def test_Property(propargs, check):
p = Property('', *args) name, args, kwds = propargs
p = Property('', *args, **kwds)
if name:
p.__set_name__(None, name)
result = {k: getattr(p, k) for k in check} result = {k: getattr(p, k) for k in check}
assert result == check assert result == check
def test_Property_basic(): def test_Property_basic():
with pytest.raises(TypeError): with pytest.raises(TypeError):
# pylint: disable=no-value-for-parameter # pylint: disable=no-value-for-parameter
@ -67,47 +87,47 @@ def test_Property_basic():
Property('', 1) Property('', 1)
Property('', IntRange(), '42', 'extname', False, False) Property('', IntRange(), '42', 'extname', False, False)
def test_Properties(): def test_Properties():
p = Properties() class Cls(HasProperties):
with pytest.raises(ProgrammingError): aa = Property('', IntRange(0, 99), '42', export=True)
p[1] = 2 bb = Property('', IntRange(), 0, export=False)
p['a'] = Property('', IntRange(), '42', export=True)
assert p['a'].default == 42 assert Cls.aa.default == 42
assert p['a'].export is True assert Cls.aa.export is True
assert p['a'].extname == '_a' assert Cls.aa.extname == '_aa'
with pytest.raises(ProgrammingError):
p['a'] = 137 cc = Cls()
with pytest.raises(ProgrammingError): with pytest.raises(BadValueError):
del p[1] cc.aa = 137
with pytest.raises(ProgrammingError):
del p['a'] assert Cls.bb.default == 0
p['a'] = Property('', IntRange(), 0, export=False) assert Cls.bb.export is False
assert p['a'].default == 0 assert Cls.bb.extname == ''
assert p['a'].export is False
assert p['a'].extname == ''
class c(HasProperties): class c(HasProperties):
properties = { # properties
'a' : Property('', IntRange(), 1), a = Property('', IntRange(), 1)
}
class cl(c): class cl(c):
properties = { # properties
'a' : Property('', IntRange(), 3), a = Property('', IntRange(), 3)
'b' : Property('', FloatRange(), 3.14), b = Property('', FloatRange(), 3.14)
'minabc': Property('', IntRange(), 8), minabc = Property('', IntRange(), 8)
'maxabc': Property('', IntRange(), 9), maxabc = Property('', IntRange(), 9)
'minx': Property('', IntRange(), 2), minx = Property('', IntRange(), 2)
'maxy': Property('', IntRange(), 1), maxy = Property('', IntRange(), 1)
}
def test_HasProperties(): def test_HasProperties():
o = c() o = c()
assert o.properties['a'] == 1 assert o.a == 1
o = cl() o = cl()
assert o.properties['a'] == 3 assert o.a == 3
assert o.properties['b'] == 3.14 assert o.b == 3.14
def test_Property_checks(): def test_Property_checks():
o = c() o = c()
@ -119,6 +139,7 @@ def test_Property_checks():
with pytest.raises(ConfigError): with pytest.raises(ConfigError):
o.checkProperties() o.checkProperties()
def test_Property_override(): def test_Property_override():
o1 = c() o1 = c()
class co(c): class co(c):
@ -131,10 +152,10 @@ def test_Property_override():
class cx(c): # pylint: disable=unused-variable class cx(c): # pylint: disable=unused-variable
def a(self): def a(self):
pass pass
assert 'collides with method' in str(e.value) assert 'collides with' in str(e.value)
with pytest.raises(ProgrammingError) as e: with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable class cz(c): # pylint: disable=unused-variable
a = 's' a = 's'
assert 'can not be set to' in str(e.value) assert 'can not set' in str(e.value)