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:
parent
bc5edec06f
commit
41baf5805f
@ -1,51 +1,20 @@
|
||||
/* this is for the sphinx_rtd_theme */
|
||||
div.wy-nav-content
|
||||
{
|
||||
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)*/
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -100,19 +100,8 @@ default_role = 'any'
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
if False: # alabaster
|
||||
html_theme = 'alabaster'
|
||||
# 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'
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# 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.
|
||||
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)
|
@ -5,10 +5,10 @@ Module Base Classes
|
||||
...................
|
||||
|
||||
.. autoclass:: secop.modules.Module
|
||||
:members: earlyInit, initModule, startModule
|
||||
:members: earlyInit, initModule, startModule, pollerClass
|
||||
|
||||
.. autoclass:: secop.modules.Readable
|
||||
:members: pollerClass, Status
|
||||
:members: Status
|
||||
|
||||
.. autoclass:: secop.modules.Writable
|
||||
|
||||
@ -21,13 +21,11 @@ Parameters, Commands and Properties
|
||||
|
||||
.. autoclass:: secop.params.Parameter
|
||||
.. autoclass:: secop.params.Command
|
||||
.. autoclass:: secop.params.Override
|
||||
.. autoclass:: secop.properties.Property
|
||||
.. autoclass:: secop.modules.Attached
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
Datatypes
|
||||
.........
|
||||
|
||||
|
@ -1,6 +1,14 @@
|
||||
PSI (SINQ)
|
||||
----------
|
||||
|
||||
CCU4 tutorial example
|
||||
.....................
|
||||
|
||||
.. automodule:: secop_psi.ccu4
|
||||
:show-inheritance:
|
||||
:members:
|
||||
|
||||
|
||||
PPMS
|
||||
....
|
||||
|
||||
|
@ -1,7 +0,0 @@
|
||||
Tutorial
|
||||
--------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
tutorial_helevel
|
@ -3,12 +3,13 @@ HeLevel - a Simple Driver
|
||||
|
||||
Coding the Driver
|
||||
-----------------
|
||||
For this tutorial we choose as an example a cryostat. Let us start with the helium level 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
|
||||
framework will deal with the SECoP interface. The code for the driver is located in a subdirectory
|
||||
named after the facility or institute programming the driver in our case *secop_psi*.
|
||||
We create a file named from the electronic device CCU4 we use here for the He level reading.
|
||||
For this tutorial we choose as an example a cryostat. Let us start with the helium level
|
||||
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 framework will deal with the SECoP interface. The code for the driver is
|
||||
located in a subdirectory named after the facility or institute programming the driver
|
||||
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:
|
||||
|
||||
@ -20,9 +21,8 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
|
||||
.. code:: python
|
||||
|
||||
# the most common classes can be imported from secop.core
|
||||
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, \
|
||||
StringIO, HasIodev
|
||||
# the most common Frappy classes can be imported from secop.core
|
||||
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
|
||||
|
||||
|
||||
class CCU4IO(StringIO):
|
||||
@ -33,30 +33,48 @@ CCU4 luckily has a very simple and logical protocol:
|
||||
identification = [('cid', r'CCU4.*')]
|
||||
|
||||
|
||||
# inheriting the HasIodev mixin creates us the things needed for talking
|
||||
# with a device by means of the sendRecv method
|
||||
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
||||
# for talking with the hardware
|
||||
# Readable as a base class defines the value and status parameters
|
||||
class HeLevel(HasIodev, Readable):
|
||||
"""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
|
||||
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):
|
||||
# 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('=')
|
||||
assert name == 'h' # check that we got a reply to our command
|
||||
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,
|
||||
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.
|
||||
|
||||
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*,
|
||||
and we have to code the communication and convert the status codes from the hardware to
|
||||
the standard SECoP status codes.
|
||||
as it is inherited from *Readable*. But we declare the new parameters *empty_length*,
|
||||
*full_length* and *sample_rate*, and we have to code the communication and convert
|
||||
the status codes from the hardware to the standard SECoP status codes.
|
||||
|
||||
.. code:: python
|
||||
|
||||
...
|
||||
# define or alter the parameters
|
||||
parameters = {
|
||||
|
||||
...
|
||||
|
||||
# the first two arguments to Parameter are 'description' and 'datatype'
|
||||
# it is highly recommended to define always the physical unit
|
||||
'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),
|
||||
}
|
||||
...
|
||||
# the first two arguments to Parameter are 'description' and 'datatype'
|
||||
# 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'),
|
||||
readonly=False)
|
||||
sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False)
|
||||
|
||||
...
|
||||
|
||||
Status = Readable.Status
|
||||
|
||||
# conversion of the code from the CCU4 parameter 'hsf'
|
||||
STATUS_MAP = {
|
||||
0: (Status.IDLE, 'sensor ok'),
|
||||
1: (Status.ERROR, 'sensor warm'),
|
||||
@ -102,69 +114,98 @@ the standard SECoP status codes.
|
||||
}
|
||||
|
||||
def read_status(self):
|
||||
name, txtvalue = self.sendRecv('hsf').split('=')
|
||||
name, txtvalue = self._iodev.communicate('hsf').split('=')
|
||||
assert name == 'hsf'
|
||||
return self.STATUS_MAP(int(txtvalue))
|
||||
|
||||
def read_emtpy(self):
|
||||
name, txtvalue = self.sendRecv('hem').split('=')
|
||||
def read_empty_length(self):
|
||||
name, txtvalue = self._iodev.communicate('hem').split('=')
|
||||
assert name == 'hem'
|
||||
return txtvalue
|
||||
|
||||
def write_empty(self, value):
|
||||
name, txtvalue = self.sendRecv('hem=%g' % value).split('=')
|
||||
def write_empty_length(self, value):
|
||||
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
|
||||
assert name == 'hem'
|
||||
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
|
||||
will become shorter:
|
||||
|
||||
|
||||
Here we start to realize, that we will repeat similar code for other parameters,
|
||||
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
|
||||
|
||||
...
|
||||
|
||||
def _sendRecv(self, cmd):
|
||||
# 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')))
|
||||
class HeLevel(Readable):
|
||||
|
||||
def read_empty(self):
|
||||
return self._sendRecv('hem')
|
||||
|
||||
def write_empty(self, value):
|
||||
return self._sendRecv('hem=%g' % value)
|
||||
|
||||
def read_full(self):
|
||||
return self._sendRecv('hfu')
|
||||
|
||||
def write_full(self, value):
|
||||
return self._sendRecv('hfu=%g' % value)
|
||||
|
||||
def read_fast(self):
|
||||
return self._sendRecv('hf')
|
||||
|
||||
def write_fast(self, value):
|
||||
return self._sendRecv('hf=%s' % value)
|
||||
...
|
||||
|
||||
|
||||
def query(self, cmd):
|
||||
"""send a query and get the response
|
||||
|
||||
:param cmd: the name of the parameter to query or '<parameter>=<value'
|
||||
for changing a parameter
|
||||
:returns: the (new) value of the parameter
|
||||
"""
|
||||
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||
|
||||
def read_value(self):
|
||||
return self.query('h')
|
||||
|
||||
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
|
||||
-------------
|
||||
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
|
||||
file determines, which code will finally be loaded. We choose the name *example_cryo*
|
||||
and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdirectory:
|
||||
The directory tree of the Frappy framework contains the code for all drivers, but the
|
||||
configuration file determines, which code will be loaded when a server is started.
|
||||
We choose the name *example_cryo* and create therefore a configuration file
|
||||
*example_cryo.cfg* in the *cfg* subdirectory:
|
||||
|
||||
``cfg/example_cryo.cfg``:
|
||||
|
||||
@ -172,7 +213,7 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
|
||||
|
||||
[NODE]
|
||||
description = this is an example cryostat for the Frappy tutorial
|
||||
id = example_cryo.sampleenvironment.org
|
||||
id = example_cryo.psi.ch
|
||||
|
||||
[INTERFACE]
|
||||
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
|
||||
class = secop_psi.ccu4.HeLevel
|
||||
uri = linse-moxa-4.psi.ch:3001
|
||||
empty = 380
|
||||
full = 0
|
||||
empty_length = 380
|
||||
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
|
||||
an id, which should be globally unique.
|
||||
The *NODE* section describes the main properties of the SEC Node: a description of the node
|
||||
and an id, which should be globally unique.
|
||||
|
||||
The *INTERFACE* section defines the address of the server, usually the only important value here
|
||||
is the TCP port under which the server will be accessible. Currently only tcp is supported.
|
||||
The *INTERFACE* section defines the address of the server, usually the only important value
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
empty.export = False
|
||||
full.export = False
|
||||
empty_length.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*
|
@ -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
|
@ -22,21 +22,22 @@
|
||||
# *****************************************************************************
|
||||
"""general SECoP client"""
|
||||
|
||||
import time
|
||||
import queue
|
||||
import json
|
||||
from threading import Event, RLock, current_thread
|
||||
import queue
|
||||
import time
|
||||
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.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
|
||||
UPDATE_MESSAGES = {EVENTREPLY, READREPLY, WRITEREPLY, ERRORPREFIX + READREQUEST, ERRORPREFIX + EVENTREPLY}
|
||||
@ -160,7 +161,6 @@ class ProxyClient:
|
||||
if not cblist:
|
||||
self.callbacks[cbname].pop(key)
|
||||
|
||||
|
||||
def callback(self, key, cbname, *args):
|
||||
"""perform callbacks
|
||||
|
||||
|
@ -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])
|
@ -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)
|
@ -26,14 +26,14 @@
|
||||
# allow to import the most important classes from 'secop'
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
|
||||
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
|
||||
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.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||
FloatRange, IntRange, ScaledInteger, StringType, StructOf, TupleOf
|
||||
from secop.iohandler import IOHandler, IOHandlerBase
|
||||
from secop.stringio import StringIO, HasIodev
|
||||
from secop.proxy import SecNode, Proxy, proxy_class
|
||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
||||
from secop.lib.enum import Enum
|
||||
from secop.modules import Attached, Communicator, \
|
||||
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
|
||||
|
@ -28,13 +28,13 @@
|
||||
import sys
|
||||
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.enum import Enum
|
||||
from secop.parse import Parser
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
|
||||
# Only export these classes for 'from secop.datatypes import *'
|
||||
__all__ = [
|
||||
'DataType', 'get_datatype',
|
||||
@ -53,6 +53,7 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
|
||||
Parser = Parser()
|
||||
|
||||
|
||||
# base class for all DataTypes
|
||||
class DataType(HasProperties):
|
||||
"""base class for all data types"""
|
||||
IS_COMMAND = False
|
||||
@ -97,7 +98,7 @@ class DataType(HasProperties):
|
||||
def set_properties(self, **kwds):
|
||||
"""init datatype properties"""
|
||||
try:
|
||||
for k,v in kwds.items():
|
||||
for k, v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
self.checkProperties()
|
||||
except Exception as e:
|
||||
@ -126,10 +127,6 @@ class DataType(HasProperties):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def short_doc(self):
|
||||
"""short description for automatic extension of doc strings"""
|
||||
return None
|
||||
|
||||
|
||||
class Stub(DataType):
|
||||
"""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():
|
||||
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
||||
for prop in dtcls.properties.values():
|
||||
for prop in dtcls.propertyDict.values():
|
||||
stub = prop.datatype
|
||||
if isinstance(stub, cls):
|
||||
prop.datatype = globals()[stub.name](*stub.args)
|
||||
|
||||
def short_doc(self):
|
||||
return self.name.replace('Type', '').replace('Range', '').lower()
|
||||
|
||||
|
||||
# SECoP types:
|
||||
|
||||
|
||||
class FloatRange(DataType):
|
||||
"""(restricted) float type
|
||||
|
||||
:param minval: (property **min**)
|
||||
: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 = {
|
||||
'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):
|
||||
def __init__(self, minval=None, maxval=None, **kwds):
|
||||
super().__init__()
|
||||
properties['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
|
||||
self.set_properties(**properties)
|
||||
kwds['min'] = minval if minval is not None else -sys.float_info.max
|
||||
kwds['max'] = maxval if maxval is not None else sys.float_info.max
|
||||
self.set_properties(**kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
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:
|
||||
return min(max(value, self.min), self.max)
|
||||
raise BadValueError('%.14g should be a float between %.14g and %.14g' %
|
||||
(value, self.min, self.max))
|
||||
(value, self.min, self.max))
|
||||
|
||||
def __repr__(self):
|
||||
hints = self.get_info()
|
||||
@ -221,7 +211,7 @@ class FloatRange(DataType):
|
||||
hints['minval'] = hints.pop('min')
|
||||
if 'max' in hints:
|
||||
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):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -249,9 +239,6 @@ class FloatRange(DataType):
|
||||
other(max(sys.float_info.min, self.min))
|
||||
other(min(sys.float_info.max, self.max))
|
||||
|
||||
def short_doc(self):
|
||||
return 'float'
|
||||
|
||||
|
||||
class IntRange(DataType):
|
||||
"""restricted int type
|
||||
@ -259,12 +246,10 @@ class IntRange(DataType):
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', 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?
|
||||
# 'unit': Property('physical unit', StringType(), extname='unit', default=''),
|
||||
}
|
||||
min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', 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?
|
||||
# unit = Property('physical unit', StringType(), extname='unit', default='')
|
||||
|
||||
def __init__(self, minval=None, maxval=None):
|
||||
super().__init__()
|
||||
@ -290,7 +275,12 @@ class IntRange(DataType):
|
||||
raise BadValueError('Can not convert %r to int' % value)
|
||||
|
||||
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):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -316,48 +306,38 @@ class IntRange(DataType):
|
||||
for i in range(self.min, self.max + 1):
|
||||
other(i)
|
||||
|
||||
def short_doc(self):
|
||||
return 'int'
|
||||
|
||||
|
||||
class ScaledInteger(DataType):
|
||||
"""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 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
|
||||
- the scale is only used for calculating to/from transport serialisation
|
||||
note: limits are for the scaled float value
|
||||
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),
|
||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
||||
'max': Property('high limit', FloatRange(), extname='max', mandatory=True),
|
||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
||||
'absolute_resolution': Property('absolute resolution', FloatRange(0),
|
||||
extname='absolute_resolution', default=0.0),
|
||||
'relative_resolution': Property('relative resolution', FloatRange(0),
|
||||
extname='relative_resolution', default=1.2e-7),
|
||||
}
|
||||
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
|
||||
min = Property('low limit', FloatRange(), extname='min', mandatory=True)
|
||||
max = Property('high limit', FloatRange(), extname='max', mandatory=True)
|
||||
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||
absolute_resolution = Property('absolute resolution', FloatRange(0),
|
||||
extname='absolute_resolution', default=0.0)
|
||||
relative_resolution = Property('relative resolution', FloatRange(0),
|
||||
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__()
|
||||
scale = float(scale)
|
||||
if absolute_resolution is None:
|
||||
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),
|
||||
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
||||
absolute_resolution=absolute_resolution,
|
||||
**properties)
|
||||
**kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||
@ -384,8 +364,8 @@ class ScaledInteger(DataType):
|
||||
|
||||
def export_datatype(self):
|
||||
return self.get_info(type='scaled',
|
||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
||||
max = int((self.max + 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))
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
@ -398,15 +378,15 @@ class ScaledInteger(DataType):
|
||||
value = min(max(value, self.min), self.max)
|
||||
else:
|
||||
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)
|
||||
value = float(intval * self.scale)
|
||||
return value # return 'actual' value (which is more discrete than a float)
|
||||
|
||||
def __repr__(self):
|
||||
hints = self.get_info(scale=float('%g' % self.scale),
|
||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
||||
max = int((self.max + 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))
|
||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||
|
||||
def export_value(self, value):
|
||||
@ -435,25 +415,19 @@ class ScaledInteger(DataType):
|
||||
other(self.min)
|
||||
other(self.max)
|
||||
|
||||
def short_doc(self):
|
||||
return 'float'
|
||||
|
||||
|
||||
class EnumType(DataType):
|
||||
"""enumeration
|
||||
|
||||
: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>
|
||||
|
||||
exception: use members=<member dict> to add members from a dict
|
||||
:param members: members dict or None when using kwds only
|
||||
:param kwds: (additional) members
|
||||
"""
|
||||
def __init__(self, enum_or_name='', **members):
|
||||
def __init__(self, enum_or_name='', *, members=None, **kwds):
|
||||
super().__init__()
|
||||
if 'members' in members:
|
||||
members = dict(members)
|
||||
members.update(members['members'])
|
||||
members.pop('members')
|
||||
self._enum = Enum(enum_or_name, **members)
|
||||
if members is not None:
|
||||
kwds.update(members)
|
||||
self._enum = Enum(enum_or_name, **kwds)
|
||||
self.default = self._enum[self._enum.members[0]]
|
||||
|
||||
def copy(self):
|
||||
@ -461,10 +435,11 @@ class EnumType(DataType):
|
||||
return EnumType(self._enum)
|
||||
|
||||
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):
|
||||
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):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -478,7 +453,7 @@ class EnumType(DataType):
|
||||
"""return the validated (internal) value or raise"""
|
||||
try:
|
||||
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))
|
||||
|
||||
def from_string(self, text):
|
||||
@ -487,25 +462,24 @@ class EnumType(DataType):
|
||||
def format_value(self, value, unit=None):
|
||||
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
||||
|
||||
def set_name(self, name):
|
||||
self._enum.name = name
|
||||
|
||||
def compatible(self, other):
|
||||
for m in self._enum.members:
|
||||
other(m)
|
||||
|
||||
def short_doc(self):
|
||||
return 'one of %s' % str(tuple(self._enum.keys()))
|
||||
|
||||
|
||||
class BLOBType(DataType):
|
||||
"""binary large object
|
||||
|
||||
internally treated as bytes
|
||||
"""
|
||||
properties = {
|
||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0),
|
||||
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||
mandatory=True),
|
||||
}
|
||||
|
||||
minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0)
|
||||
maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||
mandatory=True)
|
||||
|
||||
def __init__(self, minbytes=0, maxbytes=None):
|
||||
super().__init__()
|
||||
@ -565,21 +539,20 @@ class BLOBType(DataType):
|
||||
class StringType(DataType):
|
||||
"""string
|
||||
|
||||
for parameters see properties below
|
||||
"""
|
||||
properties = {
|
||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0),
|
||||
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='maxchars', default=UNLIMITED),
|
||||
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||
Stub('BoolType'), extname='isUTF8', default=False),
|
||||
}
|
||||
minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0)
|
||||
maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='maxchars', default=UNLIMITED)
|
||||
isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||
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__()
|
||||
if maxchars is None:
|
||||
maxchars = minchars or UNLIMITED
|
||||
self.set_properties(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8)
|
||||
self.set_properties(minchars=minchars, maxchars=maxchars, **kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = ' ' * self.minchars
|
||||
@ -635,24 +608,13 @@ class StringType(DataType):
|
||||
except AttributeError:
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
return 'str'
|
||||
|
||||
|
||||
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
||||
# 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
|
||||
# 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):
|
||||
"""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):
|
||||
if maxchars is None:
|
||||
maxchars = UNLIMITED
|
||||
@ -661,7 +623,7 @@ class TextType(StringType):
|
||||
def __repr__(self):
|
||||
if self.maxchars == UNLIMITED:
|
||||
return 'TextType()'
|
||||
return 'TextType(%d)' % (self.maxchars)
|
||||
return 'TextType(%d)' % self.maxchars
|
||||
|
||||
def copy(self):
|
||||
# DataType.copy will not work, because it is exported as 'string'
|
||||
@ -669,9 +631,7 @@ class TextType(StringType):
|
||||
|
||||
|
||||
class BoolType(DataType):
|
||||
"""boolean
|
||||
|
||||
"""
|
||||
"""boolean"""
|
||||
default = False
|
||||
|
||||
def export_datatype(self):
|
||||
@ -707,9 +667,6 @@ class BoolType(DataType):
|
||||
other(False)
|
||||
other(True)
|
||||
|
||||
def short_doc(self):
|
||||
return 'bool'
|
||||
|
||||
|
||||
Stub.fix_datatypes()
|
||||
|
||||
@ -721,14 +678,12 @@ Stub.fix_datatypes()
|
||||
class ArrayOf(DataType):
|
||||
"""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',
|
||||
default=0),
|
||||
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||
mandatory=True),
|
||||
}
|
||||
minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0)
|
||||
maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||
mandatory=True)
|
||||
|
||||
def __init__(self, members, minlen=0, maxlen=None):
|
||||
super().__init__()
|
||||
@ -759,14 +714,14 @@ class ArrayOf(DataType):
|
||||
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of members"""
|
||||
if key in self.__class__.properties:
|
||||
if key in self.propertyDict:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.members.setProperty(key, value)
|
||||
|
||||
def export_datatype(self):
|
||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||
members=self.members.export_datatype())
|
||||
members=self.members.export_datatype())
|
||||
|
||||
def __repr__(self):
|
||||
return 'ArrayOf(%s, %s, %s)' % (
|
||||
@ -818,16 +773,12 @@ class ArrayOf(DataType):
|
||||
except AttributeError:
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
return 'array of %s' % self.members.short_doc()
|
||||
|
||||
|
||||
class TupleOf(DataType):
|
||||
"""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):
|
||||
super().__init__()
|
||||
if not members:
|
||||
@ -855,11 +806,10 @@ class TupleOf(DataType):
|
||||
try:
|
||||
if len(value) != len(self.members):
|
||||
raise BadValueError(
|
||||
'Illegal number of Arguments! Need %d arguments.' %
|
||||
(len(self.members)))
|
||||
'Illegal number of Arguments! Need %d arguments.' % len(self.members))
|
||||
# validate elements and return as list
|
||||
return tuple(sub(elem)
|
||||
for sub, elem in zip(self.members, value))
|
||||
for sub, elem in zip(self.members, value))
|
||||
except Exception as exc:
|
||||
raise BadValueError('Can not validate:', str(exc))
|
||||
|
||||
@ -879,19 +829,16 @@ class TupleOf(DataType):
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
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):
|
||||
if not isinstance(other, TupleOf):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
if len(self.members) != len(other.members) :
|
||||
if len(self.members) != len(other.members):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
for a, b in zip(self.members, other.members):
|
||||
a.compatible(b)
|
||||
|
||||
def short_doc(self):
|
||||
return 'tuple of (%s)' % ', '.join(m.short_doc() for m in self.members)
|
||||
|
||||
|
||||
class ImmutableDict(dict):
|
||||
def _no(self, *args, **kwds):
|
||||
@ -902,8 +849,8 @@ class ImmutableDict(dict):
|
||||
class StructOf(DataType):
|
||||
"""data structure with named fields
|
||||
|
||||
:param optional: (*sequence*) optional members
|
||||
:param members: each argument denotes <member name>=<member data type>
|
||||
:param optional: a list of optional members
|
||||
:param members: names as keys and types as values for all members
|
||||
"""
|
||||
def __init__(self, optional=None, **members):
|
||||
super().__init__()
|
||||
@ -919,15 +866,15 @@ class StructOf(DataType):
|
||||
if name not in members:
|
||||
raise ProgrammingError(
|
||||
'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):
|
||||
"""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):
|
||||
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:
|
||||
res['optional'] = self.optional
|
||||
return res
|
||||
@ -979,18 +926,11 @@ class StructOf(DataType):
|
||||
except (AttributeError, TypeError, KeyError):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
return 'dict'
|
||||
|
||||
|
||||
class CommandType(DataType):
|
||||
"""command
|
||||
|
||||
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
|
||||
|
||||
@ -1049,16 +989,10 @@ class CommandType(DataType):
|
||||
except AttributeError:
|
||||
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)
|
||||
|
||||
class DataTypeType(DataType):
|
||||
"""DataType type"""
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is a valid datatype
|
||||
|
||||
@ -1102,9 +1036,7 @@ class ValueType(DataType):
|
||||
|
||||
|
||||
class NoneOr(DataType):
|
||||
"""validates a None or other
|
||||
|
||||
:param other: the other datatype"""
|
||||
"""validates a None or smth. else"""
|
||||
default = None
|
||||
|
||||
def __init__(self, other):
|
||||
@ -1119,16 +1051,8 @@ class NoneOr(DataType):
|
||||
return None
|
||||
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):
|
||||
"""validates one of the
|
||||
|
||||
:param types: each argument denotes one allowed type
|
||||
"""
|
||||
def __init__(self, *types):
|
||||
super().__init__()
|
||||
self.types = types
|
||||
@ -1142,12 +1066,6 @@ class OrType(DataType):
|
||||
pass
|
||||
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)
|
||||
Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
|
||||
@ -1161,12 +1079,6 @@ UInt64 = IntRange(0, (1 << 64) - 1)
|
||||
|
||||
# Goodie: Convenience Datatypes for Programming
|
||||
class LimitsType(TupleOf):
|
||||
"""limit (min, max) tuple
|
||||
|
||||
:param members: the type of both members
|
||||
|
||||
checks for min <= max
|
||||
"""
|
||||
def __init__(self, members):
|
||||
TupleOf.__init__(self, members, members)
|
||||
|
||||
@ -1178,22 +1090,13 @@ class LimitsType(TupleOf):
|
||||
|
||||
|
||||
class StatusType(TupleOf):
|
||||
"""SECoP status type
|
||||
|
||||
:param enum: the status code enum type
|
||||
|
||||
allows to access enum members directly
|
||||
"""
|
||||
|
||||
# shorten initialisation and allow access to status enumMembers from status values
|
||||
def __init__(self, enum):
|
||||
TupleOf.__init__(self, EnumType(enum), StringType())
|
||||
self.enum = enum
|
||||
self._enum = enum
|
||||
|
||||
def __getattr__(self, key):
|
||||
enum = TupleOf.__getattr__(self, 'enum')
|
||||
if hasattr(enum, key):
|
||||
return getattr(enum, key)
|
||||
return TupleOf.__getattr__(self, key)
|
||||
return getattr(self._enum, key)
|
||||
|
||||
|
||||
def floatargs(kwds):
|
||||
|
@ -22,7 +22,6 @@
|
||||
"""Define (internal) SECoP Errors"""
|
||||
|
||||
|
||||
|
||||
class SECoPError(RuntimeError):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
@ -138,12 +137,6 @@ def secop_error(exception):
|
||||
return InternalError(repr(exception))
|
||||
|
||||
|
||||
def fmt_error(exception):
|
||||
if isinstance(exception, SECoPError):
|
||||
return str(exception)
|
||||
return repr(exception)
|
||||
|
||||
|
||||
EXCEPTIONS = dict(
|
||||
NoSuchModule=NoSuchModuleError,
|
||||
NoSuchParameter=NoSuchParameterError,
|
||||
|
@ -24,11 +24,10 @@
|
||||
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, StringType, StructOf, TupleOf
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.modules import Command, Parameter
|
||||
from secop.modules import Command, HasAccessibles, Parameter
|
||||
|
||||
|
||||
class Feature(metaclass=ModuleMeta):
|
||||
class Feature(HasAccessibles):
|
||||
"""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: if extra elements are implemented in the pid struct they MUST BE
|
||||
# properly described in the description of the pid Parameter
|
||||
parameters = {
|
||||
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ),
|
||||
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ),
|
||||
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True),
|
||||
'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True),
|
||||
'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True),
|
||||
'pid': Parameter('(optional) Struct of p,i,d, minimum output value',
|
||||
datatype=StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
base_output=FloatRange(0),
|
||||
), 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),
|
||||
}
|
||||
|
||||
# parameters
|
||||
use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
|
||||
i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
|
||||
d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True)
|
||||
base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True)
|
||||
pid = Parameter('(optional) Struct of p,i,d, minimum output value',
|
||||
datatype=StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
base_output=FloatRange(0),
|
||||
), 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):
|
||||
parameters = {
|
||||
'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)),
|
||||
'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
||||
StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
_heater_range=FloatRange(0),
|
||||
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange'
|
||||
}
|
||||
|
||||
# parameters
|
||||
use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1))
|
||||
pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
||||
StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
_heater_range=FloatRange(0),
|
||||
_base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
|
||||
|
||||
|
||||
|
||||
|
||||
class HAS_Persistent(Feature):
|
||||
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
|
||||
# 'coupled' : Status.BUSY+2, # to be discussed.
|
||||
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
||||
#}
|
||||
parameters = {
|
||||
'persistent_mode': Parameter('Use persistent mode',
|
||||
datatype=EnumType(off=0,on=1),
|
||||
default=0, readonly=False),
|
||||
'is_persistent': Parameter('current state of persistence',
|
||||
datatype=BoolType(), optional=True),
|
||||
'stored_value': Parameter('current persistence value, often used as the modules value',
|
||||
datatype='main', unit='$', optional=True),
|
||||
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||
datatype='main', unit='$' ),
|
||||
}
|
||||
|
||||
# parameters
|
||||
persistent_mode = Parameter('Use persistent mode',
|
||||
datatype=EnumType(off=0,on=1),
|
||||
default=0, readonly=False)
|
||||
is_persistent = Parameter('current state of persistence',
|
||||
datatype=BoolType(), optional=True)
|
||||
stored_value = Parameter('current persistence value, often used as the modules value',
|
||||
datatype='main', unit='$', optional=True)
|
||||
driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||
datatype='main', unit='$' )
|
||||
|
||||
|
||||
|
||||
class HAS_Tolerance(Feature):
|
||||
# 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
|
||||
# for at least ´timewindow´ seconds.
|
||||
parameters = {
|
||||
'tolerance': Parameter('Half height of the Window',
|
||||
datatype=FloatRange(0), default=1, unit='$'),
|
||||
'timewindow': Parameter('Length of the timewindow to check',
|
||||
datatype=FloatRange(0), default=30, unit='s',
|
||||
optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
tolerance = Parameter('Half height of the Window',
|
||||
datatype=FloatRange(0), default=1, unit='$')
|
||||
timewindow = Parameter('Length of the timewindow to check',
|
||||
datatype=FloatRange(0), default=30, unit='s',
|
||||
optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_Timeout(Feature):
|
||||
parameters = {
|
||||
'timeout': Parameter('timeout for movement',
|
||||
datatype=FloatRange(0), default=0, unit='s'),
|
||||
}
|
||||
|
||||
# parameters
|
||||
timeout = Parameter('timeout for movement',
|
||||
datatype=FloatRange(0), default=0, unit='s')
|
||||
|
||||
|
||||
|
||||
class HAS_Pause(Feature):
|
||||
# just a proposal, can't agree on it....
|
||||
parameters = {
|
||||
'pause': Command('pauses movement', argument=None, result=None),
|
||||
'go': Command('continues movement or start a new one if target was change since the last pause',
|
||||
argument=None, result=None),
|
||||
}
|
||||
|
||||
@Command(argument=None, result=None)
|
||||
def pause(self):
|
||||
"""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):
|
||||
parameters = {
|
||||
'ramp': Parameter('speed of movement', unit='$/min',
|
||||
datatype=FloatRange(0)),
|
||||
'use_ramp': Parameter('use the ramping of the setpoint, or jump',
|
||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||
optional=True),
|
||||
'setpoint': Parameter('currently active setpoint',
|
||||
datatype=FloatRange(0), unit='$',
|
||||
readonly=True, ),
|
||||
}
|
||||
|
||||
# parameters
|
||||
ramp =Parameter('speed of movement', unit='$/min',
|
||||
datatype=FloatRange(0))
|
||||
use_ramp = Parameter('use the ramping of the setpoint, or jump',
|
||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||
optional=True)
|
||||
setpoint = Parameter('currently active setpoint',
|
||||
datatype=FloatRange(0), unit='$',
|
||||
readonly=True, )
|
||||
|
||||
|
||||
|
||||
class HAS_Speed(Feature):
|
||||
parameters = {
|
||||
'speed' : Parameter('(maximum) speed of movement (of the main value)',
|
||||
unit='$/s', datatype=FloatRange(0)),
|
||||
}
|
||||
|
||||
# parameters
|
||||
speed = Parameter('(maximum) speed of movement (of the main value)',
|
||||
unit='$/s', datatype=FloatRange(0))
|
||||
|
||||
|
||||
|
||||
class HAS_Accel(HAS_Speed):
|
||||
parameters = {
|
||||
'accel' : Parameter('acceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0)),
|
||||
'decel' : Parameter('deceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0), optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
accel = Parameter('acceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0))
|
||||
decel = Parameter('deceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0), optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_MotorCurrents(Feature):
|
||||
parameters = {
|
||||
'movecurrent' : Parameter('Current while moving',
|
||||
datatype=FloatRange(0)),
|
||||
'idlecurrent' : Parameter('Current while idle',
|
||||
datatype=FloatRange(0), optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
movecurrent = Parameter('Current while moving',
|
||||
datatype=FloatRange(0))
|
||||
idlecurrent = Parameter('Current while idle',
|
||||
datatype=FloatRange(0), optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_Curve(Feature):
|
||||
# proposed, not yet agreed upon!
|
||||
parameters = {
|
||||
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'),
|
||||
# XXX: tbd. (how to upload/download/select a curve?)
|
||||
}
|
||||
|
||||
# parameters
|
||||
curve = Parameter('Calibration curve', datatype=StringType(80), default='<unset>')
|
||||
|
@ -21,13 +21,13 @@
|
||||
# *****************************************************************************
|
||||
|
||||
import configparser
|
||||
from configparser import NoOptionError
|
||||
from collections import OrderedDict
|
||||
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
||||
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 configparser import NoOptionError
|
||||
|
||||
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'
|
||||
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 ')
|
||||
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
|
||||
elif itm.kind == PARAMETER and value:
|
||||
itm_lines[id(itm)] = value_str % (itm.name, value)
|
||||
@ -142,7 +142,7 @@ def read_config(file_path):
|
||||
else:
|
||||
param.addChild(TreeWidgetItem(PROPERTY,
|
||||
separated[1], get_value(config, section,
|
||||
option)))
|
||||
option)))
|
||||
node = get_comments(node, ifs, mods, file_path)
|
||||
return node, ifs, mods
|
||||
|
||||
|
@ -21,11 +21,11 @@
|
||||
# *****************************************************************************
|
||||
|
||||
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
|
||||
NODE = 'node'
|
||||
|
@ -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.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
|
||||
|
||||
|
||||
class NodeDisplay(QWidget):
|
||||
|
@ -20,10 +20,11 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from secop.gui.qt import QTreeWidgetItem, QFont, QWidget, QVBoxLayout, QLabel, \
|
||||
QHBoxLayout, QPushButton, QSize, QSizePolicy, QDialog, QTextEdit, pyqtSignal
|
||||
from secop.gui.cfg_editor.utils import setTreeIcon, setIcon, loadUi, \
|
||||
set_name_edit_style
|
||||
from secop.gui.cfg_editor.utils import loadUi, \
|
||||
set_name_edit_style, setIcon, setTreeIcon
|
||||
from secop.gui.qt import QDialog, QFont, QHBoxLayout, \
|
||||
QLabel, QPushButton, QSize, QSizePolicy, QTextEdit, \
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
|
||||
from secop.gui.valuewidgets import get_widget
|
||||
from secop.properties import Property
|
||||
|
||||
|
@ -20,15 +20,16 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from os import path, listdir
|
||||
import sys
|
||||
import inspect
|
||||
from secop.gui.qt import uic, QIcon, QSize, QFileDialog, QDialogButtonBox
|
||||
from secop.server import getGeneralConfig
|
||||
import sys
|
||||
from os import listdir, path
|
||||
|
||||
from secop.gui.qt import QDialogButtonBox, QFileDialog, QIcon, QSize, uic
|
||||
from secop.modules import Module
|
||||
from secop.params import Parameter
|
||||
from secop.properties import Property
|
||||
from secop.protocol.interface.tcp import TCPServer
|
||||
from secop.server import getGeneralConfig
|
||||
|
||||
uipath = path.dirname(__file__)
|
||||
|
||||
|
@ -23,15 +23,15 @@
|
||||
|
||||
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.utils import get_file_paths, get_modules, \
|
||||
get_interfaces, loadUi, set_name_edit_style, get_module_class_from_name, \
|
||||
get_all_items, get_interface_class_from_name, get_params, get_props, \
|
||||
setActionIcon
|
||||
from secop.gui.qt import QWidget, QDialog, QLabel, QTabBar, Qt, QPoint, QMenu, \
|
||||
QTreeWidget, QSize, pyqtSignal, QLineEdit, QComboBox, QDialogButtonBox, \
|
||||
QTextEdit, QTreeView, QStandardItemModel, QStandardItem
|
||||
from secop.gui.cfg_editor.utils import get_all_items, \
|
||||
get_file_paths, get_interface_class_from_name, get_interfaces, \
|
||||
get_module_class_from_name, get_modules, get_params, \
|
||||
get_props, loadUi, set_name_edit_style, setActionIcon
|
||||
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
|
||||
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
|
||||
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
|
||||
|
||||
NODE = 'node'
|
||||
MODULE = 'module'
|
||||
|
@ -26,9 +26,9 @@ import secop.client
|
||||
from secop.gui.modulectrl import ModuleCtrl
|
||||
from secop.gui.nodectrl import NodeCtrl
|
||||
from secop.gui.paramview import ParameterView
|
||||
from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \
|
||||
QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot, QBrush, QColor
|
||||
from secop.gui.util import loadUi, Value
|
||||
from secop.gui.qt import QBrush, QColor, QInputDialog, QMainWindow, \
|
||||
QMessageBox, QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot
|
||||
from secop.gui.util import Value, loadUi
|
||||
from secop.lib import formatExtendedTraceback
|
||||
|
||||
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
|
||||
@ -90,7 +90,7 @@ class QSECNode(QObject):
|
||||
|
||||
def queryCache(self, module):
|
||||
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):
|
||||
reply = self.conn.request(action, ident, data)
|
||||
|
@ -36,19 +36,19 @@ class CommandDialog(QDialog):
|
||||
loadUi(self, 'cmddialog.ui')
|
||||
|
||||
self.setWindowTitle('Arguments for %s' % cmdname)
|
||||
#row = 0
|
||||
# row = 0
|
||||
|
||||
self._labels = []
|
||||
self.widgets = []
|
||||
# improve! recursive?
|
||||
dtype = argument
|
||||
l = QLabel(repr(dtype))
|
||||
l.setWordWrap(True)
|
||||
w = get_widget(dtype, readonly=False)
|
||||
self.gridLayout.addWidget(l, 0, 0)
|
||||
self.gridLayout.addWidget(w, 0, 1)
|
||||
self._labels.append(l)
|
||||
self.widgets.append(w)
|
||||
label = QLabel(repr(dtype))
|
||||
label.setWordWrap(True)
|
||||
widget = get_widget(dtype, readonly=False)
|
||||
self.gridLayout.addWidget(label, 0, 0)
|
||||
self.gridLayout.addWidget(widget, 0, 1)
|
||||
self._labels.append(label)
|
||||
self.widgets.append(widget)
|
||||
|
||||
self.gridLayout.setRowStretch(1, 1)
|
||||
self.setModal(True)
|
||||
|
@ -25,14 +25,15 @@
|
||||
import json
|
||||
import pprint
|
||||
from time import sleep
|
||||
|
||||
import mlzlog
|
||||
|
||||
import secop.lib
|
||||
from secop.datatypes import EnumType, StringType
|
||||
from secop.errors import SECoPError
|
||||
from secop.gui.qt import QFont, QFontMetrics, QLabel, \
|
||||
QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped
|
||||
from secop.gui.util import loadUi, Value
|
||||
import secop.lib
|
||||
from secop.gui.util import Value, loadUi
|
||||
|
||||
|
||||
class NodeCtrl(QWidget):
|
||||
@ -167,7 +168,6 @@ class NodeCtrl(QWidget):
|
||||
print(secop.lib.formatExtendedTraceback())
|
||||
widget = QLabel('Bad configured Module %s! (%s)' % (modname, e))
|
||||
|
||||
|
||||
if unit:
|
||||
labelstr = '%s (%s):' % (modname, unit)
|
||||
else:
|
||||
@ -289,7 +289,7 @@ class DrivableWidget(ReadableWidget):
|
||||
|
||||
def update_current(self, value):
|
||||
self.currentLineEdit.setText(str(value))
|
||||
#elif self._is_enum:
|
||||
# elif self._is_enum:
|
||||
# member = self._map[self._revmap[value.value]]
|
||||
# self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
|
||||
|
||||
|
@ -22,12 +22,9 @@
|
||||
# *****************************************************************************
|
||||
|
||||
|
||||
from secop.datatypes import EnumType, FloatRange, IntRange
|
||||
from secop.gui.qt import QPushButton as QButton
|
||||
from secop.gui.qt import QCheckBox, QLabel, QLineEdit, \
|
||||
QMessageBox, QSizePolicy, Qt, QWidget, pyqtSignal, pyqtSlot
|
||||
from secop.datatypes import EnumType
|
||||
from secop.gui.qt import QWidget, pyqtSignal, pyqtSlot
|
||||
from secop.gui.util import loadUi
|
||||
from secop.lib import formatExtendedStack
|
||||
|
||||
|
||||
class ParameterWidget(QWidget):
|
||||
|
@ -32,6 +32,7 @@ uipath = path.dirname(__file__)
|
||||
def loadUi(widget, uiname, subdir='ui'):
|
||||
uic.loadUi(path.join(uipath, subdir, uiname), widget)
|
||||
|
||||
|
||||
class Value:
|
||||
def __init__(self, value, timestamp=None, readerror=None):
|
||||
self.value = value
|
||||
|
@ -23,12 +23,13 @@
|
||||
|
||||
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||
FloatRange, IntRange, StringType, StructOf, TupleOf, TextType
|
||||
from secop.gui.qt import QCheckBox, QComboBox, QDialog, QDoubleSpinBox, \
|
||||
QFrame, QGridLayout, QGroupBox, QLabel, QLineEdit, QSpinBox, QVBoxLayout, \
|
||||
QTextEdit
|
||||
FloatRange, IntRange, StringType, StructOf, TextType, TupleOf
|
||||
from secop.gui.qt import QCheckBox, QComboBox, QDialog, \
|
||||
QDoubleSpinBox, QFrame, QGridLayout, QGroupBox, \
|
||||
QLabel, QLineEdit, QSpinBox, QTextEdit, QVBoxLayout
|
||||
from secop.gui.util import loadUi
|
||||
|
||||
|
||||
# XXX: implement live validators !!!!
|
||||
# XXX: signals upon change of value
|
||||
# XXX: honor readonly in all cases!
|
||||
@ -171,12 +172,12 @@ class StructWidget(QGroupBox):
|
||||
self._labels = []
|
||||
for idx, name in enumerate(sorted(datatype.members)):
|
||||
dt = datatype.members[name]
|
||||
w = get_widget(dt, readonly=readonly, parent=self)
|
||||
l = QLabel(name)
|
||||
self.layout.addWidget(l, idx, 0)
|
||||
self.layout.addWidget(w, idx, 1)
|
||||
self._labels.append(l)
|
||||
self.subwidgets[name] = (w, dt)
|
||||
widget = get_widget(dt, readonly=readonly, parent=self)
|
||||
label = QLabel(name)
|
||||
self.layout.addWidget(label, idx, 0)
|
||||
self.layout.addWidget(widget, idx, 1)
|
||||
self._labels.append(label)
|
||||
self.subwidgets[name] = (widget, dt)
|
||||
self.datatypes.append(dt)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
@ -215,21 +216,22 @@ class ArrayWidget(QGroupBox):
|
||||
w.set_value(v)
|
||||
|
||||
|
||||
|
||||
def get_widget(datatype, readonly=False, parent=None):
|
||||
return {FloatRange: FloatWidget,
|
||||
IntRange: IntWidget,
|
||||
StringType: StringWidget,
|
||||
TextType: TextWidget,
|
||||
BLOBType: BlobWidget,
|
||||
EnumType: EnumWidget,
|
||||
BoolType: BoolWidget,
|
||||
TupleOf: TupleWidget,
|
||||
StructOf: StructWidget,
|
||||
ArrayOf: ArrayWidget,
|
||||
return {
|
||||
FloatRange: FloatWidget,
|
||||
IntRange: IntWidget,
|
||||
StringType: StringWidget,
|
||||
TextType: TextWidget,
|
||||
BLOBType: BlobWidget,
|
||||
EnumType: EnumWidget,
|
||||
BoolType: BoolWidget,
|
||||
TupleOf: TupleWidget,
|
||||
StructOf: StructWidget,
|
||||
ArrayOf: ArrayWidget,
|
||||
}.get(datatype.__class__)(datatype, readonly, parent)
|
||||
# TODO: handle NoneOr
|
||||
|
||||
|
||||
class msg(QDialog):
|
||||
def __init__(self, stuff, parent=None):
|
||||
super(msg, self).__init__(parent)
|
||||
@ -242,7 +244,7 @@ class msg(QDialog):
|
||||
dt = StructOf(i=IntRange(0, 10), f=FloatRange(), b=BoolType())
|
||||
w = StructWidget(dt)
|
||||
self.gridLayout.addWidget(w, row, 1)
|
||||
row+=1
|
||||
row += 1
|
||||
|
||||
self.gridLayout.addWidget(QLabel('stuff'), row, 0, 1, 0)
|
||||
row += 1 # at pos (0,0) span 2 cols, 1 row
|
||||
|
@ -54,8 +54,8 @@ method has to be called explicitly int the write_<parameter> method, if needed.
|
||||
"""
|
||||
import re
|
||||
|
||||
from secop.metaclass import Done
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.modules import Done
|
||||
|
||||
|
||||
class CmdParser:
|
||||
@ -202,13 +202,19 @@ class IOHandler(IOHandlerBase):
|
||||
: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
|
||||
omitted if no write happens
|
||||
|
||||
"""
|
||||
"""
|
||||
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
|
||||
|
||||
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.parameters = set()
|
||||
self._module_class = None
|
||||
|
@ -21,13 +21,13 @@
|
||||
# *****************************************************************************
|
||||
"""Define helpers"""
|
||||
|
||||
import importlib
|
||||
import linecache
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import importlib
|
||||
from os import path, environ
|
||||
from os import environ, path
|
||||
|
||||
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
||||
|
||||
@ -58,6 +58,7 @@ CONFIG['basedir'] = repodir
|
||||
|
||||
unset_value = object()
|
||||
|
||||
|
||||
class lazy_property:
|
||||
"""A property that calculates its value only once."""
|
||||
|
||||
|
@ -28,16 +28,18 @@ support for asynchronous communication, but may be used also for
|
||||
synchronous IO (see secop.stringio.StringIO)
|
||||
"""
|
||||
|
||||
import socket
|
||||
import select
|
||||
import time
|
||||
import ast
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
|
||||
from secop.errors import CommunicationFailedError, ConfigError
|
||||
from secop.lib import closeSocket, parseHostPort, tcpSocket
|
||||
|
||||
try:
|
||||
from serial import Serial
|
||||
except ImportError:
|
||||
Serial = None
|
||||
from secop.lib import parseHostPort, tcpSocket, closeSocket
|
||||
from secop.errors import ConfigError, CommunicationFailedError
|
||||
|
||||
|
||||
class ConnectionClosed(ConnectionError):
|
||||
@ -60,10 +62,10 @@ class AsynConn:
|
||||
except (ValueError, TypeError, AssertionError):
|
||||
if 'COM' in uri:
|
||||
raise ValueError("the correct uri for a COM port is: "
|
||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'" )
|
||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
|
||||
if '/dev' in uri:
|
||||
raise ValueError("the correct uri for a serial port is: "
|
||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'" )
|
||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
|
||||
raise ValueError('invalid uri: %s' % uri)
|
||||
iocls = cls.SCHEME_MAP['tcp']
|
||||
uri = 'tcp://%s:%d' % host_port
|
||||
|
@ -20,57 +20,168 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from inspect import cleandoc
|
||||
from textwrap import indent
|
||||
|
||||
from secop.modules import Command, HasProperties, Module, Parameter, Property
|
||||
|
||||
|
||||
def indent_description(p):
|
||||
"""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
|
||||
|
||||
:param cls: the class with the doc string to be extended
|
||||
:param name: the name of the attribute dict to be used
|
||||
:param title: the title to be used
|
||||
:param newitems: the set of new items defined for this class
|
||||
:param lines: content of the docstring, as lines
|
||||
:param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
|
||||
: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
|
||||
or an empty string to suppress output for this item
|
||||
: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, {})
|
||||
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)
|
||||
clsset = set()
|
||||
if tail: # take all
|
||||
inherited = set()
|
||||
fmted = ''.join(fmtdict.values())
|
||||
fmted = fmtdict.values()
|
||||
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)
|
||||
if not tail:
|
||||
head, _, tail = doc.partition('{no %s}' % name)
|
||||
if tail: # add no information
|
||||
return
|
||||
# no tag found: append to the end
|
||||
if fmted:
|
||||
clsset = set()
|
||||
for name in inherited:
|
||||
p = allitems[name]
|
||||
refcls = cls
|
||||
|
||||
fmted = []
|
||||
for key, formatted_item in fmtdict.items():
|
||||
if not formatted_item:
|
||||
continue
|
||||
# find where item is defined or modified
|
||||
refcls = None
|
||||
for base in cls.__mro__:
|
||||
dp = getattr(base, attrname, {}).get(name)
|
||||
if dp:
|
||||
if dp == p:
|
||||
p = getattr(base, attrname, {}).get(key)
|
||||
if isinstance(p, itemcls):
|
||||
if fmtfunc(key, p) == formatted_item:
|
||||
refcls = base
|
||||
else:
|
||||
break
|
||||
clsset.add(refcls)
|
||||
clsset.discard(cls)
|
||||
if refcls == 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:
|
||||
fmted += ' - see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||
for c in cls.__mro__ if c in clsset))
|
||||
cls.__doc__ = '%s\n\n:%s: %s\n%s' % (head, title, fmted, tail)
|
||||
fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||
for c in cls.__mro__ if c in clsset)))
|
||||
|
||||
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)
|
||||
|
@ -32,6 +32,7 @@ class EnumMember:
|
||||
has an int-type value and attributes 'name' and 'value'
|
||||
"""
|
||||
__slots__ = ['name', 'value', 'enum']
|
||||
|
||||
def __init__(self, enum, name, value):
|
||||
if not isinstance(enum, Enum):
|
||||
raise TypeError('1st Argument must be an instance of class Enum()')
|
||||
@ -49,7 +50,7 @@ class EnumMember:
|
||||
try:
|
||||
other = int(other)
|
||||
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:!
|
||||
if self.value < other:
|
||||
return -1
|
||||
@ -59,10 +60,12 @@ class EnumMember:
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
|
||||
|
||||
def __le__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, (EnumMember)):
|
||||
if isinstance(other, EnumMember):
|
||||
return other.value == self.value
|
||||
if isinstance(other, int):
|
||||
return other == self.value
|
||||
@ -72,10 +75,13 @@ class EnumMember:
|
||||
return self.name == other
|
||||
return False
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
|
||||
|
||||
@ -100,77 +106,105 @@ class EnumMember:
|
||||
def __repr__(self):
|
||||
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?
|
||||
def __add__(self, other):
|
||||
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __floordiv__(self, other):
|
||||
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __mod__(self, other):
|
||||
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __divmod__(self, other):
|
||||
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __pow__(self, other, *args):
|
||||
return self.value.__pow__(other, *args)
|
||||
|
||||
def __lshift__(self, other):
|
||||
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rshift__(self, other):
|
||||
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rfloordiv__(self, other):
|
||||
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rmod__(self, other):
|
||||
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rdivmod__(self, other):
|
||||
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rpow__(self, other, *args):
|
||||
return self.value.__rpow__(other, *args)
|
||||
|
||||
def __rlshift__(self, other):
|
||||
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rrshift__(self, other):
|
||||
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
# logical operations
|
||||
def __and__(self, other):
|
||||
return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __xor__(self, other):
|
||||
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __or__(self, other):
|
||||
return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rxor__(self, other):
|
||||
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
# other stuff
|
||||
def __neg__(self):
|
||||
return self.value.__neg__()
|
||||
|
||||
def __pos__(self):
|
||||
return self.value.__pos__()
|
||||
|
||||
def __abs__(self):
|
||||
return self.value.__abs__()
|
||||
|
||||
def __invert__(self):
|
||||
return self.value.__invert__()
|
||||
|
||||
def __int__(self):
|
||||
return self.value.__int__()
|
||||
|
||||
def __float__(self):
|
||||
return self.value.__float__()
|
||||
#return NotImplemented # makes no sense
|
||||
|
||||
def __index__(self):
|
||||
return self.value.__index__()
|
||||
|
||||
@ -206,6 +240,7 @@ class Enum(dict):
|
||||
You only can create an extended Enum.
|
||||
"""
|
||||
name = ''
|
||||
|
||||
def __init__(self, name='', parent=None, **kwds):
|
||||
super(Enum, self).__init__()
|
||||
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
|
||||
# this means, an extended Enum behaves like the parent
|
||||
# THIS MAY BE CONFUSING SOMETIMES!
|
||||
name=parent.name
|
||||
name = parent.name
|
||||
# else:
|
||||
# raise TypeError('Enum instances need a name or an Enum parent!')
|
||||
if not isinstance(name, str):
|
||||
@ -225,8 +260,9 @@ class Enum(dict):
|
||||
|
||||
names = set()
|
||||
values = set()
|
||||
|
||||
# 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"""
|
||||
if v is None:
|
||||
# sugar: take the next free number if value was None
|
||||
@ -237,7 +273,7 @@ class Enum(dict):
|
||||
if v in names:
|
||||
v = self[v].value
|
||||
while v in values:
|
||||
v +=1
|
||||
v += 1
|
||||
|
||||
# check that the value is an int
|
||||
_v = int(v)
|
||||
@ -290,7 +326,6 @@ class Enum(dict):
|
||||
|
||||
def __repr__(self):
|
||||
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):
|
||||
return self[key]
|
||||
|
@ -21,6 +21,7 @@
|
||||
# *****************************************************************************
|
||||
"""Define parsing helpers"""
|
||||
|
||||
# TODO: remove, as currently not used
|
||||
|
||||
import re
|
||||
import time
|
||||
|
@ -141,7 +141,7 @@ class SequencerMixin:
|
||||
return self.read_hw_status()
|
||||
return self.Status.IDLE, ''
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
if self.seq_is_alive():
|
||||
self._seq_stopflag = True
|
||||
|
||||
|
@ -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
|
353
secop/modules.py
353
secop/modules.py
@ -20,32 +20,158 @@
|
||||
# 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 time
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
||||
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
|
||||
from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueError,\
|
||||
SilentError, InternalError, secop_error
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
||||
IntRange, StatusType, StringType, TextType, TupleOf, get_datatype
|
||||
from secop.errors import BadValueError, ConfigError, InternalError, \
|
||||
ProgrammingError, SECoPError, SilentError, secop_error
|
||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||
from secop.lib.enum import Enum
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
|
||||
from secop.params import PREDEFINED_ACCESSIBLES, Accessible, Command, Parameter
|
||||
from secop.poller import BasicPoller, Poller
|
||||
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.
|
||||
# Idea: every Module defined herein is also a 'protocol'-Module,
|
||||
# all others MUST derive from those, the 'interface'-class is still derived
|
||||
# from these base classes (how to do this?)
|
||||
class HasAccessibles(HasProperties):
|
||||
"""base class of module
|
||||
|
||||
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
|
||||
|
||||
all SECoP modules derive from this.
|
||||
@ -58,7 +184,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
Notes:
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
# with data from a 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,
|
||||
# datatype is fixed!
|
||||
properties = {
|
||||
'export': Property('flag if this Module is to be exported', BoolType(), default=True, export=False),
|
||||
'group': Property('optional group the Module belongs to', StringType(), default='', extname='group'),
|
||||
'description': Property('description of the module', TextType(), extname='description', mandatory=True),
|
||||
'meaning': Property('dptional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
|
||||
default=('',0), extname='meaning'),
|
||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
default='user', extname='visibility'),
|
||||
'implementation': Property('internal name of the implementation class of the module', StringType(),
|
||||
extname='implementation'),
|
||||
'interface_classes': Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes'),
|
||||
}
|
||||
export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
|
||||
group = Property('optional group the module belongs to', StringType(), default='', extname='group')
|
||||
description = Property('description of the module', TextType(), extname='description', mandatory=True)
|
||||
meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
|
||||
default=('', 0), extname='meaning')
|
||||
visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
default='user', extname='visibility')
|
||||
implementation = Property('internal name of the implementation class of the module', StringType(),
|
||||
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
|
||||
parameters = {} #: definition of parameters
|
||||
commands = {} #: definition of commands
|
||||
parameters = {}
|
||||
commands = {}
|
||||
|
||||
# reference to the dispatcher (used for sending async updates)
|
||||
DISPATCHER = None
|
||||
@ -112,14 +235,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
# handle module properties
|
||||
# 1) make local copies of properties
|
||||
super(Module, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
# 2) check and apply properties specified in cfgdict
|
||||
# specified as '.<propertyname> = <propertyvalue>'
|
||||
# (this is for legacy config files only)
|
||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||
if k[0] == '.':
|
||||
if k[1:] in self.__class__.properties:
|
||||
if k[1:] in self.propertyDict:
|
||||
self.setProperty(k[1:], cfgdict.pop(k))
|
||||
else:
|
||||
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
|
||||
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
||||
for k in self.__class__.properties:
|
||||
for k in self.propertyDict:
|
||||
if k in cfgdict:
|
||||
self.setProperty(k, cfgdict.pop(k))
|
||||
|
||||
# 4) set automatic properties
|
||||
mycls = self.__class__
|
||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||
self.properties['implementation'] = myclassname
|
||||
self.implementation = myclassname
|
||||
# list of all 'secop' modules
|
||||
self.properties['interface_classes'] = [
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||
# self.interface_classes = [
|
||||
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||
# list of only the 'highest' secop module class
|
||||
self.properties['interface_classes'] = [[
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
|
||||
self.interface_classes = [
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
|
||||
|
||||
# handle Features
|
||||
# XXX: todo
|
||||
@ -149,7 +272,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# 1) make local copies of parameter objects
|
||||
# they need to be individual per instance since we use them also
|
||||
# to cache the current value + qualifiers...
|
||||
accessibles = OrderedDict()
|
||||
accessibles = {}
|
||||
# conversion from exported names to internal attribute names
|
||||
accessiblename2attr = {}
|
||||
for aname, aobj in self.accessibles.items():
|
||||
@ -158,31 +281,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if isinstance(aobj, Parameter):
|
||||
# fix default properties poll and needscfg
|
||||
if aobj.poll is None:
|
||||
aobj.properties['poll'] = bool(aobj.handler)
|
||||
aobj.poll = bool(aobj.handler)
|
||||
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
|
||||
aobj.properties['export'] = False
|
||||
aobj.export = False
|
||||
if aobj.export:
|
||||
if aobj.export is True:
|
||||
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
||||
if predefined_obj:
|
||||
if isinstance(aobj, predefined_obj):
|
||||
aobj.setProperty('export', aname)
|
||||
aobj.export = aname
|
||||
else:
|
||||
raise ProgrammingError("can not use '%s' as name of a %s" %
|
||||
(aname, aobj.__class__.__name__))
|
||||
else: # create custom parameter
|
||||
aobj.setProperty('export', '_' + aname)
|
||||
(aname, aobj.__class__.__name__))
|
||||
else: # create custom parameter
|
||||
aobj.export = '_' + aname
|
||||
accessiblename2attr[aobj.export] = aname
|
||||
accessibles[aname] = aobj
|
||||
# do not re-use self.accessibles as this is the same for all instances
|
||||
self.accessibles = accessibles
|
||||
self.accessiblename2attr = accessiblename2attr
|
||||
# provide properties to 'filter' out the parameters/commands
|
||||
self.parameters = 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.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
|
||||
self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
|
||||
|
||||
# 2) check and apply parameter_properties
|
||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||
@ -199,6 +322,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
else:
|
||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||
(self.name, paramname, propname))
|
||||
else:
|
||||
raise ConfigError('Module %s has no Parameter %r!' %
|
||||
(self.name, paramname))
|
||||
|
||||
# 3) check config for problems:
|
||||
# only accept remaining config items specified in parameters
|
||||
@ -208,7 +334,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
'Module %s:config Parameter %r '
|
||||
'not understood! (use one of %s)' %
|
||||
(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
|
||||
# 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 not pobj.readonly and pobj.initwrite is not False:
|
||||
# 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:
|
||||
pobj.value = pobj.datatype(cfgdict[pname])
|
||||
except BadValueError as e:
|
||||
@ -228,7 +355,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
else:
|
||||
if pobj.default is None:
|
||||
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!' %
|
||||
(self.name, pname))
|
||||
# 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:
|
||||
raise ProgrammingError('bad default for %s.%s: %s'
|
||||
% (name, pname, e))
|
||||
if pobj.initwrite:
|
||||
if pobj.initwrite and not pobj.readonly:
|
||||
# we will need to call write_<pname>
|
||||
# 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
|
||||
self.writeDict[pname] = value
|
||||
else:
|
||||
@ -312,7 +440,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
for cb in cblist:
|
||||
try:
|
||||
cb(arg)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# print(formatExtendedTraceback())
|
||||
pass
|
||||
|
||||
@ -358,38 +486,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
modobj.announceUpdate(p, value)
|
||||
self.valueCallbacks[pname].append(cb)
|
||||
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""helper function for treating substates of BUSY correctly"""
|
||||
# defined even for non drivable (used for dynamic polling)
|
||||
return False
|
||||
|
||||
def earlyInit(self):
|
||||
"""may be overriden in derived classes to init stuff
|
||||
|
||||
after creating the module (no super call needed)
|
||||
"""
|
||||
# may be overriden in derived classes to init stuff
|
||||
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
||||
|
||||
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__)
|
||||
|
||||
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):
|
||||
"""poll parameter <pname> with proper error handling"""
|
||||
try:
|
||||
@ -421,33 +529,33 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if 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):
|
||||
"""basic readable module"""
|
||||
# pylint: disable=invalid-name
|
||||
Status = Enum('Status',
|
||||
IDLE = 100,
|
||||
WARN = 200,
|
||||
UNSTABLE = 270,
|
||||
ERROR = 400,
|
||||
DISABLED = 0,
|
||||
UNKNOWN = 401,
|
||||
) #: status codes
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
datatype=FloatRange(),
|
||||
poll=True,
|
||||
),
|
||||
'pollinterval': Parameter('sleeptime between polls', default=5,
|
||||
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,
|
||||
),
|
||||
}
|
||||
IDLE=100,
|
||||
WARN=200,
|
||||
UNSTABLE=270,
|
||||
ERROR=400,
|
||||
DISABLED=0,
|
||||
UNKNOWN=401,
|
||||
) #: status codes
|
||||
|
||||
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||
default=(Status.IDLE, ''), poll=True)
|
||||
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||
default=5, readonly=False)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""start basic polling thread"""
|
||||
@ -496,30 +604,17 @@ class Readable(Module):
|
||||
|
||||
class Writable(Readable):
|
||||
"""basic writable module"""
|
||||
parameters = {
|
||||
'target': Parameter('target value of the Module',
|
||||
default=0, readonly=False, datatype=FloatRange(),
|
||||
),
|
||||
}
|
||||
|
||||
target = Parameter('target value of the module',
|
||||
default=0, readonly=False, datatype=FloatRange())
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
"""basic drivable module"""
|
||||
|
||||
Status = Enum(Readable.Status, BUSY=300) #: Status codes
|
||||
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||
|
||||
commands = {
|
||||
'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)),
|
||||
}
|
||||
status = Parameter(datatype=StatusType(Status)) # override Readable.status
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""check for busy, treating substates correctly
|
||||
@ -533,7 +628,6 @@ class Drivable(Writable):
|
||||
|
||||
returns True when busy, but not finalizing
|
||||
"""
|
||||
""""""
|
||||
return 300 <= (status or self.status)[0] < 390
|
||||
|
||||
# improved polling: may poll faster if module is BUSY
|
||||
@ -554,26 +648,16 @@ class Drivable(Writable):
|
||||
self.pollOneParam(pname)
|
||||
return fastpoll
|
||||
|
||||
def do_stop(self):
|
||||
# default implementation of the stop command
|
||||
# by default does nothing
|
||||
pass
|
||||
@Command(None, result=None)
|
||||
def stop(self):
|
||||
"""cease driving, go to IDLE state"""
|
||||
|
||||
|
||||
class Communicator(Module):
|
||||
"""basic communication module
|
||||
"""basic abstract communication module"""
|
||||
|
||||
providing no parameters, but a 'communicate' command.
|
||||
"""
|
||||
|
||||
commands = {
|
||||
"communicate": Command("provides the simplest mean to communication",
|
||||
argument=StringType(),
|
||||
result=StringType()
|
||||
),
|
||||
}
|
||||
|
||||
def do_communicate(self, command):
|
||||
@Command(StringType(), result=StringType())
|
||||
def communicate(self, command):
|
||||
"""communicate command
|
||||
|
||||
:param command: the command to be sent
|
||||
@ -583,7 +667,7 @@ class Communicator(Module):
|
||||
|
||||
|
||||
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,
|
||||
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
|
||||
def __init__(self, attrname=None):
|
||||
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):
|
||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
||||
|
607
secop/params.py
607
secop/params.py
@ -23,154 +23,243 @@
|
||||
"""Define classes for Parameters/Commands and Overriding them"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from inspect import cleandoc
|
||||
import inspect
|
||||
|
||||
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
||||
NoneOr, TextType, IntRange
|
||||
from secop.errors import ProgrammingError, BadValueError
|
||||
from secop.datatypes import BoolType, CommandType, DataType, \
|
||||
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
|
||||
StringType, StructOf, TextType, TupleOf, ValueType
|
||||
from secop.errors import BadValueError, ProgrammingError
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
|
||||
class CountedObj:
|
||||
ctr = [0]
|
||||
def __init__(self):
|
||||
cl = self.__class__.ctr
|
||||
cl[0] += 1
|
||||
self.ctr = cl[0]
|
||||
UNSET = object() # an argument not given, not even None
|
||||
|
||||
|
||||
class Accessible(HasProperties, CountedObj):
|
||||
'''base class for Parameter and Command'''
|
||||
class Accessible(HasProperties):
|
||||
"""base class for Parameter and Command"""
|
||||
|
||||
properties = {}
|
||||
kwds = None # is a dict if it might be used as Override
|
||||
|
||||
def __init__(self, **kwds):
|
||||
super(Accessible, self).__init__()
|
||||
# do not use self.properties.update here, as no invalid values should be
|
||||
super().__init__()
|
||||
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
|
||||
for k,v in kwds.items():
|
||||
for k, v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join(
|
||||
['%s=%r' % (k, self.properties.get(k, v.default)) for k, v in sorted(self.__class__.properties.items())]))
|
||||
def inherit(self, cls, owner):
|
||||
for base in owner.__bases__:
|
||||
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):
|
||||
# return a copy of ourselfs
|
||||
props = dict(self.properties, ctr=self.ctr)
|
||||
# deep copy, as datatype might be altered from config
|
||||
props['datatype'] = props['datatype'].copy()
|
||||
return type(self)(**props)
|
||||
"""return a (deep) copy of ourselfs"""
|
||||
raise NotImplementedError
|
||||
|
||||
def for_export(self):
|
||||
"""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):
|
||||
"""storage for parameter settings + value + qualifiers"""
|
||||
# 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....
|
||||
"""defines a parameter
|
||||
|
||||
properties = {
|
||||
'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', 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),
|
||||
}
|
||||
:param description: description
|
||||
:param datatype: the datatype
|
||||
:param inherit: whether properties not given should be inherited
|
||||
:param kwds: optional properties
|
||||
"""
|
||||
# storage for Parameter settings + value + qualifiers
|
||||
|
||||
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:
|
||||
self.ctr = ctr
|
||||
if it can not be read from the hardware''', ValueType(),
|
||||
export=False, default=None)
|
||||
export = Property(
|
||||
'''[internal] export settings
|
||||
|
||||
if not isinstance(datatype, DataType):
|
||||
if issubclass(datatype, DataType):
|
||||
# goodie: make an instance from a class (forgotten ()???)
|
||||
datatype = datatype()
|
||||
else:
|
||||
raise ProgrammingError(
|
||||
'datatype MUST be derived from class DataType!')
|
||||
* 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
|
||||
|
||||
kwds['description'] = cleandoc(description)
|
||||
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)
|
||||
may be:
|
||||
|
||||
if self.initwrite and self.readonly:
|
||||
raise ProgrammingError('can not have both readonly and initwrite!')
|
||||
* 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, settable=False)
|
||||
initwrite = Property(
|
||||
'''[internal] write this parameter on initialization
|
||||
|
||||
if self.constant is not None:
|
||||
self.properties['readonly'] = True
|
||||
default None: write if given in config''', NoneOr(BoolType()),
|
||||
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
|
||||
# serialised version of the constant, or unset
|
||||
constant = self.datatype(kwds['constant'])
|
||||
self.properties['constant'] = self.datatype.export_value(constant)
|
||||
self.constant = self.datatype.export_value(constant)
|
||||
self.readonly = True
|
||||
|
||||
# internal caching: value and timestamp of last change...
|
||||
self.value = self.default
|
||||
self.timestamp = 0
|
||||
self.readerror = None # if not None, indicates that last read was not successful
|
||||
if 'default' in self.propertyValues:
|
||||
# fixes in case datatype has changed
|
||||
try:
|
||||
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):
|
||||
return self.datatype.export_value(self.value)
|
||||
|
||||
def for_export(self):
|
||||
return dict(self.exportProperties(), readonly=self.readonly)
|
||||
|
||||
def getProperties(self):
|
||||
"""get also properties of datatype"""
|
||||
superProp = super().getProperties().copy()
|
||||
superProp.update(self.datatype.getProperties())
|
||||
return superProp
|
||||
super_prop = super().getProperties().copy()
|
||||
super_prop.update(self.datatype.getProperties())
|
||||
return super_prop
|
||||
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of datatype"""
|
||||
if key in self.__class__.properties:
|
||||
if key in self.propertyDict:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.datatype.setProperty(key, value)
|
||||
@ -179,158 +268,168 @@ class Parameter(Accessible):
|
||||
super().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):
|
||||
"""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):
|
||||
kwds['description'] = cleandoc(description)
|
||||
kwds['datatype'] = CommandType(kwds.get('argument', None), kwds.get('result', None))
|
||||
super(Command, self).__init__(**kwds)
|
||||
if ctr is not None:
|
||||
self.ctr = ctr
|
||||
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
|
||||
|
||||
@property
|
||||
def argument(self):
|
||||
return self.datatype.argument
|
||||
* 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(
|
||||
"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
|
||||
def result(self):
|
||||
return self.datatype.result
|
||||
func = None
|
||||
|
||||
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
|
||||
PREDEFINED_ACCESSIBLES = dict(
|
||||
value = Parameter,
|
||||
status = Parameter,
|
||||
target = Parameter,
|
||||
pollinterval = Parameter,
|
||||
ramp = Parameter,
|
||||
user_ramp = Parameter,
|
||||
setpoint = Parameter,
|
||||
time_to_target = Parameter,
|
||||
unit = Parameter, # reserved name
|
||||
loglevel = Parameter, # reserved name
|
||||
mode = Parameter, # reserved name
|
||||
stop = Command,
|
||||
reset = Command,
|
||||
go = Command,
|
||||
abort = Command,
|
||||
shutdown = Command,
|
||||
communicate = Command,
|
||||
value=Parameter,
|
||||
status=Parameter,
|
||||
target=Parameter,
|
||||
pollinterval=Parameter,
|
||||
ramp=Parameter,
|
||||
user_ramp=Parameter,
|
||||
setpoint=Parameter,
|
||||
time_to_target=Parameter,
|
||||
unit=Parameter, # reserved name
|
||||
loglevel=Parameter, # reserved name
|
||||
mode=Parameter, # reserved name
|
||||
stop=Command,
|
||||
reset=Command,
|
||||
go=Command,
|
||||
abort=Command,
|
||||
shutdown=Command,
|
||||
communicate=Command,
|
||||
)
|
||||
|
@ -34,10 +34,11 @@ Usage examples:
|
||||
"""
|
||||
|
||||
import time
|
||||
from threading import Event
|
||||
from heapq import heapify, heapreplace
|
||||
from secop.lib import mkthread
|
||||
from threading import Event
|
||||
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.lib import mkthread
|
||||
|
||||
# poll types:
|
||||
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
||||
@ -166,8 +167,8 @@ class Poller(PollerBase):
|
||||
continue # only one poller per handler
|
||||
handlers.add(pobj.handler)
|
||||
# placeholders 0 are used for due, lastdue and idx
|
||||
self.queues[polltype].append((0, 0,
|
||||
(0, module, pobj, pname, factors[polltype])))
|
||||
self.queues[polltype].append(
|
||||
(0, 0, (0, module, pobj, pname, factors[polltype])))
|
||||
|
||||
def poll_next(self, polltype):
|
||||
"""try to poll next item
|
||||
|
@ -23,11 +23,44 @@
|
||||
"""Define validated data types."""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from inspect import cleandoc
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
||||
from secop.lib.classdoc import append_to_doc, indent_description
|
||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||
|
||||
|
||||
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'
|
||||
@ -35,175 +68,148 @@ class Property:
|
||||
"""base class holding info about a property
|
||||
|
||||
: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.
|
||||
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
||||
when they match the default
|
||||
: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
|
||||
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
|
||||
"""
|
||||
|
||||
# note: this is intended to be used on base classes.
|
||||
# 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):
|
||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||
self.description = cleandoc(description)
|
||||
self.default = datatype.default if default is None else datatype(default)
|
||||
self.description = inspect.cleandoc(description)
|
||||
self.default = datatype.default if default is UNSET else datatype(default)
|
||||
self.datatype = datatype
|
||||
self.extname = extname
|
||||
self.export = export or bool(extname)
|
||||
if mandatory is None:
|
||||
mandatory = default is None
|
||||
mandatory = default is UNSET
|
||||
self.mandatory = mandatory
|
||||
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):
|
||||
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % (
|
||||
self.description, self.datatype, self.default, self.extname, self.export,
|
||||
self.mandatory, self.settable)
|
||||
extras = ['default=%s' % repr(self.default)]
|
||||
if self.export:
|
||||
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):
|
||||
"""a collection of `Property` objects
|
||||
|
||||
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 = {}
|
||||
class HasProperties(HasDescriptors):
|
||||
propertyValues = None
|
||||
|
||||
def __init__(self):
|
||||
super(HasProperties, self).__init__()
|
||||
self.initProperties()
|
||||
|
||||
def initProperties(self):
|
||||
# store property values in the instance, keep descriptors on the class
|
||||
self.properties = {}
|
||||
# pre-init with properties default value (if any)
|
||||
for pn, po in self.__class__.properties.items():
|
||||
value = getattr(self, '_initProp_' + pn, self)
|
||||
if value is not self: # property value was given as attribute
|
||||
self.properties[pn] = value
|
||||
elif not po.mandatory:
|
||||
self.properties[pn] = po.default
|
||||
self.propertyValues = {}
|
||||
# pre-init
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.value is not UNSET:
|
||||
self.setProperty(pn, po.value)
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
"""validates properties and checks for min... <= max..."""
|
||||
for pn, po in self.__class__.properties.items():
|
||||
if po.export and po.mandatory:
|
||||
if pn not in self.properties:
|
||||
name = getattr(self, 'name', repr(self))
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.mandatory:
|
||||
if pn not in self.propertyDict:
|
||||
name = getattr(self, 'name', self.__class__.__name__)
|
||||
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||
# apply validator (which may complain further)
|
||||
self.properties[pn] = po.datatype(self.properties[pn])
|
||||
for pn, po in self.__class__.properties.items():
|
||||
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
||||
for pn, po in self.propertyDict.items():
|
||||
if pn.startswith('min'):
|
||||
maxname = 'max' + pn[3:]
|
||||
minval = self.properties[pn]
|
||||
maxval = self.properties.get(maxname, minval)
|
||||
minval = self.propertyValues.get(pn, po.default)
|
||||
maxval = self.propertyValues.get(maxname, minval)
|
||||
if minval > maxval:
|
||||
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
||||
|
||||
|
||||
def getProperties(self):
|
||||
return self.__class__.properties
|
||||
return self.propertyDict
|
||||
|
||||
def exportProperties(self):
|
||||
# export properties which have
|
||||
# export=True and
|
||||
# mandatory=True or non_default=True
|
||||
res = {}
|
||||
for pn, po in self.__class__.properties.items():
|
||||
val = self.properties.get(pn, None)
|
||||
if po.export and (po.mandatory or val != po.default):
|
||||
for pn, po in self.propertyDict.items():
|
||||
val = self.propertyValues.get(pn, po.default)
|
||||
if po.export and (po.export == 'always' or val != po.default):
|
||||
try:
|
||||
val = po.datatype.export_value(val)
|
||||
except AttributeError:
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
res[po.extname] = val
|
||||
return res
|
||||
|
||||
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)
|
||||
|
@ -42,8 +42,8 @@ import threading
|
||||
from collections import OrderedDict
|
||||
from time import time as currenttime
|
||||
|
||||
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError, fmt_error
|
||||
from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
|
||||
from secop.params import Parameter
|
||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||
@ -54,9 +54,9 @@ def make_update(modulename, pobj):
|
||||
if pobj.readerror:
|
||||
return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||
# 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),
|
||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||
|
||||
|
||||
class Dispatcher:
|
||||
@ -109,7 +109,7 @@ class Dispatcher:
|
||||
self._subscriptions.setdefault(eventname, set()).add(conn)
|
||||
|
||||
def unsubscribe(self, conn, eventname):
|
||||
if not ':' in eventname:
|
||||
if ':' not in eventname:
|
||||
# also remove 'more specific' subscriptions
|
||||
for k, v in self._subscriptions.items():
|
||||
if k.startswith('%s:' % eventname):
|
||||
@ -177,7 +177,7 @@ class Dispatcher:
|
||||
result = {'modules': OrderedDict()}
|
||||
for modulename in self._export:
|
||||
module = self.get_module(modulename)
|
||||
if not module.properties.get('export', False):
|
||||
if not module.export:
|
||||
continue
|
||||
# some of these need rework !
|
||||
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
||||
@ -186,7 +186,7 @@ class Dispatcher:
|
||||
result['modules'][modulename] = mod_desc
|
||||
result['equipment_id'] = self.equipment_id
|
||||
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
||||
result['version'] = '2019.08'
|
||||
result['version'] = '2021.02'
|
||||
result.update(self.nodeprops)
|
||||
return result
|
||||
|
||||
@ -195,40 +195,24 @@ class Dispatcher:
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
cmdname = moduleobj.commands.exported.get(exportedname, None)
|
||||
if cmdname is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname))
|
||||
cmdspec = moduleobj.commands[cmdname]
|
||||
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)
|
||||
cname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
cobj = moduleobj.commands.get(cname)
|
||||
if cobj is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||
|
||||
# now call func
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
func = getattr(moduleobj, 'do_' + cmdname)
|
||||
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())
|
||||
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
||||
|
||||
def _setParameterValue(self, modulename, exportedname, value):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
pobj = moduleobj.parameters.get(pname)
|
||||
if pobj is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||
if pobj.constant is not None:
|
||||
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
||||
% (modulename, pname))
|
||||
@ -252,10 +236,10 @@ class Dispatcher:
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
pobj = moduleobj.parameters.get(pname)
|
||||
if pobj is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||
if pobj.constant is not None:
|
||||
# really needed? we could just construct a readreply instead....
|
||||
# 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)))
|
||||
|
||||
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)
|
||||
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
||||
|
||||
def handle_ping(self, conn, specifier, data):
|
||||
if 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):
|
||||
if data:
|
||||
|
@ -25,6 +25,7 @@ import json
|
||||
|
||||
EOL = b'\n'
|
||||
|
||||
|
||||
def encode_msg_frame(action, specifier=None, data=None):
|
||||
""" encode a msg_triple into an msg_frame, ready to be sent
|
||||
|
||||
|
@ -21,13 +21,12 @@
|
||||
# *****************************************************************************
|
||||
"""provides tcp interface to the SECoP Server"""
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from secop.datatypes import StringType, BoolType
|
||||
from secop.datatypes import BoolType, StringType
|
||||
from secop.errors import SECoPError
|
||||
from secop.lib import formatException, \
|
||||
formatExtendedStack, formatExtendedTraceback
|
||||
@ -36,7 +35,6 @@ from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
|
||||
from secop.protocol.messages import ERRORPREFIX, \
|
||||
HELPREPLY, HELPREQUEST, HelpMessage
|
||||
|
||||
|
||||
DEF_PORT = 10767
|
||||
MESSAGE_READ_SIZE = 1024
|
||||
HELP = HELPREQUEST.encode()
|
||||
@ -134,7 +132,6 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
if result[0].startswith(ERRORPREFIX) and not detailed_errors:
|
||||
# strip extra information
|
||||
result[2][2].clear()
|
||||
result[2][2]['t'] = time.time()
|
||||
self.send_reply(result)
|
||||
|
||||
def send_reply(self, data):
|
||||
|
@ -80,7 +80,6 @@ REQUEST2REPLY = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
HelpMessage = """Try one of the following:
|
||||
'%s' to query protocol version
|
||||
'%s' to read the description
|
||||
|
@ -34,12 +34,12 @@ simplifications:
|
||||
|
||||
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.errors
|
||||
import secop.protocol.dispatcher
|
||||
from secop.lib.multievent import MultiEvent
|
||||
from secop.protocol.messages import COMMANDREQUEST, DESCRIPTIONREPLY, \
|
||||
ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, READREQUEST, WRITEREQUEST
|
||||
|
||||
|
||||
class SecopClient(secop.client.SecopClient):
|
||||
|
@ -21,21 +21,19 @@
|
||||
# *****************************************************************************
|
||||
"""SECoP proxy modules"""
|
||||
|
||||
from secop.params import Parameter, Command
|
||||
from secop.modules import Module, Writable, Readable, Drivable
|
||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||
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.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):
|
||||
properties = {
|
||||
'module':
|
||||
Property('remote module name', datatype=StringType(), default=''),
|
||||
}
|
||||
module = Property('remote module name', datatype=StringType(), default='')
|
||||
|
||||
pollerClass = None
|
||||
_consistency_check_done = False
|
||||
@ -55,7 +53,7 @@ class ProxyModule(HasIodev, Module):
|
||||
|
||||
def initModule(self):
|
||||
if not self.module:
|
||||
self.properties['module'] = self.name
|
||||
self.module = self.name
|
||||
self._secnode = self._iodev.secnode
|
||||
self._secnode.register_callback(self.module, self.updateEvent,
|
||||
self.descriptiveDataChange, self.nodeStateChange)
|
||||
@ -90,10 +88,10 @@ class ProxyModule(HasIodev, Module):
|
||||
dt.compatible(pobj.datatype)
|
||||
except Exception:
|
||||
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:
|
||||
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:
|
||||
cname, cobj = cmds.popitem()
|
||||
props = remotecmds.get(cname)
|
||||
@ -103,9 +101,9 @@ class ProxyModule(HasIodev, Module):
|
||||
dt = props['datatype']
|
||||
try:
|
||||
cobj.datatype.compatible(dt)
|
||||
except Exception:
|
||||
except BadValueError:
|
||||
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?
|
||||
# we might raise an exception, but this would lead to a reconnection,
|
||||
# which might not help.
|
||||
@ -141,14 +139,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
||||
|
||||
|
||||
class SecNode(Module):
|
||||
properties = {
|
||||
'uri':
|
||||
Property('uri of a SEC node', datatype=StringType()),
|
||||
}
|
||||
commands = {
|
||||
'request':
|
||||
Command('send a request', argument=StringType(), result=StringType())
|
||||
}
|
||||
uri = Property('uri of a SEC node', datatype=StringType())
|
||||
|
||||
def earlyInit(self):
|
||||
self.secnode = SecopClient(self.uri, self.log)
|
||||
@ -156,8 +147,9 @@ class SecNode(Module):
|
||||
def startModule(self, started_callback):
|
||||
self.secnode.spawn_connect(started_callback)
|
||||
|
||||
def do_request(self, msg):
|
||||
"""for test purposes"""
|
||||
@Command(StringType(), result=StringType())
|
||||
def request(self, msg):
|
||||
"""send a request, for debugging purposes"""
|
||||
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
||||
return encode_msg_frame(*reply).decode('utf-8')
|
||||
|
||||
@ -184,17 +176,12 @@ def proxy_class(remote_class, name=None):
|
||||
else:
|
||||
raise ConfigError('%r is no SECoP module class' % remote_class)
|
||||
|
||||
parameters = {}
|
||||
commands = {}
|
||||
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
|
||||
attrs = rcls.propertyDict.copy()
|
||||
|
||||
for aname, aobj in rcls.accessibles.items():
|
||||
if isinstance(aobj, Parameter):
|
||||
pobj = aobj.copy()
|
||||
parameters[aname] = pobj
|
||||
pobj.properties['poll'] = False
|
||||
pobj.properties['handler'] = None
|
||||
pobj.properties['needscfg'] = False
|
||||
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
||||
attrs[aname] = pobj
|
||||
|
||||
def rfunc(self, pname=aname):
|
||||
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
||||
@ -216,12 +203,11 @@ def proxy_class(remote_class, name=None):
|
||||
|
||||
elif isinstance(aobj, Command):
|
||||
cobj = aobj.copy()
|
||||
commands[aname] = cobj
|
||||
|
||||
def cfunc(self, arg=None, cname=aname):
|
||||
return self._secnode.execCommand(self.name, cname, arg)
|
||||
|
||||
attrs['do_' + aname] = cfunc
|
||||
attrs[aname] = cobj(cfunc)
|
||||
|
||||
else:
|
||||
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
||||
|
@ -23,13 +23,17 @@
|
||||
# *****************************************************************************
|
||||
"""Define helpers"""
|
||||
|
||||
import os
|
||||
from os.path import join, exists, dirname, isdir
|
||||
import ast
|
||||
import time
|
||||
import threading
|
||||
import configparser
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.errors import ConfigError
|
||||
from secop.lib import formatException, get_class, getGeneralConfig
|
||||
from secop.modules import Attached
|
||||
|
||||
try:
|
||||
from daemon import DaemonContext
|
||||
try:
|
||||
@ -39,10 +43,6 @@ try:
|
||||
except ImportError:
|
||||
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:
|
||||
import systemd.daemon
|
||||
@ -95,7 +95,11 @@ class Server:
|
||||
merged_cfg = OrderedDict()
|
||||
ambiguous_sections = set()
|
||||
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)
|
||||
merged_cfg.update(cfgdict)
|
||||
self.node_cfg = merged_cfg.pop('NODE', {})
|
||||
@ -112,22 +116,9 @@ class Server:
|
||||
if ambiguous_sections:
|
||||
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
||||
self._cfgfiles = cfgfiles
|
||||
self._pidfile = join(cfg['piddir'], name + '.pid')
|
||||
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
|
||||
|
||||
def loadCfgFile(self, cfgfile):
|
||||
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']))
|
||||
def loadCfgFile(self, filename):
|
||||
self.log.debug('Parse config file %s ...' % filename)
|
||||
result = OrderedDict()
|
||||
parser = configparser.ConfigParser()
|
||||
@ -165,8 +156,8 @@ class Server:
|
||||
def start(self):
|
||||
if not DaemonContext:
|
||||
raise ConfigError('can not daemonize, as python-daemon is not installed')
|
||||
piddir = dirname(self._pidfile)
|
||||
if not isdir(piddir):
|
||||
piddir = os.path.dirname(self._pidfile)
|
||||
if not os.path.isdir(piddir):
|
||||
os.makedirs(piddir)
|
||||
pidfile = pidlockfile.TimeoutPIDLockFile(self._pidfile)
|
||||
|
||||
@ -238,7 +229,7 @@ class Server:
|
||||
# all objs created, now start them up and interconnect
|
||||
for modname, modobj in self.modules.items():
|
||||
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:
|
||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||
@ -247,14 +238,16 @@ class Server:
|
||||
|
||||
# handle attached modules
|
||||
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):
|
||||
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
|
||||
for modname, modobj in self.modules.items():
|
||||
modobj.initModule()
|
||||
|
||||
if self._testonly:
|
||||
return
|
||||
start_events = []
|
||||
for modname, modobj in self.modules.items():
|
||||
event = threading.Event()
|
||||
@ -271,10 +264,3 @@ class Server:
|
||||
if not event.wait(timeout=max(0, deadline - time.time())):
|
||||
self.log.info('WARNING: timeout when starting %s' % name)
|
||||
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))
|
||||
|
@ -22,12 +22,15 @@
|
||||
"""Define Simulation classes"""
|
||||
|
||||
|
||||
# TODO: rework after syntax change!
|
||||
|
||||
import random
|
||||
from time import sleep
|
||||
|
||||
from secop.datatypes import FloatRange
|
||||
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:
|
||||
|
@ -23,15 +23,18 @@
|
||||
implements TCP/IP and is be used as a base for SerialIO
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
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.modules import Module, Communicator, Parameter, Command, Property, Attached, Override
|
||||
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
|
||||
from secop.errors import CommunicationFailedError, CommunicationSilentError
|
||||
from secop.modules import Attached, Command, \
|
||||
Communicator, Done, Module, Parameter, Property
|
||||
from secop.poller import REGULAR
|
||||
from secop.metaclass import Done
|
||||
|
||||
|
||||
class StringIO(Communicator):
|
||||
@ -39,48 +42,22 @@ class StringIO(Communicator):
|
||||
|
||||
self healing is assured by polling the parameter 'is_connected'
|
||||
"""
|
||||
properties = {
|
||||
'uri':
|
||||
Property('hostname:portnumber', datatype=StringType()),
|
||||
'end_of_line':
|
||||
Property('end_of_line character', datatype=ValueType(),
|
||||
default='\n', settable=True),
|
||||
'encoding':
|
||||
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
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
end_of_line = Property('end_of_line character', datatype=ValueType(),
|
||||
default='\n', settable=True)
|
||||
encoding = Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True)
|
||||
identification = Property('''
|
||||
identification
|
||||
|
||||
- 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
|
||||
'''),
|
||||
'multicomm':
|
||||
Command('''
|
||||
execute multiple commands in one go
|
||||
|
||||
assuring that no other thread calls commands in between
|
||||
''',
|
||||
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
}
|
||||
a list of tuples with commands and expected responses as regexp,
|
||||
to be sent on connect''',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
|
||||
|
||||
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)
|
||||
|
||||
_reconnectCallbacks = None
|
||||
|
||||
@ -115,11 +92,12 @@ class StringIO(Communicator):
|
||||
self._conn = AsynConn(uri, self._eol_read)
|
||||
self.is_connected = True
|
||||
for command, regexp in self.identification:
|
||||
reply = self.do_communicate(command)
|
||||
reply = self.communicate(command)
|
||||
if not re.match(regexp, reply):
|
||||
self.closeConnection()
|
||||
raise CommunicationFailedError('bad response: %s does not match %s' %
|
||||
(reply, regexp))
|
||||
|
||||
def closeConnection(self):
|
||||
"""close connection
|
||||
|
||||
@ -135,7 +113,7 @@ class StringIO(Communicator):
|
||||
self.is_connected is changed only by self.connectStart or self.closeConnection
|
||||
"""
|
||||
if self.is_connected:
|
||||
return Done # no need for intermediate updates
|
||||
return Done # no need for intermediate updates
|
||||
try:
|
||||
self.connectStart()
|
||||
if self._last_error:
|
||||
@ -180,9 +158,17 @@ class StringIO(Communicator):
|
||||
if removeme:
|
||||
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:
|
||||
self.read_is_connected() # try to reconnect
|
||||
if not self._conn:
|
||||
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||
try:
|
||||
with self._lock:
|
||||
# read garbage and wait before send
|
||||
@ -214,11 +200,13 @@ class StringIO(Communicator):
|
||||
self.log.error(self._last_error)
|
||||
raise
|
||||
|
||||
def do_multicomm(self, commands):
|
||||
@Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
def multicomm(self, commands):
|
||||
"""communicate multiple request/replies in one row"""
|
||||
replies = []
|
||||
with self._lock:
|
||||
for cmd in commands:
|
||||
replies.append(self.do_communicate(cmd))
|
||||
replies.append(self.communicate(cmd))
|
||||
return replies
|
||||
|
||||
|
||||
@ -227,16 +215,15 @@ class HasIodev(Module):
|
||||
|
||||
not only StringIO !
|
||||
"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
'uri': Property('uri for automatic creation of the attached communication module', StringType(), default=''),
|
||||
}
|
||||
iodev = Attached()
|
||||
uri = Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default='')
|
||||
|
||||
iodevDict = {}
|
||||
|
||||
def __init__(self, name, logger, opts, srv):
|
||||
iodev = opts.get('iodev')
|
||||
super().__init__(name, logger, opts, srv)
|
||||
Module.__init__(self, name, logger, opts, srv)
|
||||
if self.uri:
|
||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||
'export': False}
|
||||
@ -246,7 +233,9 @@ class HasIodev(Module):
|
||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
||||
srv.modules[ioname] = iodev
|
||||
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):
|
||||
try:
|
||||
@ -257,4 +246,4 @@ class HasIodev(Module):
|
||||
super().initModule()
|
||||
|
||||
def sendRecv(self, command):
|
||||
return self._iodev.do_communicate(command)
|
||||
return self._iodev.communicate(command)
|
||||
|
@ -47,7 +47,7 @@ def get_git_version(abbrev=4, cwd=None):
|
||||
# mangle version to comply with pep440
|
||||
if version.count('-'):
|
||||
version, patchcount, githash = version.split('-')
|
||||
version += '.post%s+%s' %(patchcount, githash)
|
||||
version += '.post%s+%s' % (patchcount, githash)
|
||||
return version
|
||||
except Exception:
|
||||
return None
|
||||
|
@ -25,20 +25,17 @@ import random
|
||||
import time
|
||||
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.modules import Drivable, Override, Parameter
|
||||
|
||||
from secop.modules import Command, Drivable, Parameter
|
||||
# test custom property (value.test can be changed in config file)
|
||||
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):
|
||||
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):
|
||||
@ -49,93 +46,88 @@ class Cryostat(CryoBase):
|
||||
- thermal transfer between regulation and samplen
|
||||
"""
|
||||
|
||||
parameters = dict(
|
||||
jitter=Parameter("amount of random noise on readout values",
|
||||
datatype=FloatRange(0, 1), unit="K",
|
||||
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,
|
||||
jitter = Parameter("amount of random noise on readout values",
|
||||
datatype=FloatRange(0, 1), unit="K",
|
||||
default=0.1, readonly=False, export=False,
|
||||
),
|
||||
ramp=Parameter("ramping speed of the setpoint",
|
||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||
readonly=False,
|
||||
),
|
||||
setpoint=Parameter("current setpoint during ramping else target",
|
||||
datatype=FloatRange(), default=1, unit='K',
|
||||
),
|
||||
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=Override("target temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
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",
|
||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||
readonly=False,
|
||||
),
|
||||
value=Override("regulation temperature",
|
||||
datatype=FloatRange(0), default=0, unit="K",
|
||||
test='TEST',
|
||||
setpoint = Parameter("current setpoint during ramping else target",
|
||||
datatype=FloatRange(), default=1, unit='K',
|
||||
),
|
||||
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",
|
||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||
FloatRange(0, 100)),
|
||||
default=(40, 10, 2), readonly=False,
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter("regulation coefficient 'p'",
|
||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
p=Parameter("regulation coefficient 'p'",
|
||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
i=Parameter("regulation coefficient 'i'",
|
||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
d=Parameter("regulation coefficient 'd'",
|
||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
mode=Parameter("mode of regulation",
|
||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||
default='ramp',
|
||||
readonly=False,
|
||||
),
|
||||
pollinterval=Override("polling interval",
|
||||
datatype=FloatRange(0), default=5,
|
||||
),
|
||||
tolerance=Parameter("temperature range for stability checking",
|
||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||
i = Parameter("regulation coefficient 'i'",
|
||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
d = Parameter("regulation coefficient 'd'",
|
||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||
group='pid',
|
||||
),
|
||||
mode = Parameter("mode of regulation",
|
||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||
default='ramp',
|
||||
readonly=False,
|
||||
),
|
||||
pollinterval = Parameter("polling interval",
|
||||
datatype=FloatRange(0), default=5,
|
||||
),
|
||||
tolerance = Parameter("temperature range for stability checking",
|
||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||
readonly=False,
|
||||
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',
|
||||
),
|
||||
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):
|
||||
self._stopflag = False
|
||||
@ -180,11 +172,11 @@ class Cryostat(CryoBase):
|
||||
def read_pid(self):
|
||||
return (self.p, self.i, self.d)
|
||||
|
||||
def do_stop(self):
|
||||
""""stop the ramp
|
||||
@Command()
|
||||
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 ???
|
||||
self.write_target(self.setpoint)
|
||||
|
||||
|
@ -28,42 +28,41 @@ import time
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, IntRange, StringType, StructOf, TupleOf
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Switch(Drivable):
|
||||
"""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,
|
||||
),
|
||||
'target': Override('wanted state (on or off)',
|
||||
datatype=EnumType(on=1, off=0), default=0,
|
||||
readonly=False,
|
||||
),
|
||||
'switch_on_time': Parameter('seconds to wait after activating the switch',
|
||||
readonly=False,
|
||||
)
|
||||
switch_on_time = Parameter('seconds to wait after activating the switch',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
default=10, export=False,
|
||||
)
|
||||
switch_off_time = Parameter('cool-down time in seconds',
|
||||
datatype=FloatRange(0, 60), unit='s',
|
||||
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(),
|
||||
default='no description', mandatory=False, extname='description'),
|
||||
}
|
||||
description = Property('The description of the Module', StringType(),
|
||||
default='no description', mandatory=False, extname='description')
|
||||
|
||||
def read_value(self):
|
||||
# could ask HW
|
||||
@ -109,30 +108,29 @@ class Switch(Drivable):
|
||||
class MagneticField(Drivable):
|
||||
"""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,
|
||||
),
|
||||
'target': Override('target field in T',
|
||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||
readonly=False,
|
||||
),
|
||||
'ramp': Parameter('ramping speed',
|
||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||
readonly=False,
|
||||
),
|
||||
'mode': Parameter('what to do after changing field',
|
||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||
readonly=False,
|
||||
),
|
||||
'heatswitch': Parameter('name of heat switch device',
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
}
|
||||
readonly=False,
|
||||
)
|
||||
ramp = Parameter('ramping speed',
|
||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||
readonly=False,
|
||||
)
|
||||
mode = Parameter('what to do after changing field',
|
||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||
readonly=False,
|
||||
)
|
||||
heatswitch = Parameter('name of heat switch device',
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
|
||||
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):
|
||||
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()))
|
||||
self.log.error(self, 'main thread exited unexpectedly!')
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
self.write_target(self.read_value())
|
||||
|
||||
|
||||
class CoilTemp(Readable):
|
||||
"""a coil temperature
|
||||
"""
|
||||
parameters = {
|
||||
'value': Override('Coil temperatur',
|
||||
unit='K', datatype=FloatRange(), default=0,
|
||||
),
|
||||
'sensor': Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
),
|
||||
}
|
||||
|
||||
value = Parameter('Coil temperatur',
|
||||
unit='K', datatype=FloatRange(), default=0,
|
||||
)
|
||||
sensor = Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(2.3 + random.random(), 3)
|
||||
@ -225,18 +222,17 @@ class CoilTemp(Readable):
|
||||
class SampleTemp(Drivable):
|
||||
"""a sample temperature
|
||||
"""
|
||||
parameters = {
|
||||
'value': Override('Sample temperature',
|
||||
unit='K', datatype=FloatRange(), default=10,
|
||||
),
|
||||
'sensor': Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
),
|
||||
'ramp': Parameter('moving speed in K/min',
|
||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||
readonly=False,
|
||||
),
|
||||
}
|
||||
|
||||
value = Parameter('Sample temperature',
|
||||
unit='K', datatype=FloatRange(), default=10,
|
||||
)
|
||||
sensor = Parameter("Sensor number or calibration id",
|
||||
datatype=StringType(), readonly=True,
|
||||
)
|
||||
ramp = Parameter('moving speed in K/min',
|
||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
def initModule(self):
|
||||
_thread = threading.Thread(target=self._thread)
|
||||
@ -272,20 +268,19 @@ class Label(Readable):
|
||||
of several subdevices. used for demoing connections between
|
||||
modules.
|
||||
"""
|
||||
parameters = {
|
||||
'system': Parameter("Name of the magnet system",
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
'subdev_mf': Parameter("name of subdevice for magnet status",
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
'subdev_ts': Parameter("name of subdevice for sample temp",
|
||||
datatype=StringType(), export=False,
|
||||
),
|
||||
'value': Override("final value of label string", default='',
|
||||
datatype=StringType(),
|
||||
),
|
||||
}
|
||||
|
||||
system = Parameter("Name of the magnet system",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
subdev_mf = Parameter("name of subdevice for magnet status",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
subdev_ts = Parameter("name of subdevice for sample temp",
|
||||
datatype=StringType(), export=False,
|
||||
)
|
||||
value = Parameter("final value of label string", default='',
|
||||
datatype=StringType(),
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
strings = [self.system]
|
||||
@ -317,29 +312,25 @@ class Label(Readable):
|
||||
class DatatypesTest(Readable):
|
||||
"""for demoing all datatypes
|
||||
"""
|
||||
parameters = {
|
||||
'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||
readonly=False, default=1),
|
||||
'tupleof': Parameter('tuple of int, float and str',
|
||||
datatype=TupleOf(IntRange(), FloatRange(),
|
||||
StringType()),
|
||||
readonly=False, default=(1, 2.3, 'a')),
|
||||
'arrayof': Parameter('array: 2..3 times bool',
|
||||
datatype=ArrayOf(BoolType(), 2, 3),
|
||||
readonly=False, default=[1, 0, 1]),
|
||||
'intrange': Parameter('intrange', datatype=IntRange(2, 9),
|
||||
readonly=False, default=4),
|
||||
'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||
readonly=False, default=0, ),
|
||||
'struct': Parameter('struct(a=str, b=int, c=bool)',
|
||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||
c=BoolType()),
|
||||
),
|
||||
}
|
||||
|
||||
enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||
readonly=False, default=1)
|
||||
tupleof = Parameter('tuple of int, float and str',
|
||||
datatype=TupleOf(IntRange(), FloatRange(),
|
||||
StringType()),
|
||||
readonly=False, default=(1, 2.3, 'a'))
|
||||
arrayof = Parameter('array: 2..3 times bool',
|
||||
datatype=ArrayOf(BoolType(), 2, 3),
|
||||
readonly=False, default=[1, 0, 1])
|
||||
intrange = Parameter('intrange', datatype=IntRange(2, 9),
|
||||
readonly=False, default=4)
|
||||
floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||
readonly=False, default=0)
|
||||
struct = Parameter('struct(a=str, b=int, c=bool)',
|
||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||
c=BoolType()))
|
||||
|
||||
|
||||
class ArrayTest(Readable):
|
||||
parameters = {
|
||||
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||
default = 100000 * [0]),
|
||||
}
|
||||
x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||
default=100000 * [0])
|
||||
|
@ -24,7 +24,7 @@
|
||||
import random
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -45,11 +45,10 @@ class Heater(Drivable):
|
||||
class name indicates it to be some heating element,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
parameters = {
|
||||
'maxheaterpower': Parameter('maximum allowed heater power',
|
||||
datatype=FloatRange(0, 100), unit='W',
|
||||
),
|
||||
}
|
||||
|
||||
maxheaterpower = Parameter('maximum allowed heater power',
|
||||
datatype=FloatRange(0, 100), unit='W',
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
@ -64,22 +63,21 @@ class Temp(Drivable):
|
||||
class name indicates it to be some temperature controller,
|
||||
but the implementation may do anything
|
||||
"""
|
||||
parameters = {
|
||||
'sensor': Parameter(
|
||||
"Sensor number or calibration id",
|
||||
datatype=StringType(
|
||||
8,
|
||||
16),
|
||||
readonly=True,
|
||||
),
|
||||
'target': Override(
|
||||
"Target temperature",
|
||||
default=300.0,
|
||||
datatype=FloatRange(0),
|
||||
readonly=False,
|
||||
unit='K',
|
||||
),
|
||||
}
|
||||
|
||||
sensor = Parameter(
|
||||
"Sensor number or calibration id",
|
||||
datatype=StringType(
|
||||
8,
|
||||
16),
|
||||
readonly=True,
|
||||
)
|
||||
target = Parameter(
|
||||
"Target temperature",
|
||||
default=300.0,
|
||||
datatype=FloatRange(0),
|
||||
readonly=False,
|
||||
unit='K',
|
||||
)
|
||||
|
||||
def read_value(self):
|
||||
return round(100 * random.random(), 1)
|
||||
@ -90,8 +88,8 @@ class Temp(Drivable):
|
||||
|
||||
class Lower(Communicator):
|
||||
"""Communicator returning a lowercase version of the request"""
|
||||
command = {
|
||||
'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'),
|
||||
}
|
||||
def do_communicate(self, request):
|
||||
return str(request).lower()
|
||||
|
||||
@Command(argument=StringType(), result=StringType(), export='communicate')
|
||||
def communicate(self, command):
|
||||
"""lowercase a string"""
|
||||
return str(command).lower()
|
||||
|
@ -58,20 +58,20 @@ except ImportError:
|
||||
class EpicsReadable(Readable):
|
||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||
# Commmon parameter for all EPICS devices
|
||||
parameters = {
|
||||
'value': Parameter('EPICS generic value',
|
||||
datatype=FloatRange(),
|
||||
default=300.0,),
|
||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
||||
datatype=EnumType(v3=3, v4=4),),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'value_pv': Parameter('EPICS pv_name of value',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
'status_pv': Parameter('EPICS pv_name of status',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
value = Parameter('EPICS generic value',
|
||||
datatype=FloatRange(),
|
||||
default=300.0,)
|
||||
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||
datatype=EnumType(v3=3, v4=4),)
|
||||
value_pv = Parameter('EPICS pv_name of value',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
status_pv = Parameter('EPICS pv_name of status',
|
||||
datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
|
||||
|
||||
# Generic read and write functions
|
||||
def _read_pv(self, pv_name):
|
||||
@ -118,21 +118,21 @@ class EpicsReadable(Readable):
|
||||
class EpicsDrivable(Drivable):
|
||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||
# Commmon parameter for all EPICS devices
|
||||
parameters = {
|
||||
'target': Parameter('EPICS generic target', datatype=FloatRange(),
|
||||
default=300.0, readonly=False),
|
||||
'value': Parameter('EPICS generic value', datatype=FloatRange(),
|
||||
default=300.0,),
|
||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
||||
datatype=StringType(),),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(),
|
||||
default="unset", export=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
target = Parameter('EPICS generic target', datatype=FloatRange(),
|
||||
default=300.0, readonly=False)
|
||||
value = Parameter('EPICS generic value', datatype=FloatRange(),
|
||||
default=300.0,)
|
||||
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||
datatype=StringType(),)
|
||||
target_pv = Parameter('EPICS pv_name of target', datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
value_pv = Parameter('EPICS pv_name of value', datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
status_pv = Parameter('EPICS pv_name of status', datatype=StringType(),
|
||||
default="unset", export=False)
|
||||
|
||||
|
||||
# Generic read and write functions
|
||||
def _read_pv(self, pv_name):
|
||||
@ -191,17 +191,16 @@ class EpicsDrivable(Drivable):
|
||||
|
||||
class EpicsTempCtrl(EpicsDrivable):
|
||||
|
||||
parameters = {
|
||||
# TODO: restrict possible values with oneof datatype
|
||||
'heaterrange': Parameter('Heater range', datatype=StringType(),
|
||||
default='Off', readonly=False,),
|
||||
'tolerance': Parameter('allowed deviation between value and target',
|
||||
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
||||
readonly=False,),
|
||||
# 'private' parameters: not remotely accessible
|
||||
'heaterrange_pv': Parameter('EPICS pv_name of heater range',
|
||||
datatype=StringType(), default="unset", export=False,),
|
||||
}
|
||||
|
||||
# parameters
|
||||
heaterrange = Parameter('Heater range', datatype=StringType(),
|
||||
default='Off', readonly=False,)
|
||||
tolerance = Parameter('allowed deviation between value and target',
|
||||
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
||||
readonly=False,)
|
||||
heaterrange_pv = Parameter('EPICS pv_name of heater range',
|
||||
datatype=StringType(), default="unset", export=False,)
|
||||
|
||||
|
||||
def read_target(self):
|
||||
return self._read_pv(self.target_pv)
|
||||
|
@ -31,7 +31,7 @@ import math
|
||||
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
||||
from secop.errors import ConfigError, DisabledError
|
||||
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):
|
||||
@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
parameters = {
|
||||
'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False),
|
||||
'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False),
|
||||
'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False),
|
||||
'subdev_symmetry': Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False),
|
||||
'userlimits': Parameter('User defined limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10),
|
||||
'abslimits': Parameter('Absolute limits of device value',
|
||||
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)',
|
||||
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),
|
||||
'calibration': Parameter('Coefficients for calibration '
|
||||
'function: [c0, c1, c2, c3, c4] calculates '
|
||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||
' in T', poll=1,
|
||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||
default=(1.0, 0.0, 0.0, 0.0, 0.0)),
|
||||
'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
||||
short=ArrayOf(
|
||||
FloatRange(), 5, 5),
|
||||
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False)
|
||||
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
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)',
|
||||
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)
|
||||
calibration = Parameter('Coefficients for calibration '
|
||||
'function: [c0, c1, c2, c3, c4] calculates '
|
||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||
' in T', poll=1,
|
||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
||||
short=ArrayOf(
|
||||
FloatRange(), 5, 5),
|
||||
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False)
|
||||
|
||||
|
||||
def _current2field(self, current, *coefficients):
|
||||
"""Return field in T for given current in A.
|
||||
@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
||||
return self._currentsource.read_status()[0] == 'BUSY'
|
||||
if self._currentsource.status[0] != 'BUSY':
|
||||
if self._enable.status[0] == 'ERROR':
|
||||
self._enable.do_reset()
|
||||
self._enable.reset()
|
||||
self._enable.read_status()
|
||||
self._enable.write_target('On')
|
||||
self._enable._hw_wait()
|
||||
|
@ -30,18 +30,17 @@ MLZ TANGO interface for the respective device classes.
|
||||
|
||||
import re
|
||||
import threading
|
||||
from time import time as currenttime
|
||||
from time import sleep
|
||||
from time import time as currenttime
|
||||
|
||||
import PyTango
|
||||
|
||||
from secop.datatypes import ArrayOf, EnumType, \
|
||||
FloatRange, IntRange, StringType, TupleOf, LimitsType
|
||||
from secop.datatypes import ArrayOf, EnumType, FloatRange, \
|
||||
IntRange, LimitsType, StringType, TupleOf
|
||||
from secop.errors import CommunicationFailedError, \
|
||||
ConfigError, HardwareError, ProgrammingError
|
||||
from secop.lib import lazy_property
|
||||
from secop.modules import Command, Drivable, \
|
||||
Module, Override, Parameter, Readable, BasicPoller
|
||||
from secop.modules import BasicPoller, Command, \
|
||||
Drivable, Module, Parameter, Readable
|
||||
|
||||
#####
|
||||
|
||||
@ -160,24 +159,18 @@ class PyTangoDevice(Module):
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
parameters = {
|
||||
'comtries': Parameter('Maximum retries for communication',
|
||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||
group='communication'),
|
||||
'comdelay': Parameter('Delay between retries', datatype=FloatRange(0),
|
||||
unit='s', default=0.1, readonly=False,
|
||||
group='communication'),
|
||||
|
||||
'tangodevice': Parameter('Tango device name',
|
||||
datatype=StringType(), readonly=True,
|
||||
# export=True, # for testing only
|
||||
export=False,
|
||||
),
|
||||
}
|
||||
|
||||
commands = {
|
||||
'reset': Command('Tango reset command', argument=None, result=None),
|
||||
}
|
||||
# parameters
|
||||
comtries = Parameter('Maximum retries for communication',
|
||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||
group='communication')
|
||||
comdelay = Parameter('Delay between retries', datatype=FloatRange(0),
|
||||
unit='s', default=0.1, readonly=False,
|
||||
group='communication')
|
||||
tangodevice = Parameter('Tango device name',
|
||||
datatype=StringType(), readonly=True,
|
||||
# export=True, # for testing only
|
||||
export=False,
|
||||
)
|
||||
|
||||
tango_status_mapping = {
|
||||
PyTango.DevState.ON: Drivable.Status.IDLE,
|
||||
@ -372,7 +365,9 @@ class PyTangoDevice(Module):
|
||||
|
||||
return (myState, tangoStatus)
|
||||
|
||||
def do_reset(self):
|
||||
@Command(argument=None, result=None)
|
||||
def reset(self):
|
||||
"""Tango reset command"""
|
||||
self._dev.Reset()
|
||||
|
||||
|
||||
@ -405,13 +400,9 @@ class Sensor(AnalogInput):
|
||||
# note: we don't transport the formula to secop....
|
||||
# we support the adjust method
|
||||
|
||||
commands = {
|
||||
'setposition': Command('Set the position to the given value.',
|
||||
argument=FloatRange(), result=None,
|
||||
),
|
||||
}
|
||||
|
||||
def do_setposition(self, value):
|
||||
@Command(argument=FloatRange(), result=None)
|
||||
def setposition(self, value):
|
||||
"""Set the position to the given value."""
|
||||
self._dev.Adjust(value)
|
||||
|
||||
|
||||
@ -427,29 +418,29 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
controllers, ...
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'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',
|
||||
# parameters
|
||||
userlimits = Parameter('User defined limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
),
|
||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
datatype=FloatRange(1e-38, unit='$'),
|
||||
readonly=False, group='stability',
|
||||
),
|
||||
'window': Parameter('Time window for checking stabilization if > 0',
|
||||
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',
|
||||
),
|
||||
}
|
||||
default=(float('-Inf'), float('+Inf')),
|
||||
readonly=False, poll=10,
|
||||
)
|
||||
abslimits = Parameter('Absolute limits of device value',
|
||||
datatype=LimitsType(FloatRange(unit='$')),
|
||||
)
|
||||
precision = Parameter('Precision of the device value (allowed deviation '
|
||||
'of stable values from target)',
|
||||
datatype=FloatRange(1e-38, unit='$'),
|
||||
readonly=False, group='stability',
|
||||
)
|
||||
window = Parameter('Time window for checking stabilization if > 0',
|
||||
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 = ()
|
||||
_timeout = None
|
||||
_moving = False
|
||||
@ -566,7 +557,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
if self.status[0] == self.Status.BUSY:
|
||||
# changing target value during movement is not allowed by the
|
||||
# Tango base class state machine. If we are moving, stop first.
|
||||
self.do_stop()
|
||||
self.stop()
|
||||
self._hw_wait()
|
||||
self._dev.value = value
|
||||
# set meaningful timeout
|
||||
@ -587,7 +578,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
||||
sleep(0.3)
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
self._dev.Stop()
|
||||
|
||||
|
||||
@ -601,21 +592,14 @@ class Actuator(AnalogOutput):
|
||||
"""
|
||||
# for secop: support the speed and ramp parameters
|
||||
|
||||
parameters = {
|
||||
'speed': Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
),
|
||||
'ramp': Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
poll=30,
|
||||
),
|
||||
}
|
||||
|
||||
commands = {
|
||||
'setposition': Command('Set the position to the given value.',
|
||||
argument=FloatRange(), result=None,
|
||||
),
|
||||
}
|
||||
# parameters
|
||||
speed = Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
)
|
||||
ramp = Parameter('The speed of changing the value',
|
||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||
poll=30,
|
||||
)
|
||||
|
||||
def read_speed(self):
|
||||
return self._dev.speed
|
||||
@ -630,7 +614,9 @@ class Actuator(AnalogOutput):
|
||||
self.write_speed(value / 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)
|
||||
|
||||
|
||||
@ -641,21 +627,16 @@ class Motor(Actuator):
|
||||
It has the ability to move a real object from one place to another place.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'refpos': Parameter('Reference position',
|
||||
datatype=FloatRange(unit='$'),
|
||||
),
|
||||
'accel': Parameter('Acceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
),
|
||||
'decel': Parameter('Deceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
),
|
||||
}
|
||||
|
||||
commands = {
|
||||
'reference': Command('Do a reference run', argument=None, result=None),
|
||||
}
|
||||
# parameters
|
||||
refpos = Parameter('Reference position',
|
||||
datatype=FloatRange(unit='$'),
|
||||
)
|
||||
accel = Parameter('Acceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
)
|
||||
decel = Parameter('Deceleration',
|
||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||
)
|
||||
|
||||
def read_refpos(self):
|
||||
return float(self._getProperty('refpos'))
|
||||
@ -672,7 +653,9 @@ class Motor(Actuator):
|
||||
def write_decel(self, value):
|
||||
self._dev.decel = value
|
||||
|
||||
def do_reference(self):
|
||||
@Command()
|
||||
def reference(self):
|
||||
"""Do a reference run"""
|
||||
self._dev.Reference()
|
||||
return self.read_value()
|
||||
|
||||
@ -681,32 +664,29 @@ class TemperatureController(Actuator):
|
||||
"""A temperature control loop device.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'p': Parameter('Proportional 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',
|
||||
),
|
||||
'pid': Parameter('pid control Parameters',
|
||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||
readonly=False, group='pid', poll=30,
|
||||
),
|
||||
'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||
),
|
||||
'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||
),
|
||||
}
|
||||
# parameters
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter('Proportional 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',
|
||||
)
|
||||
pid = Parameter('pid control Parameters',
|
||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||
readonly=False, group='pid', poll=30,
|
||||
)
|
||||
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||
)
|
||||
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||
)
|
||||
|
||||
overrides = {
|
||||
# We want this to be freely user-settable, and not produce a warning
|
||||
# on startup, so select a usually sensible default.
|
||||
'precision': Override(default=0.1),
|
||||
'ramp': Override(description='Temperature ramp'),
|
||||
}
|
||||
# overrides
|
||||
precision = Parameter(default=0.1)
|
||||
ramp = Parameter(description='Temperature ramp')
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
@ -755,15 +735,14 @@ class PowerSupply(Actuator):
|
||||
"""A power supply (voltage and current) device.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'voltage': Parameter('Actual voltage',
|
||||
datatype=FloatRange(unit='V'), poll=-5),
|
||||
'current': Parameter('Actual current',
|
||||
datatype=FloatRange(unit='A'), poll=-5),
|
||||
}
|
||||
overrides = {
|
||||
'ramp': Override(description='Current/voltage ramp'),
|
||||
}
|
||||
# parameters
|
||||
voltage = Parameter('Actual voltage',
|
||||
datatype=FloatRange(unit='V'), poll=-5)
|
||||
current = Parameter('Actual current',
|
||||
datatype=FloatRange(unit='A'), poll=-5)
|
||||
|
||||
# overrides
|
||||
ramp = Parameter(description='Current/voltage ramp')
|
||||
|
||||
def read_ramp(self):
|
||||
return self._dev.ramp
|
||||
@ -782,9 +761,8 @@ class DigitalInput(PyTangoDevice, Readable):
|
||||
"""A device reading a bitfield.
|
||||
"""
|
||||
|
||||
overrides = {
|
||||
'value': Override(datatype=IntRange()),
|
||||
}
|
||||
# overrides
|
||||
value = Parameter(datatype=IntRange())
|
||||
|
||||
def read_value(self):
|
||||
return self._dev.value
|
||||
@ -794,10 +772,9 @@ class NamedDigitalInput(DigitalInput):
|
||||
"""A DigitalInput with numeric values mapped to names.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False), # XXX:!!!
|
||||
}
|
||||
# parameters
|
||||
mapping = Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False) # XXX:!!!
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalInput, self).initModule()
|
||||
@ -821,12 +798,11 @@ class PartialDigitalInput(NamedDigitalInput):
|
||||
bit width accessed.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'startbit': Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0),
|
||||
'bitwidth': Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1),
|
||||
}
|
||||
# parameters
|
||||
startbit = Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0)
|
||||
bitwidth = Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalInput, self).initModule()
|
||||
@ -844,10 +820,9 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
||||
bitfield.
|
||||
"""
|
||||
|
||||
overrides = {
|
||||
'value': Override(datatype=IntRange()),
|
||||
'target': Override(datatype=IntRange()),
|
||||
}
|
||||
# overrides
|
||||
value = Parameter(datatype=IntRange())
|
||||
target = Parameter(datatype=IntRange())
|
||||
|
||||
def read_value(self):
|
||||
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.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False),
|
||||
}
|
||||
# parameters
|
||||
mapping = Parameter('A dictionary mapping state names to integers',
|
||||
datatype=StringType(), export=False)
|
||||
|
||||
def initModule(self):
|
||||
super(NamedDigitalOutput, self).initModule()
|
||||
@ -894,12 +868,11 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
||||
bit width accessed.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'startbit': Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0),
|
||||
'bitwidth': Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1),
|
||||
}
|
||||
# parameters
|
||||
startbit = Parameter('Number of the first bit',
|
||||
datatype=IntRange(0), default=0)
|
||||
bitwidth = Parameter('Number of bits',
|
||||
datatype=IntRange(0), default=1)
|
||||
|
||||
def initModule(self):
|
||||
super(PartialDigitalOutput, self).initModule()
|
||||
@ -925,17 +898,16 @@ class StringIO(PyTangoDevice, Module):
|
||||
receives strings.
|
||||
"""
|
||||
|
||||
parameters = {
|
||||
'bustimeout': Parameter('Communication timeout',
|
||||
datatype=FloatRange(unit='s'), readonly=False,
|
||||
group='communication'),
|
||||
'endofline': Parameter('End of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication'),
|
||||
'startofline': Parameter('Start of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication'),
|
||||
}
|
||||
# parameters
|
||||
bustimeout = Parameter('Communication timeout',
|
||||
datatype=FloatRange(unit='s'), readonly=False,
|
||||
group='communication')
|
||||
endofline = Parameter('End of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication')
|
||||
startofline = Parameter('Start of line',
|
||||
datatype=StringType(), readonly=False,
|
||||
group='communication')
|
||||
|
||||
def read_bustimeout(self):
|
||||
return self._dev.communicationTimeout
|
||||
@ -955,53 +927,48 @@ class StringIO(PyTangoDevice, Module):
|
||||
def write_startofline(self, value):
|
||||
self._dev.startOfLine = value
|
||||
|
||||
commands = {
|
||||
'communicate': Command('Send a string and return the reply',
|
||||
argument=StringType(),
|
||||
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()):
|
||||
@Command(argument=StringType(), result=StringType())
|
||||
def communicate(self, value=StringType()):
|
||||
"""Send a string and return the reply"""
|
||||
return self._dev.Communicate(value)
|
||||
|
||||
def do_flush(self):
|
||||
@Command(argument=None, result=None)
|
||||
def flush(self):
|
||||
"""Flush output buffer"""
|
||||
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)
|
||||
|
||||
def do_write(self, value):
|
||||
@Command(argument=StringType(), result=None)
|
||||
def write(self, value):
|
||||
"""write some chars to output"""
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def do_availableChars(self):
|
||||
@Command(argument=None, result=IntRange(0))
|
||||
def availableChars(self):
|
||||
"""return number of chars in input buffer"""
|
||||
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
|
||||
|
@ -20,65 +20,53 @@
|
||||
# *****************************************************************************
|
||||
"""WAVE FUNCTION LECROY XX: SIGNAL GENERATOR"""
|
||||
|
||||
from secop.core import Readable, Parameter, Override, Command, FloatRange, TupleOf, \
|
||||
HasIodev, StringIO, Done, Attached, IntRange, BoolType, EnumType, StringType, Module, \
|
||||
Property
|
||||
from secop.core import Readable, Parameter, FloatRange, \
|
||||
HasIodev, IntRange, BoolType, EnumType, Module, Property
|
||||
|
||||
|
||||
class Channel(Module):
|
||||
properties = {
|
||||
'channel':Property('choose channel to manipulate',IntRange(1,2)),
|
||||
}
|
||||
parameters = {
|
||||
'freq':
|
||||
Parameter('frequency', FloatRange(1e-6,20e6,unit='Hz'),
|
||||
poll=True, initwrite=True, default=1000),
|
||||
'amp':
|
||||
Parameter('exc_volt_int', FloatRange(0.00,5,unit='Vrms'),
|
||||
poll=True, readonly=False, initwrite=True, default=0.1),
|
||||
'offset':
|
||||
Parameter('offset_volt_int', FloatRange(0.00,10,unit='V'),
|
||||
poll = True, readonly = False, initwrite = True, default = 0.0),
|
||||
'wave':
|
||||
Parameter ('type of wavefunction',
|
||||
EnumType('WaveFunction', SINE=1, SQUARE=2, RAMP=3, PULSE=4, NOISE=5, ARB=6, DC=7),
|
||||
poll=True, readonly=False, default='SINE'),
|
||||
'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),
|
||||
}
|
||||
class Channel(HasIodev, Module):
|
||||
channel = Property('choose channel to manipulate', IntRange(1, 2))
|
||||
|
||||
freq = Parameter('frequency', FloatRange(1e-6, 20e6, unit='Hz'),
|
||||
poll=True, initwrite=True, default=1000)
|
||||
amp = Parameter('exc_volt_int', FloatRange(0.00, 5, unit='Vrms'),
|
||||
poll=True, readonly=False, initwrite=True, default=0.1)
|
||||
offset = Parameter('offset_volt_int', FloatRange(0.00, 10, unit='V'),
|
||||
poll=True, readonly=False, initwrite=True, default=0.0)
|
||||
wave = Parameter('type of wavefunction',
|
||||
EnumType('WaveFunction', SINE=1, SQUARE=2, RAMP=3, PULSE=4, NOISE=5, ARB=6, DC=7),
|
||||
poll=True, readonly=False, default='SINE'),
|
||||
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):
|
||||
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'))
|
||||
return value
|
||||
|
||||
#signal wavefunction parameter
|
||||
|
||||
# signal wavefunction parameter
|
||||
def read_wave(self):
|
||||
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))
|
||||
return value
|
||||
|
||||
#signal amplitude parameter
|
||||
|
||||
# signal amplitude parameter
|
||||
def read_amp(self):
|
||||
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))
|
||||
return value
|
||||
|
||||
#offset value parameter
|
||||
# offset value parameter
|
||||
def read_offset(self):
|
||||
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))
|
||||
return value
|
||||
|
||||
|
||||
# channel symmetry
|
||||
# channel symmetry
|
||||
def read_symm(self):
|
||||
return self.sendRecv('C%d:BSWV SYM?' % self.channel)
|
||||
|
||||
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
|
||||
|
||||
# wave phase parameter
|
||||
# wave phase parameter
|
||||
def read_phase(self):
|
||||
return self.sendRecv('C%d:BSWV PHSE?' % self.channel)
|
||||
|
||||
def write_phase(self, value):
|
||||
self.sendRecv('C%d:BSWV PHSE %g' % (self.channel, str(value)))
|
||||
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# dis/enable output channel
|
||||
|
||||
# dis/enable output channel
|
||||
def read_enabled(self):
|
||||
return self.sendRecv('C%d: OUTP?' % self.channel)
|
||||
|
||||
|
||||
def write_enabled(self, value):
|
||||
self.sendRecv('C%d: OUTP %s' % (self.channel, value.name))
|
||||
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):
|
||||
pollerClass = None
|
||||
parameters = {
|
||||
'value': Override(datatype=FloatRange(unit='')),
|
||||
}
|
||||
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit=''))
|
||||
|
||||
|
||||
class arg2(Readable):
|
||||
pollerClass = None
|
||||
parameters = {
|
||||
'value': Override(datatype=BoolType(unit='')),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=BoolType())
|
||||
|
@ -20,262 +20,29 @@
|
||||
# *****************************************************************************
|
||||
"""SIGNAL RECOVERY SR7270: lOCKIN AMPLIFIER FOR AC SUSCEPTIBILITY"""
|
||||
|
||||
from secop.core import Readable, Parameter, Override, Command, FloatRange, TupleOf, \
|
||||
HasIodev, StringIO, Done, Attached, IntRange, BoolType, EnumType
|
||||
from secop.core import FloatRange, HasIodev, \
|
||||
Parameter, Readable, StringIO, TupleOf
|
||||
|
||||
|
||||
class SR7270(StringIO):
|
||||
end_of_line = b'\x00'
|
||||
|
||||
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
|
||||
# print('comm=',command,'reply=',reply,'status=',status)
|
||||
return reply + ';%d;%d' % tuple(status)
|
||||
|
||||
# end_of_line = '\x00' #termination line from maanual page 6.8
|
||||
end_of_line = '\n'
|
||||
|
||||
|
||||
class XY(HasIodev, Readable):
|
||||
value = Parameter('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V')))
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
reply = self.comm('XY.').split(',')
|
||||
x = float(reply[0])
|
||||
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
|
||||
reply = self.sendRecv('XY.').split('\x00')[-1]
|
||||
return reply.split(',')
|
||||
|
||||
def read_freq(self):
|
||||
reply = self.comm('OF.')
|
||||
reply = self.sendRecv('OF.').split('\x00')[-1]
|
||||
return reply
|
||||
|
||||
def write_freq(self,value):
|
||||
self.comm('OF. %g' % value)
|
||||
|
||||
self.sendRecv('OF. %g' % 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
|
||||
|
@ -20,7 +20,7 @@
|
||||
# *****************************************************************************
|
||||
"""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):
|
||||
@ -29,12 +29,12 @@ class Ah2700IO(StringIO):
|
||||
|
||||
|
||||
class Capacitance(HasIodev, Readable):
|
||||
parameters = {
|
||||
'value': Override('capacitance', FloatRange(unit='pF'), poll=True),
|
||||
'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0),
|
||||
'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0),
|
||||
'loss': Parameter('loss', FloatRange(unit='deg'), default=0),
|
||||
}
|
||||
|
||||
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
|
||||
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
||||
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
||||
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
||||
|
||||
iodevClass = Ah2700IO
|
||||
|
||||
def parse_reply(self, reply):
|
||||
|
@ -20,7 +20,7 @@
|
||||
# *****************************************************************************
|
||||
"""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):
|
||||
@ -28,12 +28,12 @@ class DG645(StringIO):
|
||||
|
||||
|
||||
class Delay(HasIodev, Module):
|
||||
parameters = {
|
||||
'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),
|
||||
'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),
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
iodevClass = DG645
|
||||
|
||||
def read_on1(self):
|
||||
|
@ -22,8 +22,8 @@
|
||||
|
||||
not tested yet"""
|
||||
|
||||
from secop.core import Writable, Module, Parameter, Override, Attached,\
|
||||
BoolType, FloatRange, EnumType, HasIodev, StringIO
|
||||
from secop.core import Attached, BoolType, EnumType, FloatRange, \
|
||||
HasIodev, Module, Parameter, StringIO, Writable
|
||||
|
||||
|
||||
class K2601bIO(StringIO):
|
||||
@ -42,13 +42,13 @@ SOURCECMDS = {
|
||||
|
||||
|
||||
class SourceMeter(HasIodev, Module):
|
||||
parameters = {
|
||||
'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True),
|
||||
'power': Parameter('readback power', FloatRange(unit='W'), poll=True),
|
||||
'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||
readonly=False, default=0),
|
||||
'active': Parameter('output enable', BoolType(), readonly=False, poll=True),
|
||||
}
|
||||
|
||||
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
|
||||
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
|
||||
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||
readonly=False, default=0)
|
||||
active = Parameter('output enable', BoolType(), readonly=False, poll=True)
|
||||
|
||||
iodevClass = K2601bIO
|
||||
|
||||
def read_resistivity(self):
|
||||
@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module):
|
||||
|
||||
|
||||
class Current(HasIodev, Writable):
|
||||
properties = {
|
||||
'sourcemeter': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('measured current', FloatRange(unit='A'), poll=True),
|
||||
'target': Override('set current', FloatRange(unit='A'), 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),
|
||||
}
|
||||
sourcemeter = Attached()
|
||||
|
||||
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
|
||||
target = Parameter('set current', FloatRange(unit='A'), 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):
|
||||
return self.sendRecv('print(smua.measure.i())')
|
||||
@ -120,15 +117,12 @@ class Current(HasIodev, Writable):
|
||||
|
||||
|
||||
class Voltage(HasIodev, Writable):
|
||||
properties = {
|
||||
'sourcemeter': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('measured voltage', FloatRange(unit='V'), poll=True),
|
||||
'target': Override('set voltage', FloatRange(unit='V'), poll=True),
|
||||
'active': Parameter('voltage is controlled', BoolType(), poll=True),
|
||||
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True),
|
||||
}
|
||||
sourcemeter = Attached()
|
||||
|
||||
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
|
||||
target = Parameter('set voltage', FloatRange(unit='V'), 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):
|
||||
return self.sendRecv('print(smua.measure.v())')
|
||||
@ -159,7 +153,7 @@ class Voltage(HasIodev, Writable):
|
||||
def write_active(self, value):
|
||||
if self._sourcemeter.mode != 2:
|
||||
if value:
|
||||
self._sourcemeter.write_mode(2) # switch to voltage
|
||||
self._sourcemeter.write_mode(2) # switch to voltage
|
||||
else:
|
||||
return 0
|
||||
return self._sourcemeter.write_active(value)
|
||||
|
@ -22,13 +22,13 @@
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@ -59,19 +59,18 @@ class StringIO(secop.stringio.StringIO):
|
||||
|
||||
|
||||
class Main(HasIodev, Drivable):
|
||||
parameters = {
|
||||
'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)),
|
||||
'target': Override('channel to select', datatype=IntRange(0, 17)),
|
||||
'autoscan':
|
||||
Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False),
|
||||
'pollinterval': Override('sleeptime between polls', default=1),
|
||||
}
|
||||
|
||||
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
|
||||
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
||||
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
||||
pollinterval = Parameter('sleeptime between polls', default=1)
|
||||
|
||||
pollerClass = Poller
|
||||
iodevClass = StringIO
|
||||
_channel_changed = 0 # time of last channel change
|
||||
_channels = None # dict <channel no> of <module object>
|
||||
|
||||
def earlyInit(self):
|
||||
self._channel_changed = 0
|
||||
self._channels = {}
|
||||
|
||||
def register_channel(self, modobj):
|
||||
@ -85,10 +84,8 @@ class Main(HasIodev, Drivable):
|
||||
|
||||
def read_value(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:
|
||||
return channel
|
||||
return channel
|
||||
if not self._channels[channel].enabled:
|
||||
# channel was disabled recently, but still selected
|
||||
nextchannel = 0
|
||||
@ -129,61 +126,42 @@ class ResChannel(HasIodev, Readable):
|
||||
|
||||
RES_RANGE = {key: i+1 for i, key in list(
|
||||
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
|
||||
CUR_RANGE = {key: i + 1 for i, key in list(
|
||||
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(
|
||||
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
|
||||
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),
|
||||
'main':
|
||||
Attached()
|
||||
}
|
||||
channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
|
||||
main = Attached()
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='Ohm')),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
'range':
|
||||
Parameter('reading range', readonly=False,
|
||||
datatype=EnumType(**RES_RANGE), handler=rdgrng),
|
||||
'minrange':
|
||||
Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||
datatype=EnumType(**RES_RANGE)),
|
||||
'autorange':
|
||||
Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
||||
readonly=False, handler=rdgrng, default=2),
|
||||
'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),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||
pollinterval = Parameter(visibility=3)
|
||||
range = Parameter('reading range', readonly=False,
|
||||
datatype=EnumType(**RES_RANGE), handler=rdgrng)
|
||||
minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||
datatype=EnumType(**RES_RANGE))
|
||||
autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
||||
readonly=False, handler=rdgrng, default=2)
|
||||
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):
|
||||
self._main = self.DISPATCHER.get_module(self.main)
|
||||
self._main.register_channel(self)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
self._last_range_change = 0
|
||||
super().startModule(started_callback)
|
||||
|
||||
def read_value(self):
|
||||
if self.channel != self._main.value:
|
||||
return Done
|
||||
@ -195,7 +173,7 @@ class ResChannel(HasIodev, Readable):
|
||||
if self.autorange == 'soft':
|
||||
now = time.time()
|
||||
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 abs(result) > self.RES_SCALE[rng]:
|
||||
if rng < 22:
|
||||
@ -236,8 +214,6 @@ class ResChannel(HasIodev, Readable):
|
||||
result = dict(range=rng)
|
||||
if autorange:
|
||||
result['autorange'] = 'hard'
|
||||
#elif self.autorange == 'hard':
|
||||
# result['autorange'] = 'soft'
|
||||
# else: do not change autorange
|
||||
self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
||||
if excoff:
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
from secop.modules import Communicator
|
||||
|
||||
|
||||
class Ls370Sim(Communicator):
|
||||
CHANNEL_COMMANDS = [
|
||||
('RDGR?%d', '1.0'),
|
||||
@ -32,9 +33,8 @@ class Ls370Sim(Communicator):
|
||||
]
|
||||
OTHER_COMMANDS = [
|
||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||
('SCAN?', '1,1'),
|
||||
('SCAN?', '3,1'),
|
||||
]
|
||||
channel = [None]
|
||||
|
||||
def earlyInit(self):
|
||||
self._data = dict(self.OTHER_COMMANDS)
|
||||
@ -43,7 +43,7 @@ class Ls370Sim(Communicator):
|
||||
self._data[fmt % chan] = v
|
||||
# mkthread(self.run)
|
||||
|
||||
def do_communicate(self, command):
|
||||
def communicate(self, command):
|
||||
# simulation part, time independent
|
||||
for channel in range(1,17):
|
||||
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
||||
|
@ -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.
|
||||
"""
|
||||
|
||||
import time
|
||||
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
|
||||
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.metaclass import Done
|
||||
|
||||
try:
|
||||
import secop_psi.ppmswindows as ppmshw
|
||||
@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler):
|
||||
class Main(Communicator):
|
||||
"""ppms communicator module"""
|
||||
|
||||
parameters = {
|
||||
'pollinterval': Parameter('poll interval', readonly=False,
|
||||
datatype=FloatRange(), default=2),
|
||||
'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()),
|
||||
}
|
||||
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
||||
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
|
||||
default="", readonly=True)
|
||||
|
||||
_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',
|
||||
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
|
||||
assert len(_channel_names) == 30
|
||||
@ -102,7 +96,8 @@ class Main(Communicator):
|
||||
def register(self, other):
|
||||
self.modules[other.channel] = other
|
||||
|
||||
def do_communicate(self, command):
|
||||
def communicate(self, command):
|
||||
"""GPIB command"""
|
||||
with self.lock:
|
||||
reply = self._ppms_device.send(command)
|
||||
self.log.debug("%s|%s", command, reply)
|
||||
@ -114,7 +109,7 @@ class Main(Communicator):
|
||||
if channel.enabled:
|
||||
mask |= 1 << self._channel_to_index.get(channelname, 0)
|
||||
# send, read and convert to floats and ints
|
||||
data = self.do_communicate('GETDAT? %d' % mask)
|
||||
data = self.communicate('GETDAT? %d' % mask)
|
||||
reply = data.split(',')
|
||||
mask = int(reply.pop(0))
|
||||
reply.pop(0) # pop timestamp
|
||||
@ -133,23 +128,23 @@ class Main(Communicator):
|
||||
return data # return data as string
|
||||
|
||||
|
||||
class PpmsMixin(HasIodev, Module):
|
||||
"""common methods for ppms modules"""
|
||||
|
||||
parameters = {
|
||||
'pollinterval': None,
|
||||
}
|
||||
class PpmsBase(HasIodev, Readable):
|
||||
"""common base for all ppms modules"""
|
||||
iodev = Attached()
|
||||
|
||||
pollerClass = Poller
|
||||
enabled = True # default, if no parameter enable is defined
|
||||
_last_settings = None # used by several modules
|
||||
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):
|
||||
self._iodev.register(self)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
""""""
|
||||
# no polls except on main module
|
||||
started_callback()
|
||||
|
||||
@ -160,8 +155,8 @@ class PpmsMixin(HasIodev, Module):
|
||||
|
||||
def read_status(self):
|
||||
# polling is done by the main module
|
||||
# and PPMS does not deliver really fresh status values anyway:
|
||||
# e.g. the status is not changed immediately after a target change!
|
||||
# and PPMS does not deliver really fresh status values anyway: the status is not
|
||||
# changed immediately after a target change!
|
||||
return Done
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
@ -177,29 +172,22 @@ class PpmsMixin(HasIodev, Module):
|
||||
self.status = (self.Status.IDLE, '')
|
||||
|
||||
|
||||
class Channel(PpmsMixin, Readable):
|
||||
class Channel(PpmsBase):
|
||||
"""channel base class"""
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override('main value of channels', poll=True),
|
||||
'enabled':
|
||||
Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=False),
|
||||
}
|
||||
properties = {
|
||||
'channel':
|
||||
Property('channel name',
|
||||
datatype=StringType(), export=False, default=''),
|
||||
'no':
|
||||
Property('channel number',
|
||||
datatype=IntRange(1, 4), export=False),
|
||||
}
|
||||
value = Parameter('main value of channels', poll=True)
|
||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=False)
|
||||
|
||||
channel = Property('channel name',
|
||||
datatype=StringType(), export=False, default='')
|
||||
no = Property('channel number',
|
||||
datatype=IntRange(1, 4), export=False)
|
||||
|
||||
def earlyInit(self):
|
||||
Readable.earlyInit(self)
|
||||
if not self.channel:
|
||||
self.properties['channel'] = self.name
|
||||
self.channel = self.name
|
||||
|
||||
def get_settings(self, pname):
|
||||
return ''
|
||||
@ -208,15 +196,12 @@ class Channel(PpmsMixin, Readable):
|
||||
class UserChannel(Channel):
|
||||
"""user channel"""
|
||||
|
||||
properties = {
|
||||
'no':
|
||||
Property('*(unused)*',
|
||||
datatype=IntRange(0, 0), export=False, default=0),
|
||||
'linkenable':
|
||||
Property('name of linked channel for enabling',
|
||||
datatype=StringType(), export=False, default=''),
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
}
|
||||
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):
|
||||
other = self._iodev.modules.get(self.linkenable, None)
|
||||
@ -230,14 +215,11 @@ class DriverChannel(Channel):
|
||||
|
||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
||||
|
||||
parameters = {
|
||||
'current':
|
||||
Parameter('driver current', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 5000., unit='uA')),
|
||||
'powerlimit':
|
||||
Parameter('power limit', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 1000., unit='uW')),
|
||||
}
|
||||
current = Parameter('driver current', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 5000., unit='uA'))
|
||||
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
|
||||
datatype=FloatRange(0., 1000., unit='uW'))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
def analyze_drvout(self, no, current, powerlimit):
|
||||
if self.no != no:
|
||||
@ -255,25 +237,19 @@ class BridgeChannel(Channel):
|
||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
||||
# pylint: disable=invalid-name
|
||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
||||
parameters = {
|
||||
'enabled':
|
||||
Override(handler=bridge),
|
||||
'excitation':
|
||||
Parameter('excitation current', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.01, 5000., unit='uA')),
|
||||
'powerlimit':
|
||||
Parameter('power limit', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.001, 1000., unit='uW')),
|
||||
'dcflag':
|
||||
Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||
datatype=BoolType()),
|
||||
'readingmode':
|
||||
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')),
|
||||
}
|
||||
|
||||
enabled = Parameter(handler=bridge)
|
||||
excitation = Parameter('excitation current', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.01, 5000., unit='uA'))
|
||||
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
|
||||
datatype=FloatRange(0.001, 1000., unit='uW'))
|
||||
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||
datatype=BoolType())
|
||||
readingmode = 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'))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
||||
if self.no != no:
|
||||
@ -294,23 +270,22 @@ class BridgeChannel(Channel):
|
||||
return self.no, 0, 0, change.dcflag, change.readingmode, 0
|
||||
|
||||
|
||||
class Level(PpmsMixin, Readable):
|
||||
class Level(PpmsBase):
|
||||
"""helium level"""
|
||||
|
||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
||||
|
||||
parameters = {
|
||||
'value': Override(datatype=FloatRange(unit='%'), handler=level),
|
||||
'status': Override(handler=level),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
|
||||
status = Parameter(handler=level)
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
channel = 'level'
|
||||
|
||||
def update_value_status(self, value, packed_status):
|
||||
pass
|
||||
# must be a no-op
|
||||
# when called from Main.read_data, value is always None
|
||||
# value and status is polled via settings
|
||||
pass
|
||||
|
||||
def analyze_level(self, level, status):
|
||||
# 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, ''))
|
||||
|
||||
|
||||
class Chamber(PpmsMixin, Drivable):
|
||||
class Chamber(PpmsBase, Drivable):
|
||||
"""sample chamber handling
|
||||
|
||||
value is an Enum, which is redundant with the status text
|
||||
@ -351,14 +326,13 @@ class Chamber(PpmsMixin, Drivable):
|
||||
venting_continuously=9,
|
||||
general_failure=15,
|
||||
)
|
||||
parameters = {
|
||||
'value':
|
||||
Override(description='chamber state', handler=chamber,
|
||||
datatype=EnumType(StatusCode)),
|
||||
'target':
|
||||
Override(description='chamber command', handler=chamber,
|
||||
datatype=EnumType(Operation)),
|
||||
}
|
||||
|
||||
value = Parameter(description='chamber state', handler=chamber,
|
||||
datatype=EnumType(StatusCode))
|
||||
target = Parameter(description='chamber command', handler=chamber,
|
||||
datatype=EnumType(Operation))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
||||
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
||||
@ -387,44 +361,40 @@ class Chamber(PpmsMixin, Drivable):
|
||||
return dict(target=target)
|
||||
|
||||
def change_chamber(self, change):
|
||||
# write settings, combining <pname>=<value> and current attributes
|
||||
# and request updated settings
|
||||
if change.target == self.Operation.noop:
|
||||
return None
|
||||
return (change.target,)
|
||||
|
||||
|
||||
class Temp(PpmsMixin, Drivable):
|
||||
class Temp(PpmsBase, Drivable):
|
||||
"""temperature"""
|
||||
|
||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
||||
Status = Enum(Drivable.Status,
|
||||
RAMPING = 370,
|
||||
STABILIZING = 380,
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
RAMPING=370,
|
||||
STABILIZING=380,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='K'), poll=True),
|
||||
'status':
|
||||
Override(datatype=StatusType(Status), poll=True),
|
||||
'target':
|
||||
Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False),
|
||||
'setpoint':
|
||||
Parameter('intermediate set point',
|
||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp),
|
||||
'ramp':
|
||||
Parameter('ramping speed', readonly=False, default=0,
|
||||
datatype=FloatRange(0, 20, unit='K/min')),
|
||||
'workingramp':
|
||||
Parameter('intermediate ramp value',
|
||||
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),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
|
||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
|
||||
setpoint = Parameter('intermediate set point',
|
||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
|
||||
ramp = Parameter('ramping speed', readonly=False, default=0,
|
||||
datatype=FloatRange(0, 20, unit='K/min'))
|
||||
workingramp = Parameter('intermediate ramp value',
|
||||
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
|
||||
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
|
||||
datatype=EnumType(ApproachMode))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
||||
datatype=FloatRange(0, unit='sec'), default=3600)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
TempStatus = Enum(
|
||||
'TempStatus',
|
||||
@ -449,17 +419,14 @@ class Temp(PpmsMixin, Drivable):
|
||||
14: (Status.ERROR, 'can not complete'),
|
||||
15: (Status.ERROR, 'general failure'),
|
||||
}
|
||||
properties = {
|
||||
'general_stop': Property('respect general stop', datatype=BoolType(),
|
||||
export=True, default=True)
|
||||
}
|
||||
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||
default=True, value=False)
|
||||
|
||||
channel = 'temp'
|
||||
_stopped = False
|
||||
_expected_target_time = 0
|
||||
_last_change = 0 # 0 means no target change is pending
|
||||
_last_target = None # last reached target
|
||||
general_stop = False
|
||||
_cool_deadline = 0
|
||||
_wait_at10 = False
|
||||
_ramp_at_limit = False
|
||||
@ -573,7 +540,7 @@ class Temp(PpmsMixin, Drivable):
|
||||
def calc_expected(self, target, 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():
|
||||
return
|
||||
if self.status[0] != self.Status.STABILIZING:
|
||||
@ -586,37 +553,31 @@ class Temp(PpmsMixin, Drivable):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class Field(PpmsMixin, Drivable):
|
||||
class Field(PpmsBase, Drivable):
|
||||
"""magnetic field"""
|
||||
|
||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
||||
Status = Enum(Drivable.Status,
|
||||
PREPARED = 150,
|
||||
PREPARING = 340,
|
||||
RAMPING = 370,
|
||||
FINALIZING = 390,
|
||||
Status = Enum(
|
||||
Drivable.Status,
|
||||
PREPARED=150,
|
||||
PREPARING=340,
|
||||
RAMPING=370,
|
||||
FINALIZING=390,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
||||
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='T'), poll=True),
|
||||
'status':
|
||||
Override(datatype=StatusType(Status), poll=True),
|
||||
'target':
|
||||
Override(datatype=FloatRange(-15, 15, unit='T'), handler=field),
|
||||
'ramp':
|
||||
Parameter('ramping speed', readonly=False, handler=field,
|
||||
datatype=FloatRange(0.064, 1.19, unit='T/min')),
|
||||
'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)),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
|
||||
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
|
||||
ramp = Parameter('ramping speed', readonly=False, handler=field,
|
||||
datatype=FloatRange(0.064, 1.19, unit='T/min'))
|
||||
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))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'persistent mode'),
|
||||
@ -652,7 +613,7 @@ class Field(PpmsMixin, Drivable):
|
||||
else:
|
||||
status = (self.Status.WARN, 'timeout when ramping leads')
|
||||
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:
|
||||
self._last_change = 0
|
||||
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 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():
|
||||
return
|
||||
newtarget = clamp(self._last_target, self.value, self.target)
|
||||
@ -729,23 +690,20 @@ class Field(PpmsMixin, Drivable):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class Position(PpmsMixin, Drivable):
|
||||
class Position(PpmsBase, Drivable):
|
||||
"""rotator position"""
|
||||
|
||||
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
||||
Status = Drivable.Status
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='deg'), poll=True),
|
||||
'target':
|
||||
Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move),
|
||||
'enabled':
|
||||
Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=True),
|
||||
'speed':
|
||||
Parameter('motor speed', readonly=False, handler=move,
|
||||
datatype=FloatRange(0.8, 12, unit='deg/sec')),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
|
||||
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
|
||||
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||
datatype=BoolType(), default=True)
|
||||
speed = Parameter('motor speed', readonly=False, handler=move,
|
||||
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
||||
# pollinterval = Parameter(visibility=3)
|
||||
|
||||
STATUS_MAP = {
|
||||
1: (Status.IDLE, 'at target'),
|
||||
5: (Status.BUSY, 'moving'),
|
||||
@ -824,7 +782,7 @@ class Position(PpmsMixin, Drivable):
|
||||
self.speed = value
|
||||
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():
|
||||
return
|
||||
newtarget = clamp(self._last_target, self.value, self.target)
|
||||
|
@ -20,9 +20,9 @@
|
||||
# *****************************************************************************
|
||||
"""PPMS mf proxy"""
|
||||
|
||||
from secop.core import Enum, FloatRange, EnumType, Override, Parameter, Drivable
|
||||
from secop.datatypes import StatusType
|
||||
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
|
||||
|
||||
|
||||
|
@ -18,9 +18,10 @@
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
import time
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
|
||||
|
||||
def num(string):
|
||||
return json.loads(string)
|
||||
|
@ -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
|
||||
"""
|
||||
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import json
|
||||
from os.path import join, expanduser
|
||||
from os.path import expanduser, join
|
||||
|
||||
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.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
|
||||
|
||||
|
||||
CFG_HEADER = """[NODE]
|
||||
id = %(samenv)s.psi.ch
|
||||
description = %(samenv)s over SEA
|
||||
@ -76,7 +76,7 @@ def get_sea_port(instance):
|
||||
for line in f:
|
||||
linesplit = line.split()
|
||||
if len(linesplit) == 3:
|
||||
cmd, var, value = line.split()
|
||||
_, var, value = line.split()
|
||||
if var == 'serverport':
|
||||
return value
|
||||
except FileNotFoundError:
|
||||
@ -87,23 +87,10 @@ def get_sea_port(instance):
|
||||
class SeaClient(ProxyClient, Module):
|
||||
"""connection to SEA"""
|
||||
|
||||
properties = {
|
||||
'json_path': Property('path to SEA json descriptors',
|
||||
datatype=StringType(),
|
||||
default=join(expanduser('~'), 'sea/tcl/json'))
|
||||
}
|
||||
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()),
|
||||
}
|
||||
json_path = Property('path to SEA json descriptors', StringType())
|
||||
|
||||
uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000')
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=10)
|
||||
|
||||
def __init__(self, name, log, opts, srv):
|
||||
instance = srv.node_cfg['name'].rsplit('_', 1)[0]
|
||||
@ -198,7 +185,7 @@ class SeaClient(ProxyClient, Module):
|
||||
if msg.startswith('_E '):
|
||||
try:
|
||||
_, path, readerror = msg.split(None, 2)
|
||||
except Exception as e:
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
@ -241,11 +228,15 @@ class SeaClient(ProxyClient, Module):
|
||||
# do not update unchanged values within 0.1 sec
|
||||
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)
|
||||
return reply
|
||||
|
||||
def do_describe(self):
|
||||
@Command(result=StringType())
|
||||
def describe(self):
|
||||
"""save objects (and sub-objects) description"""
|
||||
reply = self.request('describe_all')
|
||||
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
|
||||
samenv, reply = json.loads(reply)
|
||||
@ -288,9 +279,7 @@ def get_datatype(paramdesc):
|
||||
|
||||
|
||||
class SeaModule(Module):
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
}
|
||||
iodev = Attached()
|
||||
|
||||
# pollerClass=None
|
||||
path2param = None
|
||||
@ -329,8 +318,7 @@ class SeaModule(Module):
|
||||
else: # take all
|
||||
main = ''
|
||||
path2param = {}
|
||||
parameters = {}
|
||||
attributes = dict(sea_object=sea_object, path2param=path2param, parameters=parameters)
|
||||
attributes = dict(sea_object=sea_object, path2param=path2param)
|
||||
for paramdesc in descr:
|
||||
path = paramdesc['path']
|
||||
readonly = paramdesc.get('readonly', True)
|
||||
@ -351,6 +339,7 @@ class SeaModule(Module):
|
||||
else:
|
||||
kwds['group'] = pathlist[-2]
|
||||
# flatten path to parameter name
|
||||
key = None
|
||||
for i in reversed(range(len(pathlist))):
|
||||
key = '_'.join(pathlist[i:])
|
||||
if not key in cls.accessibles:
|
||||
@ -361,12 +350,12 @@ class SeaModule(Module):
|
||||
if key in cls.accessibles:
|
||||
if key == 'target':
|
||||
kwds['readonly'] = False
|
||||
pobj = Override(**kwds)
|
||||
pobj = cls.accessibles[key].override(**kwds)
|
||||
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
|
||||
else:
|
||||
pobj = Parameter(**kwds)
|
||||
datatype = pobj.datatype
|
||||
parameters[key] = pobj
|
||||
attributes[key] = pobj
|
||||
if not hasattr(cls, 'read_' + key):
|
||||
def rfunc(self, cmd='hval /sics/%s/%s' % (sea_object, path)):
|
||||
print('READ', cmd)
|
||||
@ -395,19 +384,20 @@ class SeaModule(Module):
|
||||
return Done
|
||||
|
||||
attributes['write_' + key] = wfunc
|
||||
|
||||
# create standard parameters like value and status, if not yet there
|
||||
for pname, pobj in cls.accessibles.items():
|
||||
if pname == 'pollinterval':
|
||||
parameters[pname] = Override(export=False)
|
||||
elif pname not in parameters and isinstance(pobj, Parameter):
|
||||
parameters[pname] = Override(poll=False, needscfg=False)
|
||||
attributes[pname] = pobj.override(export=False)
|
||||
elif pname not in attributes and isinstance(pobj, Parameter):
|
||||
attributes[pname] = pobj.override(poll=False, needscfg=False)
|
||||
|
||||
classname = '%s_%s' % (cls.__name__, sea_object)
|
||||
newcls = ModuleMeta.__new__(ModuleMeta, classname, (cls,), attributes)
|
||||
newcls = type(classname, (cls,), attributes)
|
||||
return Module.__new__(newcls)
|
||||
|
||||
def __init__(self, name, logger, cfgdict, dispatcher):
|
||||
Module.__init__(self, name, logger, cfgdict, dispatcher)
|
||||
# def __init__(self, name, logger, cfgdict, dispatcher):
|
||||
# Module.__init__(self, name, logger, cfgdict, dispatcher)
|
||||
|
||||
def updateEvent(self, module, parameter, value, timestamp, readerror):
|
||||
upd = getattr(self, 'update_' + parameter, None)
|
||||
@ -442,9 +432,9 @@ class SeaReadable(SeaModule, Readable):
|
||||
if readerror:
|
||||
value = repr(readerror)
|
||||
if value == '':
|
||||
self.status = [self.Status.IDLE, '']
|
||||
self.status = (self.Status.IDLE, '')
|
||||
else:
|
||||
self.status = [self.Status.ERROR, value]
|
||||
self.status = (self.Status.ERROR, value)
|
||||
|
||||
def read_status(self):
|
||||
return self.status
|
||||
@ -485,11 +475,11 @@ class SeaDrivable(SeaModule, Drivable):
|
||||
|
||||
def updateStatus(self):
|
||||
if self._sea_status:
|
||||
self.status = [self.Status.ERROR, self._sea_status]
|
||||
self.status = (self.Status.ERROR, self._sea_status)
|
||||
elif self._is_running:
|
||||
self.status = [self.Status.BUSY, 'driving']
|
||||
self.status = (self.Status.BUSY, 'driving')
|
||||
else:
|
||||
self.status = [self.Status.IDLE, '']
|
||||
self.status = (self.Status.IDLE, '')
|
||||
|
||||
def updateTarget(self, module, parameter, value, timestamp, readerror):
|
||||
if value is not None:
|
||||
|
@ -21,33 +21,33 @@
|
||||
"""senis hall sensor"""
|
||||
|
||||
|
||||
import time
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
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):
|
||||
pollerClass = None
|
||||
parameters = {
|
||||
'value': Override(datatype=FloatRange(unit='degC')),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='degC'))
|
||||
|
||||
|
||||
class Bcomp(Readable):
|
||||
pollerClass = None
|
||||
parameters = {
|
||||
'value': Override(datatype=FloatRange(unit='T')),
|
||||
'range': Parameter('working range', FloatRange(unit='T'), default=0),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='T'))
|
||||
range = Parameter('working range', FloatRange(unit='T'), default=0)
|
||||
|
||||
|
||||
class Raw(Readable):
|
||||
pollerClass = None
|
||||
parameters = {
|
||||
'value': Override(datatype=FloatRange()),
|
||||
}
|
||||
|
||||
value = Parameter(datatype=FloatRange())
|
||||
|
||||
|
||||
class TeslameterBase(Readable):
|
||||
@ -58,18 +58,15 @@ class TeslameterBase(Readable):
|
||||
|
||||
the B components (and temperatures for 3MH6) are implemented as separate modules
|
||||
"""
|
||||
properties = {
|
||||
'x': Attached(),
|
||||
'y': Attached(),
|
||||
'z': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('B vector', poll=True,
|
||||
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T'))),
|
||||
'usb': Parameter('usb device', StringType(), readonly=False),
|
||||
'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),
|
||||
}
|
||||
x = Attached()
|
||||
y = Attached()
|
||||
z = Attached()
|
||||
|
||||
value = Parameter('B vector', poll=True,
|
||||
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T')))
|
||||
usb = Parameter('usb device', StringType(), readonly=False)
|
||||
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):
|
||||
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
|
||||
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):
|
||||
self.init_serial(115200)
|
||||
@ -122,7 +117,7 @@ class Teslameter3MH3(TeslameterBase):
|
||||
s.timeout = 0.1 + 0.02 * self.nsample
|
||||
for _ in range(2):
|
||||
self.write_bytes(b'B')
|
||||
t = time.time()
|
||||
# t = time.time()
|
||||
reply = self.read_bytes(8 * self.nsample)
|
||||
s.timeout = 0.1
|
||||
self.stop_reading()
|
||||
@ -147,21 +142,19 @@ class Teslameter3MH3(TeslameterBase):
|
||||
|
||||
class Teslameter3MH6(TeslameterBase):
|
||||
"""luxury model with probe and box temperature and autorange"""
|
||||
properties = {
|
||||
'x_direct': Attached(),
|
||||
'y_direct': Attached(),
|
||||
'z_direct': Attached(),
|
||||
'probe_temp': Attached(),
|
||||
'box_temp': Attached(),
|
||||
'probe_temp_direct': Attached(),
|
||||
'box_temp_direct': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'range': Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0),
|
||||
'rate': Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
|
||||
readonly=False, poll=True),
|
||||
'avtime': Parameter('data acquisition time', FloatRange(), default=0),
|
||||
}
|
||||
x_direct = Attached()
|
||||
y_direct = Attached()
|
||||
z_direct = Attached()
|
||||
probe_temp = Attached()
|
||||
box_temp = Attached()
|
||||
probe_temp_direct = Attached()
|
||||
box_temp_direct = Attached()
|
||||
|
||||
range = Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0)
|
||||
rate = Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
|
||||
readonly=False, poll=True)
|
||||
avtime = Parameter('data acquisition time', FloatRange(), default=0)
|
||||
|
||||
SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000,
|
||||
0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10}
|
||||
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')
|
||||
if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0):
|
||||
status = 'checksum error'
|
||||
continue
|
||||
continue
|
||||
# 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):
|
||||
break
|
||||
@ -219,7 +212,7 @@ class Teslameter3MH6(TeslameterBase):
|
||||
self._z.value = mean['z'] * 0.001
|
||||
self._probe_temp.value = mean['thc']
|
||||
self._box_temp.value = mean['tec']
|
||||
|
||||
|
||||
self.write_bytes(b'D') # put into NONcalibrated mode
|
||||
if self.read_bytes(1) != b'd':
|
||||
self.log.error('missing response to D command')
|
||||
|
@ -20,13 +20,14 @@
|
||||
# *****************************************************************************
|
||||
"""Software calibration"""
|
||||
|
||||
import os
|
||||
from os.path import join, exists, basename
|
||||
import math
|
||||
import numpy as np
|
||||
from scipy.interpolate import splrep, splev # pylint: disable=import-error
|
||||
import os
|
||||
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):
|
||||
@ -102,6 +103,7 @@ class CalCurve:
|
||||
sensopt = calibspec.split(',')
|
||||
calibname = sensopt.pop(0)
|
||||
_, dot, ext = basename(calibname).rpartition('.')
|
||||
kind = None
|
||||
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
||||
# first try without adding kind
|
||||
filename = join(path.strip(), calibname)
|
||||
@ -109,8 +111,8 @@ class CalCurve:
|
||||
kind = ext if dot else None
|
||||
break
|
||||
# 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))
|
||||
if exists(filename):
|
||||
break
|
||||
@ -150,16 +152,14 @@ class CalCurve:
|
||||
|
||||
|
||||
class Sensor(Readable):
|
||||
properties = {
|
||||
'rawsensor': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'calib': Parameter('calibration name', datatype=StringType(), readonly=False),
|
||||
'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True),
|
||||
'value': Override(unit='K'),
|
||||
'pollinterval': Override(export=False),
|
||||
'status': Override(default=(Readable.Status.ERROR, 'unintialized'))
|
||||
}
|
||||
rawsensor = Attached()
|
||||
|
||||
calib = Parameter('calibration name', datatype=StringType(), readonly=False)
|
||||
abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True)
|
||||
value = Parameter(unit='K')
|
||||
pollinterval = Parameter(export=False)
|
||||
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||
|
||||
pollerClass = None
|
||||
description = 'a calibrated sensor value'
|
||||
_value_error = None
|
||||
@ -179,7 +179,7 @@ class Sensor(Readable):
|
||||
self._value_error = None
|
||||
|
||||
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
|
||||
return None
|
||||
self._value_error = repr(err)
|
||||
|
@ -20,40 +20,38 @@
|
||||
# *****************************************************************************
|
||||
"""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):
|
||||
|
||||
commands = {
|
||||
'arg':
|
||||
Command('5 args',
|
||||
argument=TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())),
|
||||
result=StringType()),
|
||||
'keyed':
|
||||
Command('keyworded arg', argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']), result=StringType()),
|
||||
'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=''))),
|
||||
}
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def do_one(self, arg):
|
||||
@Command(argument=FloatRange(), result=StringType())
|
||||
def one(self, arg):
|
||||
"""1 arg"""
|
||||
return repr(arg)
|
||||
|
||||
def do_none(self):
|
||||
@Command(result=StringType())
|
||||
def none(self):
|
||||
"""no arg"""
|
||||
return repr(None)
|
||||
|
@ -20,31 +20,23 @@
|
||||
# *****************************************************************************
|
||||
"""Temp"""
|
||||
|
||||
from secop.modules import Readable, Drivable, Parameter, Override
|
||||
from secop.datatypes import FloatRange, IntRange, StringType
|
||||
from secop.modules import Drivable, Parameter, Readable
|
||||
from secop.stringio import HasIodev
|
||||
|
||||
Status = Drivable.Status
|
||||
|
||||
class TempLoop(HasIodev, Drivable):
|
||||
'''temperature channel on Lakeshore 336'''
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='K'), default=0, poll=True),
|
||||
'status':
|
||||
Override(poll=False),
|
||||
'target':
|
||||
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),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
'channel':
|
||||
Parameter('the Lakeshore channel', datatype=StringType(), export=False),
|
||||
'loop':
|
||||
Parameter('the Lakeshore loop number', datatype=IntRange(1,3), export=False),
|
||||
}
|
||||
class TempLoop(HasIodev, Drivable):
|
||||
"""temperature channel on Lakeshore 336"""
|
||||
|
||||
value = Parameter(datatype=FloatRange(unit='K'), default=0, poll=True)
|
||||
status = Parameter(poll=False)
|
||||
target = Parameter(datatype=FloatRange(1.0, 402.0, unit='K'), default=1.3, poll=True)
|
||||
tolerance = Parameter('the tolerance', FloatRange(-400, 400), default=1, readonly=False)
|
||||
pollinterval = Parameter(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):
|
||||
super(TempLoop, self).earlyInit()
|
||||
@ -67,24 +59,18 @@ class TempLoop(HasIodev, Drivable):
|
||||
float('x')
|
||||
return result
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
self.target = self.value
|
||||
self.status = [Status.IDLE, 'stopped']
|
||||
|
||||
|
||||
class TempChannel(HasIodev, Readable):
|
||||
'''temperature channel on Lakeshore 336'''
|
||||
"""temperature channel on Lakeshore 336"""
|
||||
|
||||
parameters = {
|
||||
'value':
|
||||
Override(datatype=FloatRange(unit='K'), default=0, poll=True),
|
||||
'status':
|
||||
Override(poll=False, constant=[Status.IDLE, 'idle']),
|
||||
'pollinterval':
|
||||
Override(visibility=3),
|
||||
'channel':
|
||||
Parameter('the Lakeshore channel', datatype=StringType(), export=False),
|
||||
}
|
||||
value = Parameter(datatype=FloatRange(unit='K'), default=0, poll=True)
|
||||
status = Parameter(poll=False, constant=[Status.IDLE, 'idle'])
|
||||
pollinterval = Parameter(visibility=3)
|
||||
channel = Parameter('the Lakeshore channel', datatype=StringType(), export=False)
|
||||
|
||||
def read_value(self):
|
||||
result = self.sendRecv('KRDG?%s' % self.channel)
|
||||
|
@ -20,17 +20,19 @@
|
||||
# *****************************************************************************
|
||||
"""frappy support for ultrasound"""
|
||||
|
||||
import math
|
||||
#import serial
|
||||
import os
|
||||
import math
|
||||
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 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):
|
||||
tm = time.localtime(t)
|
||||
@ -43,32 +45,27 @@ def fname_from_time(t, extension):
|
||||
|
||||
|
||||
class Roi(Readable):
|
||||
properties = {
|
||||
'main': Attached(),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('amplitude', FloatRange(), default=0),
|
||||
'phase': Parameter('phase', FloatRange(unit='deg'), default=0),
|
||||
'i': Parameter('in phase', FloatRange(), default=0),
|
||||
'q': Parameter('out of phase', FloatRange(), default=0),
|
||||
'time': Parameter('start time', FloatRange(unit='nsec'),
|
||||
readonly=False),
|
||||
'size': Parameter('interval (symmetric around time)', FloatRange(unit='nsec'),
|
||||
readonly=False),
|
||||
'enable': Parameter('calculate this roi', BoolType(), readonly=False, default=True),
|
||||
#'status': Override(export=False),
|
||||
'pollinterval': Override(export=False),
|
||||
}
|
||||
|
||||
main = Attached()
|
||||
|
||||
value = Parameter('amplitude', FloatRange(), default=0)
|
||||
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||
i = Parameter('in phase', FloatRange(), default=0)
|
||||
q = Parameter('out of phase', FloatRange(), default=0)
|
||||
time = Parameter('start time', FloatRange(unit='nsec'), readonly=False)
|
||||
size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
|
||||
enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
|
||||
#status = Parameter(export=False)
|
||||
pollinterval = Parameter(export=False)
|
||||
|
||||
interval = (0,0)
|
||||
|
||||
|
||||
def initModule(self):
|
||||
self._main.register_roi(self)
|
||||
self.calc_interval()
|
||||
|
||||
def calc_interval(self):
|
||||
self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size)
|
||||
|
||||
|
||||
def write_time(self, value):
|
||||
self.time = value
|
||||
self.calc_interval()
|
||||
@ -83,53 +80,49 @@ class Roi(Readable):
|
||||
class Pars(Module):
|
||||
description = 'relevant parameters from SEA'
|
||||
|
||||
parameters = {
|
||||
'timestamp': Parameter('unix timestamp', StringType(), default='0', readonly=False),
|
||||
'temperature': Parameter('T', FloatRange(unit='K'), 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),
|
||||
}
|
||||
timestamp = Parameter('unix timestamp', StringType(), default='0', readonly=False)
|
||||
temperature = Parameter('T', FloatRange(unit='K'), 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)
|
||||
|
||||
|
||||
class FreqStringIO(StringIO):
|
||||
end_of_line = '\r'
|
||||
|
||||
|
||||
|
||||
|
||||
class Frequency(HasIodev, Readable):
|
||||
properties = {
|
||||
'pars': Attached(),
|
||||
'sr': Property('samples per record', datatype=IntRange(), default=16384),
|
||||
'maxy': Property('plot y scale', datatype=FloatRange(), default=0.5),
|
||||
}
|
||||
parameters = {
|
||||
'value': Override('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0),
|
||||
'basefreq': Parameter('base frequency', FloatRange(unit='Hz'), readonly=False),
|
||||
'nr': Parameter('number of records', datatype=IntRange(1,10000), default=500),
|
||||
'freq': Parameter('target frequency', FloatRange(unit='Hz'), readonly=False, poll=True),
|
||||
'amp': Parameter('amplitude', FloatRange(unit='dBm'), readonly=False, poll=True),
|
||||
'control': Parameter('control loop on?', BoolType(), readonly=False, default=True),
|
||||
'time': Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
readonly=False),
|
||||
'size': Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
readonly=False),
|
||||
'pulselen': Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1),
|
||||
'maxstep': Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000),
|
||||
'minstep': Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000),
|
||||
'slope': Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6),
|
||||
'plot': Parameter('create plot images', BoolType(), readonly=False, default=True),
|
||||
'save': Parameter('save data', BoolType(), readonly=False, default=True),
|
||||
'pollinterval': Override(datatype=FloatRange(0,120)),
|
||||
}
|
||||
pars = Attached()
|
||||
sr = Property('samples per record', datatype=IntRange(), default=16384)
|
||||
maxy = Property('plot y scale', datatype=FloatRange(), default=0.5)
|
||||
|
||||
value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0)
|
||||
basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False)
|
||||
nr = Parameter('number of records', datatype=IntRange(1,10000), default=500)
|
||||
freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False, poll=True)
|
||||
amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False, poll=True)
|
||||
control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||
time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||
readonly=False)
|
||||
pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||
default=10000)
|
||||
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||
readonly=False, default=4000)
|
||||
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||
default=1e6)
|
||||
plot = Parameter('create plot images', BoolType(), readonly=False, default=True)
|
||||
save = Parameter('save data', BoolType(), readonly=False, default=True)
|
||||
pollinterval = Parameter(datatype=FloatRange(0,120))
|
||||
|
||||
iodevClass = FreqStringIO
|
||||
|
||||
lastfreq = None
|
||||
old = None
|
||||
starttime = None
|
||||
interval = (0,0)
|
||||
|
||||
|
||||
def earlyInit(self):
|
||||
#assert self.iodev.startswith('serial:')
|
||||
#self._iodev = serial.Serial(self.iodev[7:])
|
||||
@ -142,30 +135,30 @@ class Frequency(HasIodev, Readable):
|
||||
|
||||
def calc_interval(self):
|
||||
self.interval = (self.time, self.time + self.size)
|
||||
|
||||
|
||||
def write_time(self, value):
|
||||
self.time = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
|
||||
def write_size(self, value):
|
||||
self.size = value
|
||||
self.calc_interval()
|
||||
return Done
|
||||
|
||||
|
||||
def write_nr(self, value):
|
||||
# self.pollinterval = value * 0.0001
|
||||
return value
|
||||
|
||||
|
||||
def register_roi(self, roi):
|
||||
self.roilist.append(roi)
|
||||
|
||||
|
||||
def set_freq(self):
|
||||
freq = self.freq + self.basefreq
|
||||
reply = self.sendRecv('FREQ %.15g;FREQ?' % freq)
|
||||
self.sendRecv('FREQ %.15g;FREQ?' % freq)
|
||||
#self._iodev.readline().decode('ascii')
|
||||
return freq
|
||||
|
||||
|
||||
def write_amp(self, amp):
|
||||
reply = self.sendRecv('AMPR %g;AMPR?' % amp)
|
||||
return float(reply)
|
||||
@ -173,11 +166,11 @@ class Frequency(HasIodev, Readable):
|
||||
def read_amp(self):
|
||||
reply = self.sendRecv('AMPR?')
|
||||
return float(reply)
|
||||
|
||||
|
||||
def write_freq(self, value):
|
||||
self.skipctrl = 2 # suppress control for the 2 next steps
|
||||
return value
|
||||
|
||||
|
||||
def read_freq(self):
|
||||
"""used as main polling loop body"""
|
||||
if self.lastfreq is None:
|
||||
@ -197,7 +190,7 @@ class Frequency(HasIodev, Readable):
|
||||
self.adq.start() # start next acq
|
||||
times.append(('start',time.time()))
|
||||
roilist = [r for r in self.roilist if r.enable]
|
||||
|
||||
|
||||
gates = self.adq.gates_and_curves(data, freq, self.interval,
|
||||
[r.interval for r in roilist])
|
||||
if self.save:
|
||||
|
@ -24,10 +24,11 @@
|
||||
# no fixtures needed
|
||||
import pytest
|
||||
|
||||
from secop.basic_validators import FloatProperty, PositiveFloatProperty, \
|
||||
NonNegativeFloatProperty, IntProperty, PositiveIntProperty, \
|
||||
NonNegativeIntProperty, BoolProperty, StringProperty, UnitProperty, \
|
||||
FmtStrProperty, OneOfProperty, NoneOr, EnumProperty, TupleProperty
|
||||
from secop.basic_validators import BoolProperty, EnumProperty, FloatProperty, \
|
||||
FmtStrProperty, IntProperty, NoneOr, NonNegativeFloatProperty, \
|
||||
NonNegativeIntProperty, OneOfProperty, PositiveFloatProperty, \
|
||||
PositiveIntProperty, StringProperty, TupleProperty, UnitProperty
|
||||
|
||||
|
||||
class unprintable:
|
||||
def __str__(self):
|
||||
|
@ -26,8 +26,9 @@
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
|
||||
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
||||
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
|
||||
IntRange, ProgrammingError, ScaledInteger, StatusType, \
|
||||
StringType, StructOf, TextType, TupleOf, get_datatype
|
||||
|
||||
|
||||
def copytest(dt):
|
||||
@ -359,6 +360,7 @@ def test_BoolType():
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
BoolType(unit='K')
|
||||
|
||||
|
||||
def test_ArrayOf():
|
||||
# test constructor catching illegal arguments
|
||||
with pytest.raises(ValueError):
|
||||
@ -478,6 +480,14 @@ def test_Command():
|
||||
'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():
|
||||
with pytest.raises(ValueError):
|
||||
get_datatype(1)
|
||||
|
@ -23,10 +23,11 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import FloatRange, IntRange, Property, StringType
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.iohandler import CmdParser, IOHandler
|
||||
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', [
|
||||
('%d,%d', '2,3', [2,3], None),
|
||||
@ -107,15 +108,11 @@ def test_IOHandler():
|
||||
|
||||
|
||||
class Module1(Module):
|
||||
properties = {
|
||||
'channel': Property('the channel', IntRange(), default=3),
|
||||
'loop': Property('the loop', IntRange(), default=2),
|
||||
}
|
||||
parameters = {
|
||||
'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),
|
||||
}
|
||||
channel = Property('the channel', IntRange(), default=3)
|
||||
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)
|
||||
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||
|
||||
def sendRecv(self, 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
|
||||
# pylint: disable=unused-variable
|
||||
class Module2(Module):
|
||||
parameters = {
|
||||
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
|
||||
}
|
||||
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
|
||||
|
@ -22,13 +22,14 @@
|
||||
# *****************************************************************************
|
||||
"""test data types."""
|
||||
|
||||
# no fixtures needed
|
||||
#import pytest
|
||||
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import BoolType, FloatRange, StringType
|
||||
from secop.errors import ProgrammingError
|
||||
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
|
||||
|
||||
|
||||
@ -64,30 +65,27 @@ def test_Communicator():
|
||||
assert event.is_set() # event should be set immediately
|
||||
|
||||
|
||||
def test_ModuleMeta():
|
||||
def test_ModuleMagic():
|
||||
class Newclass1(Drivable):
|
||||
parameters = {
|
||||
'pollinterval': Override(reorder=True),
|
||||
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
||||
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
|
||||
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
||||
}
|
||||
commands = {
|
||||
# intermixing parameters with commands is not recommended,
|
||||
# but acceptable for influencing the order
|
||||
'a1': Parameter('a1', datatype=BoolType(), default=False),
|
||||
'a2': Parameter('a2', datatype=BoolType(), default=True),
|
||||
'value': Override(datatype=StringType(), default='first'),
|
||||
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
|
||||
}
|
||||
param1 = Parameter('param1', datatype=BoolType(), default=False)
|
||||
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
|
||||
|
||||
@Command(argument=BoolType(), result=BoolType())
|
||||
def cmd(self, arg):
|
||||
"""stuff"""
|
||||
return not arg
|
||||
|
||||
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
||||
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
||||
value = Parameter(datatype=StringType(), default='first')
|
||||
|
||||
@Command(argument=BoolType(), result=BoolType())
|
||||
def cmd2(self, arg):
|
||||
"""another stuff"""
|
||||
return not arg
|
||||
|
||||
pollerClass = BasicPoller
|
||||
|
||||
def do_cmd(self, arg):
|
||||
return not arg
|
||||
|
||||
def do_cmd2(self, arg):
|
||||
return not arg
|
||||
|
||||
def read_param1(self):
|
||||
return True
|
||||
|
||||
@ -103,19 +101,31 @@ def test_ModuleMeta():
|
||||
def read_value(self):
|
||||
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
|
||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
||||
with pytest.raises(ProgrammingError):
|
||||
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']
|
||||
|
||||
class Newclass2(Newclass1):
|
||||
parameters = {
|
||||
'cmd2': Override('another stuff'),
|
||||
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
|
||||
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
|
||||
'b2': Parameter('<b2>', datatype=BoolType(), default=True,
|
||||
poll=True, readonly=False, initwrite=True),
|
||||
}
|
||||
paramOrder = 'param1', 'param2', 'cmd', 'value'
|
||||
|
||||
@Command(description='another stuff')
|
||||
def cmd2(self, arg):
|
||||
return arg
|
||||
|
||||
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):
|
||||
self._a1_written = value
|
||||
@ -128,14 +138,15 @@ def test_ModuleMeta():
|
||||
def read_value(self):
|
||||
return 0
|
||||
|
||||
sortcheck2 = ['value', 'status', 'target', 'pollinterval',
|
||||
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'a1', 'b2']
|
||||
# first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
|
||||
sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
|
||||
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
|
||||
|
||||
logger = LoggerStub()
|
||||
updates = {}
|
||||
srv = ServerStub(updates)
|
||||
|
||||
params_found = set() # set of instance accessibles
|
||||
params_found = set() # set of instance accessibles
|
||||
objects = []
|
||||
|
||||
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
|
||||
@ -143,16 +154,11 @@ def test_ModuleMeta():
|
||||
o2 = newclass('o2', logger, {'.description':''}, srv)
|
||||
for obj in [o1, o2]:
|
||||
objects.append(obj)
|
||||
ctr_found = set()
|
||||
for n, o in obj.accessibles.items():
|
||||
for o in obj.accessibles.values():
|
||||
# check that instance accessibles are unique objects
|
||||
assert o not in params_found
|
||||
params_found.add(o)
|
||||
assert o.ctr not in ctr_found
|
||||
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)
|
||||
assert list(obj.accessibles) == sortcheck
|
||||
|
||||
# check for inital updates working properly
|
||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||
@ -214,7 +220,7 @@ def test_ModuleMeta():
|
||||
assert acs is not None
|
||||
else: # do not check object or mixin
|
||||
acs = {}
|
||||
for n, o in acs.items():
|
||||
for o in acs.values():
|
||||
# check that class accessibles are not reused as instance accessibles
|
||||
assert o not in params_found
|
||||
|
||||
|
@ -23,8 +23,8 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.protocol.interface import encode_msg_frame, decode_msg
|
||||
import secop.protocol.messages as m
|
||||
from secop.protocol.interface import decode_msg, encode_msg_frame
|
||||
|
||||
# args are: msg tuple, msg bytes
|
||||
MSG = [
|
||||
|
@ -25,66 +25,78 @@
|
||||
# no fixtures needed
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import BoolType, IntRange
|
||||
from secop.params import Command, Override, Parameter, Parameters
|
||||
from secop.datatypes import BoolType, FloatRange, IntRange
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.modules import HasAccessibles
|
||||
from secop.params import Command, Parameter
|
||||
|
||||
|
||||
def test_Command():
|
||||
cmd = Command('do_something')
|
||||
assert cmd.description == 'do_something'
|
||||
assert cmd.ctr
|
||||
assert cmd.argument is None
|
||||
assert cmd.result is None
|
||||
assert cmd.for_export() == {'datainfo': {'type': 'command'},
|
||||
'description': 'do_something'}
|
||||
class Mod(HasAccessibles):
|
||||
@Command()
|
||||
def cmd(self):
|
||||
"""do something"""
|
||||
@Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
|
||||
def cmd2(self):
|
||||
pass
|
||||
|
||||
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1))
|
||||
assert cmd.description
|
||||
assert isinstance(cmd.argument, IntRange)
|
||||
assert isinstance(cmd.result, IntRange)
|
||||
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9},
|
||||
'result': {'type': 'int', 'min':-1, 'max':1}},
|
||||
'description': 'do_something'}
|
||||
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
|
||||
'result': {'type': 'int', 'max': 1, 'min': -1}},
|
||||
'description': 'do_something'}
|
||||
assert Mod.cmd.description == 'do something'
|
||||
assert Mod.cmd.argument is None
|
||||
assert Mod.cmd.result is None
|
||||
assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
|
||||
'description': 'do something'}
|
||||
|
||||
assert Mod.cmd2.description == 'do some other thing'
|
||||
assert isinstance(Mod.cmd2.argument, IntRange)
|
||||
assert isinstance(Mod.cmd2.result, IntRange)
|
||||
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():
|
||||
p1 = Parameter('description1', datatype=IntRange(), default=0)
|
||||
p2 = Parameter('description2', datatype=IntRange(), constant=1)
|
||||
assert p1 != p2
|
||||
assert p1.ctr != p2.ctr
|
||||
class Mod(HasAccessibles):
|
||||
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
|
||||
p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
|
||||
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):
|
||||
Parameter(None, datatype=float)
|
||||
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
|
||||
Parameter(None, datatype=float, inherit=False)
|
||||
|
||||
|
||||
def test_Override():
|
||||
p = Parameter('description1', datatype=BoolType, default=False)
|
||||
o = Override(default=True, reorder=True)
|
||||
assert o.ctr != p.ctr
|
||||
q = o.apply(p)
|
||||
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
|
||||
class Base(HasAccessibles):
|
||||
p1 = Parameter('description1', datatype=BoolType, default=False)
|
||||
p2 = Parameter('description1', datatype=BoolType, default=False)
|
||||
p3 = Parameter('description1', datatype=BoolType, default=False)
|
||||
|
||||
p2 = Parameter('description2', datatype=BoolType, default=False)
|
||||
o2 = Override(default=True)
|
||||
assert o2.ctr != p2.ctr
|
||||
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
|
||||
class Mod(Base):
|
||||
p1 = Parameter(default=True)
|
||||
p2 = Parameter() # override without change
|
||||
|
||||
def test_Parameters():
|
||||
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True)))
|
||||
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True)
|
||||
assert ps['_p2'].export == '_p2'
|
||||
assert Mod.p1 != Base.p1
|
||||
assert Mod.p2 != Base.p2
|
||||
assert Mod.p3 == Base.p3
|
||||
|
||||
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'
|
||||
|
@ -22,8 +22,8 @@
|
||||
"""test data types."""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from ast import literal_eval
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -23,9 +23,12 @@
|
||||
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.modules import Drivable
|
||||
from secop.poller import Poller, REGULAR, DYNAMIC, SLOW
|
||||
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
|
||||
|
||||
Status = Drivable.Status
|
||||
|
||||
class Time:
|
||||
|
@ -23,39 +23,59 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
||||
from secop.errors import ProgrammingError, ConfigError
|
||||
from secop.properties import Property, Properties, HasProperties
|
||||
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
|
||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||
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 = [
|
||||
[(StringType(), 'default', 'extname', False, False),
|
||||
dict(default='default', extname='extname', export=True, mandatory=False)],
|
||||
[(IntRange(), '42', '_extname', False, True),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
||||
[(IntRange(), '42', '_extname', True, False),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=False)],
|
||||
[(IntRange(), 42, '_extname', True, True),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
||||
[(IntRange(), 0, '', True, True),
|
||||
dict(default=0, extname='', export=True, mandatory=True)],
|
||||
[(IntRange(), 0, '', True, False),
|
||||
dict(default=0, extname='', export=True, mandatory=False)],
|
||||
[(IntRange(), 0, '', False, True),
|
||||
dict(default=0, extname='', export=False, mandatory=True)],
|
||||
[(IntRange(), 0, '', False, False),
|
||||
dict(default=0, extname='', export=False, mandatory=False)],
|
||||
[(IntRange(), None, '', None),
|
||||
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory
|
||||
[(ValueType(), 1, '', False),
|
||||
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory
|
||||
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
||||
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||
],
|
||||
[Prop(IntRange(), '42', export=True, name='name'),
|
||||
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||
dict(default=42, extname='_extname', export=True, mandatory=True)
|
||||
],
|
||||
[Prop(IntRange(), 0, export=True, mandatory=True),
|
||||
dict(default=0, extname='', export=True, mandatory=True)
|
||||
],
|
||||
[Prop(IntRange(), 0, export=True, mandatory=False),
|
||||
dict(default=0, extname='', export=True, mandatory=False)
|
||||
],
|
||||
[Prop(IntRange(), 0, export=False, mandatory=True),
|
||||
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)
|
||||
def test_Property(args, check):
|
||||
p = Property('', *args)
|
||||
@pytest.mark.parametrize('propargs, check', V_test_Property)
|
||||
def test_Property(propargs, check):
|
||||
name, args, kwds = propargs
|
||||
p = Property('', *args, **kwds)
|
||||
if name:
|
||||
p.__set_name__(None, name)
|
||||
result = {k: getattr(p, k) for k in check}
|
||||
assert result == check
|
||||
|
||||
|
||||
def test_Property_basic():
|
||||
with pytest.raises(TypeError):
|
||||
# pylint: disable=no-value-for-parameter
|
||||
@ -67,47 +87,47 @@ def test_Property_basic():
|
||||
Property('', 1)
|
||||
Property('', IntRange(), '42', 'extname', False, False)
|
||||
|
||||
|
||||
def test_Properties():
|
||||
p = Properties()
|
||||
with pytest.raises(ProgrammingError):
|
||||
p[1] = 2
|
||||
p['a'] = Property('', IntRange(), '42', export=True)
|
||||
assert p['a'].default == 42
|
||||
assert p['a'].export is True
|
||||
assert p['a'].extname == '_a'
|
||||
with pytest.raises(ProgrammingError):
|
||||
p['a'] = 137
|
||||
with pytest.raises(ProgrammingError):
|
||||
del p[1]
|
||||
with pytest.raises(ProgrammingError):
|
||||
del p['a']
|
||||
p['a'] = Property('', IntRange(), 0, export=False)
|
||||
assert p['a'].default == 0
|
||||
assert p['a'].export is False
|
||||
assert p['a'].extname == ''
|
||||
class Cls(HasProperties):
|
||||
aa = Property('', IntRange(0, 99), '42', export=True)
|
||||
bb = Property('', IntRange(), 0, export=False)
|
||||
|
||||
assert Cls.aa.default == 42
|
||||
assert Cls.aa.export is True
|
||||
assert Cls.aa.extname == '_aa'
|
||||
|
||||
cc = Cls()
|
||||
with pytest.raises(BadValueError):
|
||||
cc.aa = 137
|
||||
|
||||
assert Cls.bb.default == 0
|
||||
assert Cls.bb.export is False
|
||||
assert Cls.bb.extname == ''
|
||||
|
||||
|
||||
class c(HasProperties):
|
||||
properties = {
|
||||
'a' : Property('', IntRange(), 1),
|
||||
}
|
||||
# properties
|
||||
a = Property('', IntRange(), 1)
|
||||
|
||||
|
||||
class cl(c):
|
||||
properties = {
|
||||
'a' : Property('', IntRange(), 3),
|
||||
'b' : Property('', FloatRange(), 3.14),
|
||||
'minabc': Property('', IntRange(), 8),
|
||||
'maxabc': Property('', IntRange(), 9),
|
||||
'minx': Property('', IntRange(), 2),
|
||||
'maxy': Property('', IntRange(), 1),
|
||||
}
|
||||
# properties
|
||||
a = Property('', IntRange(), 3)
|
||||
b = Property('', FloatRange(), 3.14)
|
||||
minabc = Property('', IntRange(), 8)
|
||||
maxabc = Property('', IntRange(), 9)
|
||||
minx = Property('', IntRange(), 2)
|
||||
maxy = Property('', IntRange(), 1)
|
||||
|
||||
|
||||
def test_HasProperties():
|
||||
o = c()
|
||||
assert o.properties['a'] == 1
|
||||
assert o.a == 1
|
||||
o = cl()
|
||||
assert o.properties['a'] == 3
|
||||
assert o.properties['b'] == 3.14
|
||||
assert o.a == 3
|
||||
assert o.b == 3.14
|
||||
|
||||
|
||||
def test_Property_checks():
|
||||
o = c()
|
||||
@ -119,6 +139,7 @@ def test_Property_checks():
|
||||
with pytest.raises(ConfigError):
|
||||
o.checkProperties()
|
||||
|
||||
|
||||
def test_Property_override():
|
||||
o1 = c()
|
||||
class co(c):
|
||||
@ -131,10 +152,10 @@ def test_Property_override():
|
||||
class cx(c): # pylint: disable=unused-variable
|
||||
def a(self):
|
||||
pass
|
||||
assert 'collides with method' in str(e.value)
|
||||
assert 'collides with' in str(e.value)
|
||||
|
||||
with pytest.raises(ProgrammingError) as e:
|
||||
class cz(c): # pylint: disable=unused-variable
|
||||
a = 's'
|
||||
|
||||
assert 'can not be set to' in str(e.value)
|
||||
assert 'can not set' in str(e.value)
|
||||
|
Loading…
x
Reference in New Issue
Block a user