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
|
div.wy-nav-content
|
||||||
{
|
{
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* this is for the alabaser theme */
|
|
||||||
div.body {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0 0 0 26%;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar {
|
|
||||||
width: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre, tt, code {
|
|
||||||
font-size: 0.75em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 875px) {
|
|
||||||
div.bodywrapper {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
div.sphinxsidebar {
|
|
||||||
width: 102.5%;
|
|
||||||
}
|
|
||||||
div.document {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* make nested bullet lists nicer (ales too much space above inner nested list) */
|
|
||||||
.rst-content .section ul li ul {
|
|
||||||
margin-top: 0px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* make some bullet lists more dense (this rule exists in theme.css, but not important)*/
|
/* make some bullet lists more dense (this rule exists in theme.css, but not important)*/
|
||||||
.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child {
|
.wy-plain-list-disc li p:last-child, .rst-content .section ul li p:last-child, .rst-content .toctree-wrapper ul li p:last-child, article ul li p:last-child {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* overwrite custom font (to save bandwidth not using a custom font) */
|
||||||
|
body {
|
||||||
|
font-family: "proxima-nova", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
|
||||||
|
font-family: "ff-tisa-web-pro", "Georgia", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,19 +100,8 @@ default_role = 'any'
|
|||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
if False: # alabaster
|
import sphinx_rtd_theme
|
||||||
html_theme = 'alabaster'
|
html_theme = 'sphinx_rtd_theme'
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
|
||||||
# further. For a list of options available for each theme, see the
|
|
||||||
# documentation.
|
|
||||||
#
|
|
||||||
html_theme_options = {
|
|
||||||
'page_width': '100%',
|
|
||||||
'fixed_sidebar': True,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
import sphinx_rtd_theme
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
|
|
||||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||||
# bottom, using the given strftime format.
|
# bottom, using the given strftime format.
|
||||||
@ -223,3 +212,8 @@ epub_exclude_files = ['search.html']
|
|||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
intersphinx_mapping = {'https://docs.python.org/': None}
|
intersphinx_mapping = {'https://docs.python.org/': None}
|
||||||
|
|
||||||
|
from secop.lib.classdoc import class_doc_handler
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.connect('autodoc-process-docstring', class_doc_handler)
|
@ -5,10 +5,10 @@ Module Base Classes
|
|||||||
...................
|
...................
|
||||||
|
|
||||||
.. autoclass:: secop.modules.Module
|
.. autoclass:: secop.modules.Module
|
||||||
:members: earlyInit, initModule, startModule
|
:members: earlyInit, initModule, startModule, pollerClass
|
||||||
|
|
||||||
.. autoclass:: secop.modules.Readable
|
.. autoclass:: secop.modules.Readable
|
||||||
:members: pollerClass, Status
|
:members: Status
|
||||||
|
|
||||||
.. autoclass:: secop.modules.Writable
|
.. autoclass:: secop.modules.Writable
|
||||||
|
|
||||||
@ -21,13 +21,11 @@ Parameters, Commands and Properties
|
|||||||
|
|
||||||
.. autoclass:: secop.params.Parameter
|
.. autoclass:: secop.params.Parameter
|
||||||
.. autoclass:: secop.params.Command
|
.. autoclass:: secop.params.Command
|
||||||
.. autoclass:: secop.params.Override
|
|
||||||
.. autoclass:: secop.properties.Property
|
.. autoclass:: secop.properties.Property
|
||||||
.. autoclass:: secop.modules.Attached
|
.. autoclass:: secop.modules.Attached
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Datatypes
|
Datatypes
|
||||||
.........
|
.........
|
||||||
|
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
PSI (SINQ)
|
PSI (SINQ)
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
CCU4 tutorial example
|
||||||
|
.....................
|
||||||
|
|
||||||
|
.. automodule:: secop_psi.ccu4
|
||||||
|
:show-inheritance:
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
PPMS
|
PPMS
|
||||||
....
|
....
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
Tutorial
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
tutorial_helevel
|
|
@ -3,12 +3,13 @@ HeLevel - a Simple Driver
|
|||||||
|
|
||||||
Coding the Driver
|
Coding the Driver
|
||||||
-----------------
|
-----------------
|
||||||
For this tutorial we choose as an example a cryostat. Let us start with the helium level meter,
|
For this tutorial we choose as an example a cryostat. Let us start with the helium level
|
||||||
as this is the simplest module.
|
meter, as this is the simplest module.
|
||||||
As mentioned in the introduction, we have to code the access to the hardware (driver), and the Frappy
|
As mentioned in the introduction, we have to code the access to the hardware (driver),
|
||||||
framework will deal with the SECoP interface. The code for the driver is located in a subdirectory
|
and the Frappy framework will deal with the SECoP interface. The code for the driver is
|
||||||
named after the facility or institute programming the driver in our case *secop_psi*.
|
located in a subdirectory named after the facility or institute programming the driver
|
||||||
We create a file named from the electronic device CCU4 we use here for the He level reading.
|
in our case *secop_psi*. We create a file named from the electronic device CCU4 we use
|
||||||
|
here for the He level reading.
|
||||||
|
|
||||||
CCU4 luckily has a very simple and logical protocol:
|
CCU4 luckily has a very simple and logical protocol:
|
||||||
|
|
||||||
@ -20,9 +21,8 @@ CCU4 luckily has a very simple and logical protocol:
|
|||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
# the most common classes can be imported from secop.core
|
# the most common Frappy classes can be imported from secop.core
|
||||||
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, \
|
from secop.core import Readable, Parameter, FloatRange, BoolType, StringIO, HasIodev
|
||||||
StringIO, HasIodev
|
|
||||||
|
|
||||||
|
|
||||||
class CCU4IO(StringIO):
|
class CCU4IO(StringIO):
|
||||||
@ -33,30 +33,48 @@ CCU4 luckily has a very simple and logical protocol:
|
|||||||
identification = [('cid', r'CCU4.*')]
|
identification = [('cid', r'CCU4.*')]
|
||||||
|
|
||||||
|
|
||||||
# inheriting the HasIodev mixin creates us the things needed for talking
|
# inheriting the HasIodev mixin creates us a private attribute *_iodev*
|
||||||
# with a device by means of the sendRecv method
|
# for talking with the hardware
|
||||||
# Readable as a base class defines the value and status parameters
|
# Readable as a base class defines the value and status parameters
|
||||||
class HeLevel(HasIodev, Readable):
|
class HeLevel(HasIodev, Readable):
|
||||||
"""He Level channel of CCU4"""
|
"""He Level channel of CCU4"""
|
||||||
|
|
||||||
# define or alter the parameters
|
|
||||||
parameters = {
|
|
||||||
# we are changing the 'unit' parameter property of the inherited 'value'
|
|
||||||
# parameter, therefore 'Override'
|
|
||||||
'value': Override(unit='%'),
|
|
||||||
}
|
|
||||||
# define the communication class to create the IO module
|
# define the communication class to create the IO module
|
||||||
iodevClass = CCU4IO
|
iodevClass = CCU4IO
|
||||||
|
|
||||||
|
# define or alter the parameters
|
||||||
|
# as Readable.value exists already, we give only the modified property 'unit'
|
||||||
|
value = Parameter(unit='%')
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
# method for reading the main value
|
# method for reading the main value
|
||||||
reply = self.sendRecv('h') # send 'h\n' and get the reply 'h=<value>\n'
|
reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=<value>\n'
|
||||||
name, txtvalue = reply.split('=')
|
name, txtvalue = reply.split('=')
|
||||||
assert name == 'h' # check that we got a reply to our command
|
assert name == 'h' # check that we got a reply to our command
|
||||||
return txtvalue # the framework will automatically convert the string to a float
|
return txtvalue # the framework will automatically convert the string to a float
|
||||||
|
|
||||||
The class :class:`CCU4`, an extension of (:class:`secop.stringio.StringIO`) serves as
|
|
||||||
communication class.
|
The class :class:`secop_psi.ccu4.CCU4IO`, an extension of (:class:`secop.stringio.StringIO`)
|
||||||
|
serves as communication class.
|
||||||
|
|
||||||
|
:Note:
|
||||||
|
|
||||||
|
You might wonder why the parameter *value* is declared here as class attribute.
|
||||||
|
In Python, usually class attributes are used to set a default value which might
|
||||||
|
be overwritten in a method. But class attributes can do more, look for Python
|
||||||
|
descriptors or properties if you are interested in details.
|
||||||
|
In Frappy, the *Parameter* class is a descriptor, which does the magic needed for
|
||||||
|
the SECoP interface. Given ``lev`` as an instance of the class ``HeLevel`` above,
|
||||||
|
``lev.value`` will just return its internal cached value.
|
||||||
|
``lev.value = 85.3`` will try to convert to the data type of the parameter,
|
||||||
|
put it to the internal cache and send a messages to the SECoP clients telling
|
||||||
|
that ``lev.value`` has got a new value.
|
||||||
|
For getting a value from the hardware, you have to call ``lev.read_value()``.
|
||||||
|
Frappy has replaced your version of *read_value* with a wrapped one which
|
||||||
|
also takes care to announce the change to the clients.
|
||||||
|
Even when you did not code this method, Frappy adds it silently, so calling
|
||||||
|
``<module>.read_<parameter>`` will be possible for all parameters declared
|
||||||
|
in a module.
|
||||||
|
|
||||||
Above is already the code for a very simple working He Level meter driver. For a next step,
|
Above is already the code for a very simple working He Level meter driver. For a next step,
|
||||||
we want to improve it:
|
we want to improve it:
|
||||||
@ -66,32 +84,26 @@ we want to improve it:
|
|||||||
* We want to be able to switch the Level Monitor to fast reading before we start to fill.
|
* We want to be able to switch the Level Monitor to fast reading before we start to fill.
|
||||||
|
|
||||||
Let us start to code these additions. We do not need to declare the status parameter,
|
Let us start to code these additions. We do not need to declare the status parameter,
|
||||||
as it is inherited from *Readable*. But we declare the new parameters *empty*, *full* and *fast*,
|
as it is inherited from *Readable*. But we declare the new parameters *empty_length*,
|
||||||
and we have to code the communication and convert the status codes from the hardware to
|
*full_length* and *sample_rate*, and we have to code the communication and convert
|
||||||
the standard SECoP status codes.
|
the status codes from the hardware to the standard SECoP status codes.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
...
|
...
|
||||||
# define or alter the parameters
|
# the first two arguments to Parameter are 'description' and 'datatype'
|
||||||
parameters = {
|
# it is highly recommended to define always the physical unit
|
||||||
|
empty_length = Parameter('warm length when empty', FloatRange(0, 2000, unit='mm'),
|
||||||
...
|
readonly=False)
|
||||||
|
full_length = Parameter('warm length when full', FloatRange(0, 2000, unit='mm'),
|
||||||
# the first two arguments to Parameter are 'description' and 'datatype'
|
readonly=False)
|
||||||
# it is highly recommended to define always the physical unit
|
sample_rate = Parameter('sample rate', EnumType(slow=0, fast=1), readonly=False)
|
||||||
'empty': Parameter('warm length when empty', FloatRange(0, 2000),
|
|
||||||
readonly=False, unit='mm'),
|
|
||||||
'full': Parameter('warm length when full', FloatRange(0, 2000),
|
|
||||||
readonly=False, unit='mm'),
|
|
||||||
'fast': Parameter('fast reading', BoolType(),
|
|
||||||
readonly=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
Status = Readable.Status
|
Status = Readable.Status
|
||||||
|
|
||||||
|
# conversion of the code from the CCU4 parameter 'hsf'
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
0: (Status.IDLE, 'sensor ok'),
|
0: (Status.IDLE, 'sensor ok'),
|
||||||
1: (Status.ERROR, 'sensor warm'),
|
1: (Status.ERROR, 'sensor warm'),
|
||||||
@ -102,69 +114,98 @@ the standard SECoP status codes.
|
|||||||
}
|
}
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
name, txtvalue = self.sendRecv('hsf').split('=')
|
name, txtvalue = self._iodev.communicate('hsf').split('=')
|
||||||
assert name == 'hsf'
|
assert name == 'hsf'
|
||||||
return self.STATUS_MAP(int(txtvalue))
|
return self.STATUS_MAP(int(txtvalue))
|
||||||
|
|
||||||
def read_emtpy(self):
|
def read_empty_length(self):
|
||||||
name, txtvalue = self.sendRecv('hem').split('=')
|
name, txtvalue = self._iodev.communicate('hem').split('=')
|
||||||
assert name == 'hem'
|
assert name == 'hem'
|
||||||
return txtvalue
|
return txtvalue
|
||||||
|
|
||||||
def write_empty(self, value):
|
def write_empty_length(self, value):
|
||||||
name, txtvalue = self.sendRecv('hem=%g' % value).split('=')
|
name, txtvalue = self._iodev.communicate('hem=%g' % value).split('=')
|
||||||
assert name == 'hem'
|
assert name == 'hem'
|
||||||
return txtvalue
|
return txtvalue
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
Here we start to realize, that we will repeat similar code for other parameters, which means it might be
|
|
||||||
worth to create our own *_sendRecv* method, and then the *read_<param>* and *write_<param>* methods
|
Here we start to realize, that we will repeat similar code for other parameters,
|
||||||
will become shorter:
|
which means it might be worth to create a *query* method, and then the
|
||||||
|
*read_<param>* and *write_<param>* methods will become shorter:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
def _sendRecv(self, cmd):
|
class HeLevel(Readable):
|
||||||
# method may be used for reading and writing parameters
|
|
||||||
name, txtvalue = self.sendRecv(cmd).split('=')
|
|
||||||
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
|
||||||
return txtvalue # the framework will automatically convert the string to a float
|
|
||||||
|
|
||||||
def read_value(self):
|
|
||||||
return self._sendRecv('h')
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
def read_status(self):
|
|
||||||
return self.STATUS_MAP(int(self._sendRecv('hsf')))
|
|
||||||
|
|
||||||
def read_empty(self):
|
...
|
||||||
return self._sendRecv('hem')
|
|
||||||
|
|
||||||
def write_empty(self, value):
|
def query(self, cmd):
|
||||||
return self._sendRecv('hem=%g' % value)
|
"""send a query and get the response
|
||||||
|
|
||||||
def read_full(self):
|
:param cmd: the name of the parameter to query or '<parameter>=<value'
|
||||||
return self._sendRecv('hfu')
|
for changing a parameter
|
||||||
|
:returns: the (new) value of the parameter
|
||||||
def write_full(self, value):
|
"""
|
||||||
return self._sendRecv('hfu=%g' % value)
|
name, txtvalue = self._iodev.communicate(cmd).split('=')
|
||||||
|
assert name == cmd.split('=')[0] # check that we got a reply to our command
|
||||||
def read_fast(self):
|
return txtvalue # Frappy will automatically convert the string to the needed data type
|
||||||
return self._sendRecv('hf')
|
|
||||||
|
def read_value(self):
|
||||||
def write_fast(self, value):
|
return self.query('h')
|
||||||
return self._sendRecv('hf=%s' % value)
|
|
||||||
|
def read_status(self):
|
||||||
|
return self.STATUS_MAP[int(self.query('hsf'))]
|
||||||
|
|
||||||
|
def read_empty_length(self):
|
||||||
|
return self.query('hem')
|
||||||
|
|
||||||
|
def write_empty_length(self, value):
|
||||||
|
return self.query('hem=%g' % value)
|
||||||
|
|
||||||
|
def read_full_length(self):
|
||||||
|
return self.query('hfu')
|
||||||
|
|
||||||
|
def write_full_length(self, value):
|
||||||
|
return self.query('hfu=%g' % value)
|
||||||
|
|
||||||
|
def read_sample_rate(self):
|
||||||
|
return self.query('hf')
|
||||||
|
|
||||||
|
def write_sample_rate(self, value):
|
||||||
|
return self.query('hf=%d' % value)
|
||||||
|
|
||||||
|
|
||||||
|
:Note:
|
||||||
|
|
||||||
|
It make sense to unify *empty_length* and *full_length* to one parameter *calibration*,
|
||||||
|
as a :class:`secop.datatypes.StructOf` with members *empty_length* and *full_length*:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
calibration = Parameter(
|
||||||
|
'sensor calibration',
|
||||||
|
StructOf(empty_length=FloatRange(0, 2000, unit='mm'),
|
||||||
|
full_length=FloatRange(0, 2000, unit='mm')),
|
||||||
|
readonly=False)
|
||||||
|
|
||||||
|
For simplicity we stay with two float parameters for this tutorial.
|
||||||
|
|
||||||
|
|
||||||
|
The full documentation of the example can be found here: :class:`secop_psi.ccu4.HeLevel`
|
||||||
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
Before we continue coding, we may try out what we have coded and create a configuration file.
|
Before we continue coding, we may try out what we have coded and create a configuration file.
|
||||||
The directory tree of the Frappy framework contains the code for all drivers, but the configuration
|
The directory tree of the Frappy framework contains the code for all drivers, but the
|
||||||
file determines, which code will finally be loaded. We choose the name *example_cryo*
|
configuration file determines, which code will be loaded when a server is started.
|
||||||
and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdirectory:
|
We choose the name *example_cryo* and create therefore a configuration file
|
||||||
|
*example_cryo.cfg* in the *cfg* subdirectory:
|
||||||
|
|
||||||
``cfg/example_cryo.cfg``:
|
``cfg/example_cryo.cfg``:
|
||||||
|
|
||||||
@ -172,7 +213,7 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
|
|||||||
|
|
||||||
[NODE]
|
[NODE]
|
||||||
description = this is an example cryostat for the Frappy tutorial
|
description = this is an example cryostat for the Frappy tutorial
|
||||||
id = example_cryo.sampleenvironment.org
|
id = example_cryo.psi.ch
|
||||||
|
|
||||||
[INTERFACE]
|
[INTERFACE]
|
||||||
uri = tcp://5000
|
uri = tcp://5000
|
||||||
@ -181,28 +222,29 @@ and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdir
|
|||||||
description = He level of the cryostat He reservoir
|
description = He level of the cryostat He reservoir
|
||||||
class = secop_psi.ccu4.HeLevel
|
class = secop_psi.ccu4.HeLevel
|
||||||
uri = linse-moxa-4.psi.ch:3001
|
uri = linse-moxa-4.psi.ch:3001
|
||||||
empty = 380
|
empty_length = 380
|
||||||
full = 0
|
full_length = 0
|
||||||
|
|
||||||
A configuration file contains several sections with a header encloded by rectangular brackets.
|
A configuration file contains several sections with a header enclosed by rectangular brackets.
|
||||||
|
|
||||||
The *NODE* section describes the main properties of the SEC Node: a description of the node and
|
The *NODE* section describes the main properties of the SEC Node: a description of the node
|
||||||
an id, which should be globally unique.
|
and an id, which should be globally unique.
|
||||||
|
|
||||||
The *INTERFACE* section defines the address of the server, usually the only important value here
|
The *INTERFACE* section defines the address of the server, usually the only important value
|
||||||
is the TCP port under which the server will be accessible. Currently only tcp is supported.
|
here is the TCP port under which the server will be accessible. Currently only tcp is
|
||||||
|
supported.
|
||||||
|
|
||||||
All the other sections define the SECoP modules to be used. A module section at least contains a
|
All the other sections define the SECoP modules to be used. A module section at least contains a
|
||||||
human readable *description*, and the Python *class* used. Other properties or parameter values may
|
human readable *description*, and the Python *class* used. Other properties or parameter values may
|
||||||
follow, in this case the *uri* for the communication with the He level monitor and the values for
|
follow, in this case the *uri* for the communication with the He level monitor and the values for
|
||||||
configuring the He Level sensor. We might also alter parameter properties, for example we may hide
|
configuring the He Level sensor. We might also alter parameter properties, for example we may hide
|
||||||
the parameters *empty* and *full* from the client by defining:
|
the parameters *empty_length* and *full_length* from the client by defining:
|
||||||
|
|
||||||
.. code:: ini
|
.. code:: ini
|
||||||
|
|
||||||
empty.export = False
|
empty_length.export = False
|
||||||
full.export = False
|
full_length.export = False
|
||||||
|
|
||||||
However, we do not do this here, as it is nice to try out chaning parameters for a test!
|
However, we do not put this here, as it is nice to try out changing parameters for a test!
|
||||||
|
|
||||||
**name** *(x)*
|
*to be continued*
|
@ -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"""
|
"""general SECoP client"""
|
||||||
|
|
||||||
import time
|
|
||||||
import queue
|
|
||||||
import json
|
import json
|
||||||
from threading import Event, RLock, current_thread
|
import queue
|
||||||
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from threading import Event, RLock, current_thread
|
||||||
|
|
||||||
from secop.lib import mkthread, formatExtendedTraceback, formatExtendedStack
|
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
|
||||||
from secop.datatypes import get_datatype
|
|
||||||
from secop.protocol.interface import encode_msg_frame, decode_msg
|
|
||||||
from secop.protocol.messages import REQUEST2REPLY, ERRORPREFIX, EVENTREPLY, WRITEREQUEST, WRITEREPLY, \
|
|
||||||
READREQUEST, READREPLY, IDENTREQUEST, IDENTPREFIX, ENABLEEVENTSREQUEST, COMMANDREQUEST, \
|
|
||||||
DESCRIPTIONREQUEST, HEARTBEATREQUEST
|
|
||||||
import secop.errors
|
import secop.errors
|
||||||
import secop.params
|
import secop.params
|
||||||
|
from secop.datatypes import get_datatype
|
||||||
|
from secop.lib import mkthread
|
||||||
|
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
|
from secop.protocol.interface import decode_msg, encode_msg_frame
|
||||||
|
from secop.protocol.messages import COMMANDREQUEST, \
|
||||||
|
DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST, ERRORPREFIX, \
|
||||||
|
EVENTREPLY, HEARTBEATREQUEST, IDENTPREFIX, IDENTREQUEST, \
|
||||||
|
READREPLY, READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST
|
||||||
|
|
||||||
# replies to be handled for cache
|
# replies to be handled for cache
|
||||||
UPDATE_MESSAGES = {EVENTREPLY, READREPLY, WRITEREPLY, ERRORPREFIX + READREQUEST, ERRORPREFIX + EVENTREPLY}
|
UPDATE_MESSAGES = {EVENTREPLY, READREPLY, WRITEREPLY, ERRORPREFIX + READREQUEST, ERRORPREFIX + EVENTREPLY}
|
||||||
@ -160,7 +161,6 @@ class ProxyClient:
|
|||||||
if not cblist:
|
if not cblist:
|
||||||
self.callbacks[cbname].pop(key)
|
self.callbacks[cbname].pop(key)
|
||||||
|
|
||||||
|
|
||||||
def callback(self, key, cbname, *args):
|
def callback(self, key, cbname, *args):
|
||||||
"""perform callbacks
|
"""perform callbacks
|
||||||
|
|
||||||
|
@ -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'
|
# allow to import the most important classes from 'secop'
|
||||||
|
|
||||||
# pylint: disable=unused-import
|
# pylint: disable=unused-import
|
||||||
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||||
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
|
FloatRange, IntRange, ScaledInteger, StringType, StructOf, TupleOf
|
||||||
from secop.lib.enum import Enum
|
|
||||||
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
|
|
||||||
from secop.properties import Property
|
|
||||||
from secop.params import Parameter, Command, Override
|
|
||||||
from secop.metaclass import Done
|
|
||||||
from secop.iohandler import IOHandler, IOHandlerBase
|
from secop.iohandler import IOHandler, IOHandlerBase
|
||||||
from secop.stringio import StringIO, HasIodev
|
from secop.lib.enum import Enum
|
||||||
from secop.proxy import SecNode, Proxy, proxy_class
|
from secop.modules import Attached, Communicator, \
|
||||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
Done, Drivable, Module, Readable, Writable
|
||||||
|
from secop.params import Command, Parameter
|
||||||
|
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
||||||
|
from secop.properties import Property
|
||||||
|
from secop.proxy import Proxy, SecNode, proxy_class
|
||||||
|
from secop.stringio import HasIodev, StringIO
|
||||||
|
@ -28,13 +28,13 @@
|
|||||||
import sys
|
import sys
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
from secop.errors import ProgrammingError, ProtocolError, BadValueError, ConfigError
|
from secop.errors import BadValueError, \
|
||||||
|
ConfigError, ProgrammingError, ProtocolError
|
||||||
from secop.lib import clamp
|
from secop.lib import clamp
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.parse import Parser
|
from secop.parse import Parser
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
|
||||||
|
|
||||||
# Only export these classes for 'from secop.datatypes import *'
|
# Only export these classes for 'from secop.datatypes import *'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'DataType', 'get_datatype',
|
'DataType', 'get_datatype',
|
||||||
@ -53,6 +53,7 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
|
|||||||
Parser = Parser()
|
Parser = Parser()
|
||||||
|
|
||||||
|
|
||||||
|
# base class for all DataTypes
|
||||||
class DataType(HasProperties):
|
class DataType(HasProperties):
|
||||||
"""base class for all data types"""
|
"""base class for all data types"""
|
||||||
IS_COMMAND = False
|
IS_COMMAND = False
|
||||||
@ -97,7 +98,7 @@ class DataType(HasProperties):
|
|||||||
def set_properties(self, **kwds):
|
def set_properties(self, **kwds):
|
||||||
"""init datatype properties"""
|
"""init datatype properties"""
|
||||||
try:
|
try:
|
||||||
for k,v in kwds.items():
|
for k, v in kwds.items():
|
||||||
self.setProperty(k, v)
|
self.setProperty(k, v)
|
||||||
self.checkProperties()
|
self.checkProperties()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -126,10 +127,6 @@ class DataType(HasProperties):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
"""short description for automatic extension of doc strings"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Stub(DataType):
|
class Stub(DataType):
|
||||||
"""incomplete datatype, to be replaced with a proper one later during module load
|
"""incomplete datatype, to be replaced with a proper one later during module load
|
||||||
@ -154,42 +151,35 @@ class Stub(DataType):
|
|||||||
"""
|
"""
|
||||||
for dtcls in globals().values():
|
for dtcls in globals().values():
|
||||||
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
||||||
for prop in dtcls.properties.values():
|
for prop in dtcls.propertyDict.values():
|
||||||
stub = prop.datatype
|
stub = prop.datatype
|
||||||
if isinstance(stub, cls):
|
if isinstance(stub, cls):
|
||||||
prop.datatype = globals()[stub.name](*stub.args)
|
prop.datatype = globals()[stub.name](*stub.args)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return self.name.replace('Type', '').replace('Range', '').lower()
|
|
||||||
|
|
||||||
|
|
||||||
# SECoP types:
|
# SECoP types:
|
||||||
|
|
||||||
|
|
||||||
class FloatRange(DataType):
|
class FloatRange(DataType):
|
||||||
"""(restricted) float type
|
"""(restricted) float type
|
||||||
|
|
||||||
:param minval: (property **min**)
|
:param minval: (property **min**)
|
||||||
:param maxval: (property **max**)
|
:param maxval: (property **max**)
|
||||||
:param properties: any of the properties below
|
:param kwds: any of the properties below
|
||||||
"""
|
"""
|
||||||
|
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
|
||||||
|
max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
|
||||||
|
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||||
|
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||||
|
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
|
||||||
|
extname='absolute_resolution', default=0.0)
|
||||||
|
relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
|
||||||
|
extname='relative_resolution', default=1.2e-7)
|
||||||
|
|
||||||
properties = {
|
def __init__(self, minval=None, maxval=None, **kwds):
|
||||||
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
|
|
||||||
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
|
|
||||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
|
||||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
|
||||||
'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0),
|
|
||||||
extname='absolute_resolution', default=0.0),
|
|
||||||
'relative_resolution': Property('relative resolution', Stub('FloatRange', 0),
|
|
||||||
extname='relative_resolution', default=1.2e-7),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None, **properties):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
properties['min'] = minval if minval is not None else -sys.float_info.max
|
kwds['min'] = minval if minval is not None else -sys.float_info.max
|
||||||
properties['max'] = maxval if maxval is not None else sys.float_info.max
|
kwds['max'] = maxval if maxval is not None else sys.float_info.max
|
||||||
self.set_properties(**properties)
|
self.set_properties(**kwds)
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||||
@ -213,7 +203,7 @@ class FloatRange(DataType):
|
|||||||
if self.min - prec <= value <= self.max + prec:
|
if self.min - prec <= value <= self.max + prec:
|
||||||
return min(max(value, self.min), self.max)
|
return min(max(value, self.min), self.max)
|
||||||
raise BadValueError('%.14g should be a float between %.14g and %.14g' %
|
raise BadValueError('%.14g should be a float between %.14g and %.14g' %
|
||||||
(value, self.min, self.max))
|
(value, self.min, self.max))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
hints = self.get_info()
|
hints = self.get_info()
|
||||||
@ -221,7 +211,7 @@ class FloatRange(DataType):
|
|||||||
hints['minval'] = hints.pop('min')
|
hints['minval'] = hints.pop('min')
|
||||||
if 'max' in hints:
|
if 'max' in hints:
|
||||||
hints['maxval'] = hints.pop('max')
|
hints['maxval'] = hints.pop('max')
|
||||||
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k,v) for k,v in hints.items()))
|
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k, v) for k, v in hints.items()))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -249,9 +239,6 @@ class FloatRange(DataType):
|
|||||||
other(max(sys.float_info.min, self.min))
|
other(max(sys.float_info.min, self.min))
|
||||||
other(min(sys.float_info.max, self.max))
|
other(min(sys.float_info.max, self.max))
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'float'
|
|
||||||
|
|
||||||
|
|
||||||
class IntRange(DataType):
|
class IntRange(DataType):
|
||||||
"""restricted int type
|
"""restricted int type
|
||||||
@ -259,12 +246,10 @@ class IntRange(DataType):
|
|||||||
:param minval: (property **min**)
|
:param minval: (property **min**)
|
||||||
:param maxval: (property **max**)
|
:param maxval: (property **max**)
|
||||||
"""
|
"""
|
||||||
properties = {
|
min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True)
|
||||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True),
|
max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True)
|
||||||
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
|
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
||||||
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
# unit = Property('physical unit', StringType(), extname='unit', default='')
|
||||||
# 'unit': Property('physical unit', StringType(), extname='unit', default=''),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None):
|
def __init__(self, minval=None, maxval=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -290,7 +275,12 @@ class IntRange(DataType):
|
|||||||
raise BadValueError('Can not convert %r to int' % value)
|
raise BadValueError('Can not convert %r to int' % value)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'IntRange(%d, %d)' % (self.min, self.max)
|
args = (self.min, self.max)
|
||||||
|
if args[1] == DEFAULT_MAX_INT:
|
||||||
|
args = args[:1]
|
||||||
|
if args[0] == DEFAULT_MIN_INT:
|
||||||
|
args = ()
|
||||||
|
return 'IntRange%s' % repr(args)
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -316,48 +306,38 @@ class IntRange(DataType):
|
|||||||
for i in range(self.min, self.max + 1):
|
for i in range(self.min, self.max + 1):
|
||||||
other(i)
|
other(i)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'int'
|
|
||||||
|
|
||||||
|
|
||||||
class ScaledInteger(DataType):
|
class ScaledInteger(DataType):
|
||||||
"""scaled integer (= fixed resolution float) type
|
"""scaled integer (= fixed resolution float) type
|
||||||
|
|
||||||
| In general *ScaledInteger* is needed only in special cases,
|
|
||||||
e.g. when the a SEC node is running on very limited hardware
|
|
||||||
without floating point support.
|
|
||||||
| Please use *FloatRange* instead.
|
|
||||||
|
|
||||||
:param minval: (property **min**)
|
:param minval: (property **min**)
|
||||||
:param maxval: (property **max**)
|
:param maxval: (property **max**)
|
||||||
:param properties: any of the properties below
|
:param kwds: any of the properties below
|
||||||
|
|
||||||
{properties}
|
note: limits are for the scaled float value
|
||||||
:note: - limits are for the scaled float value
|
the scale is only used for calculating to/from transport serialisation
|
||||||
- the scale is only used for calculating to/from transport serialisation
|
|
||||||
"""
|
"""
|
||||||
properties = {
|
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
|
||||||
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
|
min = Property('low limit', FloatRange(), extname='min', mandatory=True)
|
||||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
max = Property('high limit', FloatRange(), extname='max', mandatory=True)
|
||||||
'max': Property('high limit', FloatRange(), extname='max', mandatory=True),
|
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
absolute_resolution = Property('absolute resolution', FloatRange(0),
|
||||||
'absolute_resolution': Property('absolute resolution', FloatRange(0),
|
extname='absolute_resolution', default=0.0)
|
||||||
extname='absolute_resolution', default=0.0),
|
relative_resolution = Property('relative resolution', FloatRange(0),
|
||||||
'relative_resolution': Property('relative resolution', FloatRange(0),
|
extname='relative_resolution', default=1.2e-7)
|
||||||
extname='relative_resolution', default=1.2e-7),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **properties):
|
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
scale = float(scale)
|
scale = float(scale)
|
||||||
if absolute_resolution is None:
|
if absolute_resolution is None:
|
||||||
absolute_resolution = scale
|
absolute_resolution = scale
|
||||||
self.set_properties(scale=scale,
|
self.set_properties(
|
||||||
|
scale=scale,
|
||||||
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
|
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
|
||||||
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
||||||
absolute_resolution=absolute_resolution,
|
absolute_resolution=absolute_resolution,
|
||||||
**properties)
|
**kwds)
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||||
@ -384,8 +364,8 @@ class ScaledInteger(DataType):
|
|||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='scaled',
|
return self.get_info(type='scaled',
|
||||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
min=int((self.min + self.scale * 0.5) // self.scale),
|
||||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
try:
|
try:
|
||||||
@ -398,15 +378,15 @@ class ScaledInteger(DataType):
|
|||||||
value = min(max(value, self.min), self.max)
|
value = min(max(value, self.min), self.max)
|
||||||
else:
|
else:
|
||||||
raise BadValueError('%g should be a float between %g and %g' %
|
raise BadValueError('%g should be a float between %g and %g' %
|
||||||
(value, self.min, self.max))
|
(value, self.min, self.max))
|
||||||
intval = int((value + self.scale * 0.5) // self.scale)
|
intval = int((value + self.scale * 0.5) // self.scale)
|
||||||
value = float(intval * self.scale)
|
value = float(intval * self.scale)
|
||||||
return value # return 'actual' value (which is more discrete than a float)
|
return value # return 'actual' value (which is more discrete than a float)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
hints = self.get_info(scale=float('%g' % self.scale),
|
hints = self.get_info(scale=float('%g' % self.scale),
|
||||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
min=int((self.min + self.scale * 0.5) // self.scale),
|
||||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
@ -435,25 +415,19 @@ class ScaledInteger(DataType):
|
|||||||
other(self.min)
|
other(self.min)
|
||||||
other(self.max)
|
other(self.max)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'float'
|
|
||||||
|
|
||||||
|
|
||||||
class EnumType(DataType):
|
class EnumType(DataType):
|
||||||
"""enumeration
|
"""enumeration
|
||||||
|
|
||||||
:param enum_or_name: the name of the Enum or an Enum to inherit from
|
:param enum_or_name: the name of the Enum or an Enum to inherit from
|
||||||
:param members: each argument denotes <member name>=<member int value>
|
:param members: members dict or None when using kwds only
|
||||||
|
:param kwds: (additional) members
|
||||||
exception: use members=<member dict> to add members from a dict
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, enum_or_name='', **members):
|
def __init__(self, enum_or_name='', *, members=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if 'members' in members:
|
if members is not None:
|
||||||
members = dict(members)
|
kwds.update(members)
|
||||||
members.update(members['members'])
|
self._enum = Enum(enum_or_name, **kwds)
|
||||||
members.pop('members')
|
|
||||||
self._enum = Enum(enum_or_name, **members)
|
|
||||||
self.default = self._enum[self._enum.members[0]]
|
self.default = self._enum[self._enum.members[0]]
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
@ -461,10 +435,11 @@ class EnumType(DataType):
|
|||||||
return EnumType(self._enum)
|
return EnumType(self._enum)
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return {'type': 'enum', 'members':dict((m.name, m.value) for m in self._enum.members)}
|
return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)}
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "EnumType(%r, %s)" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members))
|
return "EnumType(%r, %s)" % (self._enum.name,
|
||||||
|
', '.join('%s=%d' % (m.name, m.value) for m in self._enum.members))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -478,7 +453,7 @@ class EnumType(DataType):
|
|||||||
"""return the validated (internal) value or raise"""
|
"""return the validated (internal) value or raise"""
|
||||||
try:
|
try:
|
||||||
return self._enum[value]
|
return self._enum[value]
|
||||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||||
raise BadValueError('%r is not a member of enum %r' % (value, self._enum))
|
raise BadValueError('%r is not a member of enum %r' % (value, self._enum))
|
||||||
|
|
||||||
def from_string(self, text):
|
def from_string(self, text):
|
||||||
@ -487,25 +462,24 @@ class EnumType(DataType):
|
|||||||
def format_value(self, value, unit=None):
|
def format_value(self, value, unit=None):
|
||||||
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
||||||
|
|
||||||
|
def set_name(self, name):
|
||||||
|
self._enum.name = name
|
||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
for m in self._enum.members:
|
for m in self._enum.members:
|
||||||
other(m)
|
other(m)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'one of %s' % str(tuple(self._enum.keys()))
|
|
||||||
|
|
||||||
|
|
||||||
class BLOBType(DataType):
|
class BLOBType(DataType):
|
||||||
"""binary large object
|
"""binary large object
|
||||||
|
|
||||||
internally treated as bytes
|
internally treated as bytes
|
||||||
"""
|
"""
|
||||||
properties = {
|
|
||||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||||
default=0),
|
default=0)
|
||||||
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||||
mandatory=True),
|
mandatory=True)
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minbytes=0, maxbytes=None):
|
def __init__(self, minbytes=0, maxbytes=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -565,21 +539,20 @@ class BLOBType(DataType):
|
|||||||
class StringType(DataType):
|
class StringType(DataType):
|
||||||
"""string
|
"""string
|
||||||
|
|
||||||
|
for parameters see properties below
|
||||||
"""
|
"""
|
||||||
properties = {
|
minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
extname='minchars', default=0)
|
||||||
extname='minchars', default=0),
|
maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||||
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED),
|
extname='maxchars', default=UNLIMITED)
|
||||||
extname='maxchars', default=UNLIMITED),
|
isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||||
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
Stub('BoolType'), extname='isUTF8', default=False)
|
||||||
Stub('BoolType'), extname='isUTF8', default=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, minchars=0, maxchars=None, isUTF8=False):
|
def __init__(self, minchars=0, maxchars=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if maxchars is None:
|
if maxchars is None:
|
||||||
maxchars = minchars or UNLIMITED
|
maxchars = minchars or UNLIMITED
|
||||||
self.set_properties(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8)
|
self.set_properties(minchars=minchars, maxchars=maxchars, **kwds)
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
self.default = ' ' * self.minchars
|
self.default = ' ' * self.minchars
|
||||||
@ -635,24 +608,13 @@ class StringType(DataType):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'str'
|
|
||||||
|
|
||||||
|
|
||||||
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
||||||
# whereas StringType is supposed to not contain '\n'
|
# whereas StringType is supposed to not contain '\n'
|
||||||
# unfortunately, SECoP makes no distinction here....
|
# unfortunately, SECoP makes no distinction here....
|
||||||
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation
|
# note: content is supposed to follow the format of a git commit message,
|
||||||
|
# i.e. a line of text, 2 '\n' + a longer explanation
|
||||||
class TextType(StringType):
|
class TextType(StringType):
|
||||||
"""special string type, intended for longer texts
|
|
||||||
|
|
||||||
:param maxchars: maximum number of characters
|
|
||||||
|
|
||||||
whereas StringType is supposed to not contain '\n'
|
|
||||||
unfortunately, SECoP makes no distinction here....
|
|
||||||
note: content is supposed to follow the format of a git commit message,
|
|
||||||
i.e. a line of text, 2 '\n' + a longer explanation
|
|
||||||
"""
|
|
||||||
def __init__(self, maxchars=None):
|
def __init__(self, maxchars=None):
|
||||||
if maxchars is None:
|
if maxchars is None:
|
||||||
maxchars = UNLIMITED
|
maxchars = UNLIMITED
|
||||||
@ -661,7 +623,7 @@ class TextType(StringType):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.maxchars == UNLIMITED:
|
if self.maxchars == UNLIMITED:
|
||||||
return 'TextType()'
|
return 'TextType()'
|
||||||
return 'TextType(%d)' % (self.maxchars)
|
return 'TextType(%d)' % self.maxchars
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
# DataType.copy will not work, because it is exported as 'string'
|
# DataType.copy will not work, because it is exported as 'string'
|
||||||
@ -669,9 +631,7 @@ class TextType(StringType):
|
|||||||
|
|
||||||
|
|
||||||
class BoolType(DataType):
|
class BoolType(DataType):
|
||||||
"""boolean
|
"""boolean"""
|
||||||
|
|
||||||
"""
|
|
||||||
default = False
|
default = False
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
@ -707,9 +667,6 @@ class BoolType(DataType):
|
|||||||
other(False)
|
other(False)
|
||||||
other(True)
|
other(True)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'bool'
|
|
||||||
|
|
||||||
|
|
||||||
Stub.fix_datatypes()
|
Stub.fix_datatypes()
|
||||||
|
|
||||||
@ -721,14 +678,12 @@ Stub.fix_datatypes()
|
|||||||
class ArrayOf(DataType):
|
class ArrayOf(DataType):
|
||||||
"""data structure with fields of homogeneous type
|
"""data structure with fields of homogeneous type
|
||||||
|
|
||||||
:param members: the datatype for all elements
|
:param members: the datatype of the elements
|
||||||
"""
|
"""
|
||||||
properties = {
|
minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||||
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
default=0)
|
||||||
default=0),
|
maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||||
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen',
|
mandatory=True)
|
||||||
mandatory=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, members, minlen=0, maxlen=None):
|
def __init__(self, members, minlen=0, maxlen=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -759,14 +714,14 @@ class ArrayOf(DataType):
|
|||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
"""set also properties of members"""
|
"""set also properties of members"""
|
||||||
if key in self.__class__.properties:
|
if key in self.propertyDict:
|
||||||
super().setProperty(key, value)
|
super().setProperty(key, value)
|
||||||
else:
|
else:
|
||||||
self.members.setProperty(key, value)
|
self.members.setProperty(key, value)
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||||
members=self.members.export_datatype())
|
members=self.members.export_datatype())
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'ArrayOf(%s, %s, %s)' % (
|
return 'ArrayOf(%s, %s, %s)' % (
|
||||||
@ -818,16 +773,12 @@ class ArrayOf(DataType):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'array of %s' % self.members.short_doc()
|
|
||||||
|
|
||||||
|
|
||||||
class TupleOf(DataType):
|
class TupleOf(DataType):
|
||||||
"""data structure with fields of inhomogeneous type
|
"""data structure with fields of inhomogeneous type
|
||||||
|
|
||||||
:param members: each argument is a datatype of an element
|
types are given as positional arguments
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *members):
|
def __init__(self, *members):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if not members:
|
if not members:
|
||||||
@ -855,11 +806,10 @@ class TupleOf(DataType):
|
|||||||
try:
|
try:
|
||||||
if len(value) != len(self.members):
|
if len(value) != len(self.members):
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'Illegal number of Arguments! Need %d arguments.' %
|
'Illegal number of Arguments! Need %d arguments.' % len(self.members))
|
||||||
(len(self.members)))
|
|
||||||
# validate elements and return as list
|
# validate elements and return as list
|
||||||
return tuple(sub(elem)
|
return tuple(sub(elem)
|
||||||
for sub, elem in zip(self.members, value))
|
for sub, elem in zip(self.members, value))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise BadValueError('Can not validate:', str(exc))
|
raise BadValueError('Can not validate:', str(exc))
|
||||||
|
|
||||||
@ -879,19 +829,16 @@ class TupleOf(DataType):
|
|||||||
|
|
||||||
def format_value(self, value, unit=None):
|
def format_value(self, value, unit=None):
|
||||||
return '(%s)' % (', '.join([sub.format_value(elem)
|
return '(%s)' % (', '.join([sub.format_value(elem)
|
||||||
for sub, elem in zip(self.members, value)]))
|
for sub, elem in zip(self.members, value)]))
|
||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if not isinstance(other, TupleOf):
|
if not isinstance(other, TupleOf):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
if len(self.members) != len(other.members) :
|
if len(self.members) != len(other.members):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
for a, b in zip(self.members, other.members):
|
for a, b in zip(self.members, other.members):
|
||||||
a.compatible(b)
|
a.compatible(b)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'tuple of (%s)' % ', '.join(m.short_doc() for m in self.members)
|
|
||||||
|
|
||||||
|
|
||||||
class ImmutableDict(dict):
|
class ImmutableDict(dict):
|
||||||
def _no(self, *args, **kwds):
|
def _no(self, *args, **kwds):
|
||||||
@ -902,8 +849,8 @@ class ImmutableDict(dict):
|
|||||||
class StructOf(DataType):
|
class StructOf(DataType):
|
||||||
"""data structure with named fields
|
"""data structure with named fields
|
||||||
|
|
||||||
:param optional: (*sequence*) optional members
|
:param optional: a list of optional members
|
||||||
:param members: each argument denotes <member name>=<member data type>
|
:param members: names as keys and types as values for all members
|
||||||
"""
|
"""
|
||||||
def __init__(self, optional=None, **members):
|
def __init__(self, optional=None, **members):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -919,15 +866,15 @@ class StructOf(DataType):
|
|||||||
if name not in members:
|
if name not in members:
|
||||||
raise ProgrammingError(
|
raise ProgrammingError(
|
||||||
'Only members of StructOf may be declared as optional!')
|
'Only members of StructOf may be declared as optional!')
|
||||||
self.default = dict((k,el.default) for k, el in members.items())
|
self.default = dict((k, el.default) for k, el in members.items())
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""DataType.copy does not work when members contain enums"""
|
"""DataType.copy does not work when members contain enums"""
|
||||||
return StructOf(self.optional, **{k: v.copy() for k,v in self.members.items()})
|
return StructOf(self.optional, **{k: v.copy() for k, v in self.members.items()})
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
res = dict(type='struct', members=dict((n, s.export_datatype())
|
res = dict(type='struct', members=dict((n, s.export_datatype())
|
||||||
for n, s in list(self.members.items())))
|
for n, s in list(self.members.items())))
|
||||||
if self.optional:
|
if self.optional:
|
||||||
res['optional'] = self.optional
|
res['optional'] = self.optional
|
||||||
return res
|
return res
|
||||||
@ -979,18 +926,11 @@ class StructOf(DataType):
|
|||||||
except (AttributeError, TypeError, KeyError):
|
except (AttributeError, TypeError, KeyError):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
return 'dict'
|
|
||||||
|
|
||||||
|
|
||||||
class CommandType(DataType):
|
class CommandType(DataType):
|
||||||
"""command
|
"""command
|
||||||
|
|
||||||
a pseudo datatype for commands with arguments and return values
|
a pseudo datatype for commands with arguments and return values
|
||||||
|
|
||||||
:param argument: None or the data type of the argument. multiple arguments may be simulated
|
|
||||||
by TupleOf or StructOf
|
|
||||||
:param result: None or the data type of the result
|
|
||||||
"""
|
"""
|
||||||
IS_COMMAND = True
|
IS_COMMAND = True
|
||||||
|
|
||||||
@ -1049,16 +989,10 @@ class CommandType(DataType):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
argument = self.argument.short_doc() if self.argument else ''
|
|
||||||
result = ' -> %s' % self.argument.short_doc() if self.result else ''
|
|
||||||
return '(%s)%s' % (argument, result) # return argument list only
|
|
||||||
|
|
||||||
|
|
||||||
# internally used datatypes (i.e. only for programming the SEC-node)
|
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||||
|
|
||||||
class DataTypeType(DataType):
|
class DataTypeType(DataType):
|
||||||
"""DataType type"""
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""check if given value (a python obj) is a valid datatype
|
"""check if given value (a python obj) is a valid datatype
|
||||||
|
|
||||||
@ -1102,9 +1036,7 @@ class ValueType(DataType):
|
|||||||
|
|
||||||
|
|
||||||
class NoneOr(DataType):
|
class NoneOr(DataType):
|
||||||
"""validates a None or other
|
"""validates a None or smth. else"""
|
||||||
|
|
||||||
:param other: the other datatype"""
|
|
||||||
default = None
|
default = None
|
||||||
|
|
||||||
def __init__(self, other):
|
def __init__(self, other):
|
||||||
@ -1119,16 +1051,8 @@ class NoneOr(DataType):
|
|||||||
return None
|
return None
|
||||||
return self.other.export_value(value)
|
return self.other.export_value(value)
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
other = self.other.short_doc()
|
|
||||||
return '%s or None' % other if other else None
|
|
||||||
|
|
||||||
|
|
||||||
class OrType(DataType):
|
class OrType(DataType):
|
||||||
"""validates one of the
|
|
||||||
|
|
||||||
:param types: each argument denotes one allowed type
|
|
||||||
"""
|
|
||||||
def __init__(self, *types):
|
def __init__(self, *types):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.types = types
|
self.types = types
|
||||||
@ -1142,12 +1066,6 @@ class OrType(DataType):
|
|||||||
pass
|
pass
|
||||||
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
|
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
|
||||||
|
|
||||||
def short_doc(self):
|
|
||||||
types = [t.short_doc() for t in self.types]
|
|
||||||
if None in types:
|
|
||||||
return None
|
|
||||||
return ' or '.join(types)
|
|
||||||
|
|
||||||
|
|
||||||
Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
|
Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
|
||||||
Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
|
Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
|
||||||
@ -1161,12 +1079,6 @@ UInt64 = IntRange(0, (1 << 64) - 1)
|
|||||||
|
|
||||||
# Goodie: Convenience Datatypes for Programming
|
# Goodie: Convenience Datatypes for Programming
|
||||||
class LimitsType(TupleOf):
|
class LimitsType(TupleOf):
|
||||||
"""limit (min, max) tuple
|
|
||||||
|
|
||||||
:param members: the type of both members
|
|
||||||
|
|
||||||
checks for min <= max
|
|
||||||
"""
|
|
||||||
def __init__(self, members):
|
def __init__(self, members):
|
||||||
TupleOf.__init__(self, members, members)
|
TupleOf.__init__(self, members, members)
|
||||||
|
|
||||||
@ -1178,22 +1090,13 @@ class LimitsType(TupleOf):
|
|||||||
|
|
||||||
|
|
||||||
class StatusType(TupleOf):
|
class StatusType(TupleOf):
|
||||||
"""SECoP status type
|
# shorten initialisation and allow access to status enumMembers from status values
|
||||||
|
|
||||||
:param enum: the status code enum type
|
|
||||||
|
|
||||||
allows to access enum members directly
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, enum):
|
def __init__(self, enum):
|
||||||
TupleOf.__init__(self, EnumType(enum), StringType())
|
TupleOf.__init__(self, EnumType(enum), StringType())
|
||||||
self.enum = enum
|
self._enum = enum
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
enum = TupleOf.__getattr__(self, 'enum')
|
return getattr(self._enum, key)
|
||||||
if hasattr(enum, key):
|
|
||||||
return getattr(enum, key)
|
|
||||||
return TupleOf.__getattr__(self, key)
|
|
||||||
|
|
||||||
|
|
||||||
def floatargs(kwds):
|
def floatargs(kwds):
|
||||||
|
@ -22,7 +22,6 @@
|
|||||||
"""Define (internal) SECoP Errors"""
|
"""Define (internal) SECoP Errors"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SECoPError(RuntimeError):
|
class SECoPError(RuntimeError):
|
||||||
|
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
@ -138,12 +137,6 @@ def secop_error(exception):
|
|||||||
return InternalError(repr(exception))
|
return InternalError(repr(exception))
|
||||||
|
|
||||||
|
|
||||||
def fmt_error(exception):
|
|
||||||
if isinstance(exception, SECoPError):
|
|
||||||
return str(exception)
|
|
||||||
return repr(exception)
|
|
||||||
|
|
||||||
|
|
||||||
EXCEPTIONS = dict(
|
EXCEPTIONS = dict(
|
||||||
NoSuchModule=NoSuchModuleError,
|
NoSuchModule=NoSuchModuleError,
|
||||||
NoSuchParameter=NoSuchParameterError,
|
NoSuchParameter=NoSuchParameterError,
|
||||||
|
@ -24,11 +24,10 @@
|
|||||||
|
|
||||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||||
FloatRange, StringType, StructOf, TupleOf
|
FloatRange, StringType, StructOf, TupleOf
|
||||||
from secop.metaclass import ModuleMeta
|
from secop.modules import Command, HasAccessibles, Parameter
|
||||||
from secop.modules import Command, Parameter
|
|
||||||
|
|
||||||
|
|
||||||
class Feature(metaclass=ModuleMeta):
|
class Feature(HasAccessibles):
|
||||||
"""all things belonging to a small, predefined functionality influencing the working of a module"""
|
"""all things belonging to a small, predefined functionality influencing the working of a module"""
|
||||||
|
|
||||||
|
|
||||||
@ -39,33 +38,37 @@ class HAS_PID(Feature):
|
|||||||
# note: (i would still but them in the same group, though)
|
# note: (i would still but them in the same group, though)
|
||||||
# note: if extra elements are implemented in the pid struct they MUST BE
|
# note: if extra elements are implemented in the pid struct they MUST BE
|
||||||
# properly described in the description of the pid Parameter
|
# properly described in the description of the pid Parameter
|
||||||
parameters = {
|
|
||||||
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ),
|
# parameters
|
||||||
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ),
|
use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
|
||||||
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True),
|
# pylint: disable=invalid-name
|
||||||
'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True),
|
p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
|
||||||
'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True),
|
i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
|
||||||
'pid': Parameter('(optional) Struct of p,i,d, minimum output value',
|
d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True)
|
||||||
datatype=StructOf(p=FloatRange(0),
|
base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True)
|
||||||
i=FloatRange(0),
|
pid = Parameter('(optional) Struct of p,i,d, minimum output value',
|
||||||
d=FloatRange(0),
|
datatype=StructOf(p=FloatRange(0),
|
||||||
base_output=FloatRange(0),
|
i=FloatRange(0),
|
||||||
), optional=True,
|
d=FloatRange(0),
|
||||||
), # note: struct may be extended with custom elements (names should be prefixed with '_')
|
base_output=FloatRange(0),
|
||||||
'output' : Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False),
|
), optional=True,
|
||||||
}
|
) # note: struct may be extended with custom elements (names should be prefixed with '_')
|
||||||
|
output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Has_PIDTable(HAS_PID):
|
class Has_PIDTable(HAS_PID):
|
||||||
parameters = {
|
|
||||||
'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)),
|
# parameters
|
||||||
'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1))
|
||||||
StructOf(p=FloatRange(0),
|
pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
||||||
i=FloatRange(0),
|
StructOf(p=FloatRange(0),
|
||||||
d=FloatRange(0),
|
i=FloatRange(0),
|
||||||
_heater_range=FloatRange(0),
|
d=FloatRange(0),
|
||||||
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange'
|
_heater_range=FloatRange(0),
|
||||||
}
|
_base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Persistent(Feature):
|
class HAS_Persistent(Feature):
|
||||||
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
|
|||||||
# 'coupled' : Status.BUSY+2, # to be discussed.
|
# 'coupled' : Status.BUSY+2, # to be discussed.
|
||||||
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
||||||
#}
|
#}
|
||||||
parameters = {
|
|
||||||
'persistent_mode': Parameter('Use persistent mode',
|
# parameters
|
||||||
datatype=EnumType(off=0,on=1),
|
persistent_mode = Parameter('Use persistent mode',
|
||||||
default=0, readonly=False),
|
datatype=EnumType(off=0,on=1),
|
||||||
'is_persistent': Parameter('current state of persistence',
|
default=0, readonly=False)
|
||||||
datatype=BoolType(), optional=True),
|
is_persistent = Parameter('current state of persistence',
|
||||||
'stored_value': Parameter('current persistence value, often used as the modules value',
|
datatype=BoolType(), optional=True)
|
||||||
datatype='main', unit='$', optional=True),
|
stored_value = Parameter('current persistence value, often used as the modules value',
|
||||||
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
datatype='main', unit='$', optional=True)
|
||||||
datatype='main', unit='$' ),
|
driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||||
}
|
datatype='main', unit='$' )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Tolerance(Feature):
|
class HAS_Tolerance(Feature):
|
||||||
# detects IDLE status by checking if the value lies in a given window:
|
# detects IDLE status by checking if the value lies in a given window:
|
||||||
# tolerance is the maximum allowed deviation from target, value must lie in this interval
|
# tolerance is the maximum allowed deviation from target, value must lie in this interval
|
||||||
# for at least ´timewindow´ seconds.
|
# for at least ´timewindow´ seconds.
|
||||||
parameters = {
|
|
||||||
'tolerance': Parameter('Half height of the Window',
|
# parameters
|
||||||
datatype=FloatRange(0), default=1, unit='$'),
|
tolerance = Parameter('Half height of the Window',
|
||||||
'timewindow': Parameter('Length of the timewindow to check',
|
datatype=FloatRange(0), default=1, unit='$')
|
||||||
datatype=FloatRange(0), default=30, unit='s',
|
timewindow = Parameter('Length of the timewindow to check',
|
||||||
optional=True),
|
datatype=FloatRange(0), default=30, unit='s',
|
||||||
}
|
optional=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Timeout(Feature):
|
class HAS_Timeout(Feature):
|
||||||
parameters = {
|
|
||||||
'timeout': Parameter('timeout for movement',
|
# parameters
|
||||||
datatype=FloatRange(0), default=0, unit='s'),
|
timeout = Parameter('timeout for movement',
|
||||||
}
|
datatype=FloatRange(0), default=0, unit='s')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Pause(Feature):
|
class HAS_Pause(Feature):
|
||||||
# just a proposal, can't agree on it....
|
# just a proposal, can't agree on it....
|
||||||
parameters = {
|
|
||||||
'pause': Command('pauses movement', argument=None, result=None),
|
@Command(argument=None, result=None)
|
||||||
'go': Command('continues movement or start a new one if target was change since the last pause',
|
def pause(self):
|
||||||
argument=None, result=None),
|
"""pauses movement"""
|
||||||
}
|
|
||||||
|
@Command(argument=None, result=None)
|
||||||
|
def go(self):
|
||||||
|
"""continues movement or start a new one if target was change since the last pause"""
|
||||||
|
|
||||||
|
|
||||||
class HAS_Ramp(Feature):
|
class HAS_Ramp(Feature):
|
||||||
parameters = {
|
|
||||||
'ramp': Parameter('speed of movement', unit='$/min',
|
# parameters
|
||||||
datatype=FloatRange(0)),
|
ramp =Parameter('speed of movement', unit='$/min',
|
||||||
'use_ramp': Parameter('use the ramping of the setpoint, or jump',
|
datatype=FloatRange(0))
|
||||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
use_ramp = Parameter('use the ramping of the setpoint, or jump',
|
||||||
optional=True),
|
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||||
'setpoint': Parameter('currently active setpoint',
|
optional=True)
|
||||||
datatype=FloatRange(0), unit='$',
|
setpoint = Parameter('currently active setpoint',
|
||||||
readonly=True, ),
|
datatype=FloatRange(0), unit='$',
|
||||||
}
|
readonly=True, )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Speed(Feature):
|
class HAS_Speed(Feature):
|
||||||
parameters = {
|
|
||||||
'speed' : Parameter('(maximum) speed of movement (of the main value)',
|
# parameters
|
||||||
unit='$/s', datatype=FloatRange(0)),
|
speed = Parameter('(maximum) speed of movement (of the main value)',
|
||||||
}
|
unit='$/s', datatype=FloatRange(0))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Accel(HAS_Speed):
|
class HAS_Accel(HAS_Speed):
|
||||||
parameters = {
|
|
||||||
'accel' : Parameter('acceleration of movement', unit='$/s^2',
|
# parameters
|
||||||
datatype=FloatRange(0)),
|
accel = Parameter('acceleration of movement', unit='$/s^2',
|
||||||
'decel' : Parameter('deceleration of movement', unit='$/s^2',
|
datatype=FloatRange(0))
|
||||||
datatype=FloatRange(0), optional=True),
|
decel = Parameter('deceleration of movement', unit='$/s^2',
|
||||||
}
|
datatype=FloatRange(0), optional=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_MotorCurrents(Feature):
|
class HAS_MotorCurrents(Feature):
|
||||||
parameters = {
|
|
||||||
'movecurrent' : Parameter('Current while moving',
|
# parameters
|
||||||
datatype=FloatRange(0)),
|
movecurrent = Parameter('Current while moving',
|
||||||
'idlecurrent' : Parameter('Current while idle',
|
datatype=FloatRange(0))
|
||||||
datatype=FloatRange(0), optional=True),
|
idlecurrent = Parameter('Current while idle',
|
||||||
}
|
datatype=FloatRange(0), optional=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HAS_Curve(Feature):
|
class HAS_Curve(Feature):
|
||||||
# proposed, not yet agreed upon!
|
# proposed, not yet agreed upon!
|
||||||
parameters = {
|
|
||||||
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'),
|
# parameters
|
||||||
# XXX: tbd. (how to upload/download/select a curve?)
|
curve = Parameter('Calibration curve', datatype=StringType(80), default='<unset>')
|
||||||
}
|
|
||||||
|
@ -21,13 +21,13 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
from configparser import NoOptionError
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
from configparser import NoOptionError
|
||||||
from secop.gui.cfg_editor.utils import get_all_items, get_params, get_props,\
|
|
||||||
get_all_children_with_names, get_module_class_from_name, \
|
|
||||||
get_interface_class_from_name
|
|
||||||
|
|
||||||
|
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
||||||
|
from secop.gui.cfg_editor.utils import get_all_children_with_names, \
|
||||||
|
get_all_items, get_interface_class_from_name, \
|
||||||
|
get_module_class_from_name, get_params, get_props
|
||||||
|
|
||||||
NODE = 'node'
|
NODE = 'node'
|
||||||
INTERFACE = 'interface'
|
INTERFACE = 'interface'
|
||||||
@ -58,7 +58,7 @@ def write_config(file_name, tree_widget):
|
|||||||
value = value.replace('\n\n', '\n.\n')
|
value = value.replace('\n\n', '\n.\n')
|
||||||
value = value.replace('\n', '\n ')
|
value = value.replace('\n', '\n ')
|
||||||
itm_lines[id(itm)] = '[%s %s]\n' % (itm.kind, itm.name) +\
|
itm_lines[id(itm)] = '[%s %s]\n' % (itm.kind, itm.name) +\
|
||||||
value_str % (SECTIONS[itm.kind], value)
|
value_str % (SECTIONS[itm.kind], value)
|
||||||
# TODO params and props
|
# TODO params and props
|
||||||
elif itm.kind == PARAMETER and value:
|
elif itm.kind == PARAMETER and value:
|
||||||
itm_lines[id(itm)] = value_str % (itm.name, value)
|
itm_lines[id(itm)] = value_str % (itm.name, value)
|
||||||
@ -142,7 +142,7 @@ def read_config(file_path):
|
|||||||
else:
|
else:
|
||||||
param.addChild(TreeWidgetItem(PROPERTY,
|
param.addChild(TreeWidgetItem(PROPERTY,
|
||||||
separated[1], get_value(config, section,
|
separated[1], get_value(config, section,
|
||||||
option)))
|
option)))
|
||||||
node = get_comments(node, ifs, mods, file_path)
|
node = get_comments(node, ifs, mods, file_path)
|
||||||
return node, ifs, mods
|
return node, ifs, mods
|
||||||
|
|
||||||
|
@ -21,11 +21,11 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from secop.gui.qt import QMainWindow, QMessageBox
|
|
||||||
from secop.gui.cfg_editor.node_display import NodeDisplay
|
|
||||||
from secop.gui.cfg_editor.utils import loadUi, get_file_paths
|
|
||||||
from secop.gui.cfg_editor.widgets import TabBar
|
|
||||||
|
|
||||||
|
from secop.gui.cfg_editor.node_display import NodeDisplay
|
||||||
|
from secop.gui.cfg_editor.utils import get_file_paths, loadUi
|
||||||
|
from secop.gui.cfg_editor.widgets import TabBar
|
||||||
|
from secop.gui.qt import QMainWindow, QMessageBox
|
||||||
|
|
||||||
# TODO move secop mainwinodw to gui/client and all specific stuff
|
# TODO move secop mainwinodw to gui/client and all specific stuff
|
||||||
NODE = 'node'
|
NODE = 'node'
|
||||||
|
@ -20,8 +20,8 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
from secop.gui.qt import QWidget, Qt, QHBoxLayout, QSpacerItem, QSizePolicy
|
|
||||||
from secop.gui.cfg_editor.utils import loadUi
|
from secop.gui.cfg_editor.utils import loadUi
|
||||||
|
from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
|
||||||
|
|
||||||
|
|
||||||
class NodeDisplay(QWidget):
|
class NodeDisplay(QWidget):
|
||||||
|
@ -20,10 +20,11 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
from secop.gui.qt import QTreeWidgetItem, QFont, QWidget, QVBoxLayout, QLabel, \
|
from secop.gui.cfg_editor.utils import loadUi, \
|
||||||
QHBoxLayout, QPushButton, QSize, QSizePolicy, QDialog, QTextEdit, pyqtSignal
|
set_name_edit_style, setIcon, setTreeIcon
|
||||||
from secop.gui.cfg_editor.utils import setTreeIcon, setIcon, loadUi, \
|
from secop.gui.qt import QDialog, QFont, QHBoxLayout, \
|
||||||
set_name_edit_style
|
QLabel, QPushButton, QSize, QSizePolicy, QTextEdit, \
|
||||||
|
QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
|
||||||
from secop.gui.valuewidgets import get_widget
|
from secop.gui.valuewidgets import get_widget
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
|
|
||||||
|
@ -20,15 +20,16 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
from os import path, listdir
|
|
||||||
import sys
|
|
||||||
import inspect
|
import inspect
|
||||||
from secop.gui.qt import uic, QIcon, QSize, QFileDialog, QDialogButtonBox
|
import sys
|
||||||
from secop.server import getGeneralConfig
|
from os import listdir, path
|
||||||
|
|
||||||
|
from secop.gui.qt import QDialogButtonBox, QFileDialog, QIcon, QSize, uic
|
||||||
from secop.modules import Module
|
from secop.modules import Module
|
||||||
from secop.params import Parameter
|
from secop.params import Parameter
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.protocol.interface.tcp import TCPServer
|
from secop.protocol.interface.tcp import TCPServer
|
||||||
|
from secop.server import getGeneralConfig
|
||||||
|
|
||||||
uipath = path.dirname(__file__)
|
uipath = path.dirname(__file__)
|
||||||
|
|
||||||
|
@ -23,15 +23,15 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from secop.gui.cfg_editor.config_file import write_config, read_config
|
from secop.gui.cfg_editor.config_file import read_config, write_config
|
||||||
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
||||||
from secop.gui.cfg_editor.utils import get_file_paths, get_modules, \
|
from secop.gui.cfg_editor.utils import get_all_items, \
|
||||||
get_interfaces, loadUi, set_name_edit_style, get_module_class_from_name, \
|
get_file_paths, get_interface_class_from_name, get_interfaces, \
|
||||||
get_all_items, get_interface_class_from_name, get_params, get_props, \
|
get_module_class_from_name, get_modules, get_params, \
|
||||||
setActionIcon
|
get_props, loadUi, set_name_edit_style, setActionIcon
|
||||||
from secop.gui.qt import QWidget, QDialog, QLabel, QTabBar, Qt, QPoint, QMenu, \
|
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
|
||||||
QTreeWidget, QSize, pyqtSignal, QLineEdit, QComboBox, QDialogButtonBox, \
|
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
|
||||||
QTextEdit, QTreeView, QStandardItemModel, QStandardItem
|
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
|
||||||
|
|
||||||
NODE = 'node'
|
NODE = 'node'
|
||||||
MODULE = 'module'
|
MODULE = 'module'
|
||||||
|
@ -26,9 +26,9 @@ import secop.client
|
|||||||
from secop.gui.modulectrl import ModuleCtrl
|
from secop.gui.modulectrl import ModuleCtrl
|
||||||
from secop.gui.nodectrl import NodeCtrl
|
from secop.gui.nodectrl import NodeCtrl
|
||||||
from secop.gui.paramview import ParameterView
|
from secop.gui.paramview import ParameterView
|
||||||
from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \
|
from secop.gui.qt import QBrush, QColor, QInputDialog, QMainWindow, \
|
||||||
QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot, QBrush, QColor
|
QMessageBox, QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot
|
||||||
from secop.gui.util import loadUi, Value
|
from secop.gui.util import Value, loadUi
|
||||||
from secop.lib import formatExtendedTraceback
|
from secop.lib import formatExtendedTraceback
|
||||||
|
|
||||||
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
|
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
|
||||||
@ -90,7 +90,7 @@ class QSECNode(QObject):
|
|||||||
|
|
||||||
def queryCache(self, module):
|
def queryCache(self, module):
|
||||||
return {k: Value(*self.conn.cache[(module, k)])
|
return {k: Value(*self.conn.cache[(module, k)])
|
||||||
for k in self.modules[module]['parameters']}
|
for k in self.modules[module]['parameters']}
|
||||||
|
|
||||||
def syncCommunicate(self, action, ident='', data=None):
|
def syncCommunicate(self, action, ident='', data=None):
|
||||||
reply = self.conn.request(action, ident, data)
|
reply = self.conn.request(action, ident, data)
|
||||||
|
@ -36,19 +36,19 @@ class CommandDialog(QDialog):
|
|||||||
loadUi(self, 'cmddialog.ui')
|
loadUi(self, 'cmddialog.ui')
|
||||||
|
|
||||||
self.setWindowTitle('Arguments for %s' % cmdname)
|
self.setWindowTitle('Arguments for %s' % cmdname)
|
||||||
#row = 0
|
# row = 0
|
||||||
|
|
||||||
self._labels = []
|
self._labels = []
|
||||||
self.widgets = []
|
self.widgets = []
|
||||||
# improve! recursive?
|
# improve! recursive?
|
||||||
dtype = argument
|
dtype = argument
|
||||||
l = QLabel(repr(dtype))
|
label = QLabel(repr(dtype))
|
||||||
l.setWordWrap(True)
|
label.setWordWrap(True)
|
||||||
w = get_widget(dtype, readonly=False)
|
widget = get_widget(dtype, readonly=False)
|
||||||
self.gridLayout.addWidget(l, 0, 0)
|
self.gridLayout.addWidget(label, 0, 0)
|
||||||
self.gridLayout.addWidget(w, 0, 1)
|
self.gridLayout.addWidget(widget, 0, 1)
|
||||||
self._labels.append(l)
|
self._labels.append(label)
|
||||||
self.widgets.append(w)
|
self.widgets.append(widget)
|
||||||
|
|
||||||
self.gridLayout.setRowStretch(1, 1)
|
self.gridLayout.setRowStretch(1, 1)
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
|
@ -25,14 +25,15 @@
|
|||||||
import json
|
import json
|
||||||
import pprint
|
import pprint
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import mlzlog
|
import mlzlog
|
||||||
|
|
||||||
|
import secop.lib
|
||||||
from secop.datatypes import EnumType, StringType
|
from secop.datatypes import EnumType, StringType
|
||||||
from secop.errors import SECoPError
|
from secop.errors import SECoPError
|
||||||
from secop.gui.qt import QFont, QFontMetrics, QLabel, \
|
from secop.gui.qt import QFont, QFontMetrics, QLabel, \
|
||||||
QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped
|
QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped
|
||||||
from secop.gui.util import loadUi, Value
|
from secop.gui.util import Value, loadUi
|
||||||
import secop.lib
|
|
||||||
|
|
||||||
|
|
||||||
class NodeCtrl(QWidget):
|
class NodeCtrl(QWidget):
|
||||||
@ -167,7 +168,6 @@ class NodeCtrl(QWidget):
|
|||||||
print(secop.lib.formatExtendedTraceback())
|
print(secop.lib.formatExtendedTraceback())
|
||||||
widget = QLabel('Bad configured Module %s! (%s)' % (modname, e))
|
widget = QLabel('Bad configured Module %s! (%s)' % (modname, e))
|
||||||
|
|
||||||
|
|
||||||
if unit:
|
if unit:
|
||||||
labelstr = '%s (%s):' % (modname, unit)
|
labelstr = '%s (%s):' % (modname, unit)
|
||||||
else:
|
else:
|
||||||
@ -289,7 +289,7 @@ class DrivableWidget(ReadableWidget):
|
|||||||
|
|
||||||
def update_current(self, value):
|
def update_current(self, value):
|
||||||
self.currentLineEdit.setText(str(value))
|
self.currentLineEdit.setText(str(value))
|
||||||
#elif self._is_enum:
|
# elif self._is_enum:
|
||||||
# member = self._map[self._revmap[value.value]]
|
# member = self._map[self._revmap[value.value]]
|
||||||
# self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
|
# self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
|
||||||
|
|
||||||
|
@ -22,12 +22,9 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
|
|
||||||
from secop.datatypes import EnumType, FloatRange, IntRange
|
from secop.datatypes import EnumType
|
||||||
from secop.gui.qt import QPushButton as QButton
|
from secop.gui.qt import QWidget, pyqtSignal, pyqtSlot
|
||||||
from secop.gui.qt import QCheckBox, QLabel, QLineEdit, \
|
|
||||||
QMessageBox, QSizePolicy, Qt, QWidget, pyqtSignal, pyqtSlot
|
|
||||||
from secop.gui.util import loadUi
|
from secop.gui.util import loadUi
|
||||||
from secop.lib import formatExtendedStack
|
|
||||||
|
|
||||||
|
|
||||||
class ParameterWidget(QWidget):
|
class ParameterWidget(QWidget):
|
||||||
|
@ -32,6 +32,7 @@ uipath = path.dirname(__file__)
|
|||||||
def loadUi(widget, uiname, subdir='ui'):
|
def loadUi(widget, uiname, subdir='ui'):
|
||||||
uic.loadUi(path.join(uipath, subdir, uiname), widget)
|
uic.loadUi(path.join(uipath, subdir, uiname), widget)
|
||||||
|
|
||||||
|
|
||||||
class Value:
|
class Value:
|
||||||
def __init__(self, value, timestamp=None, readerror=None):
|
def __init__(self, value, timestamp=None, readerror=None):
|
||||||
self.value = value
|
self.value = value
|
||||||
|
@ -23,12 +23,13 @@
|
|||||||
|
|
||||||
|
|
||||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||||
FloatRange, IntRange, StringType, StructOf, TupleOf, TextType
|
FloatRange, IntRange, StringType, StructOf, TextType, TupleOf
|
||||||
from secop.gui.qt import QCheckBox, QComboBox, QDialog, QDoubleSpinBox, \
|
from secop.gui.qt import QCheckBox, QComboBox, QDialog, \
|
||||||
QFrame, QGridLayout, QGroupBox, QLabel, QLineEdit, QSpinBox, QVBoxLayout, \
|
QDoubleSpinBox, QFrame, QGridLayout, QGroupBox, \
|
||||||
QTextEdit
|
QLabel, QLineEdit, QSpinBox, QTextEdit, QVBoxLayout
|
||||||
from secop.gui.util import loadUi
|
from secop.gui.util import loadUi
|
||||||
|
|
||||||
|
|
||||||
# XXX: implement live validators !!!!
|
# XXX: implement live validators !!!!
|
||||||
# XXX: signals upon change of value
|
# XXX: signals upon change of value
|
||||||
# XXX: honor readonly in all cases!
|
# XXX: honor readonly in all cases!
|
||||||
@ -171,12 +172,12 @@ class StructWidget(QGroupBox):
|
|||||||
self._labels = []
|
self._labels = []
|
||||||
for idx, name in enumerate(sorted(datatype.members)):
|
for idx, name in enumerate(sorted(datatype.members)):
|
||||||
dt = datatype.members[name]
|
dt = datatype.members[name]
|
||||||
w = get_widget(dt, readonly=readonly, parent=self)
|
widget = get_widget(dt, readonly=readonly, parent=self)
|
||||||
l = QLabel(name)
|
label = QLabel(name)
|
||||||
self.layout.addWidget(l, idx, 0)
|
self.layout.addWidget(label, idx, 0)
|
||||||
self.layout.addWidget(w, idx, 1)
|
self.layout.addWidget(widget, idx, 1)
|
||||||
self._labels.append(l)
|
self._labels.append(label)
|
||||||
self.subwidgets[name] = (w, dt)
|
self.subwidgets[name] = (widget, dt)
|
||||||
self.datatypes.append(dt)
|
self.datatypes.append(dt)
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
@ -215,21 +216,22 @@ class ArrayWidget(QGroupBox):
|
|||||||
w.set_value(v)
|
w.set_value(v)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_widget(datatype, readonly=False, parent=None):
|
def get_widget(datatype, readonly=False, parent=None):
|
||||||
return {FloatRange: FloatWidget,
|
return {
|
||||||
IntRange: IntWidget,
|
FloatRange: FloatWidget,
|
||||||
StringType: StringWidget,
|
IntRange: IntWidget,
|
||||||
TextType: TextWidget,
|
StringType: StringWidget,
|
||||||
BLOBType: BlobWidget,
|
TextType: TextWidget,
|
||||||
EnumType: EnumWidget,
|
BLOBType: BlobWidget,
|
||||||
BoolType: BoolWidget,
|
EnumType: EnumWidget,
|
||||||
TupleOf: TupleWidget,
|
BoolType: BoolWidget,
|
||||||
StructOf: StructWidget,
|
TupleOf: TupleWidget,
|
||||||
ArrayOf: ArrayWidget,
|
StructOf: StructWidget,
|
||||||
|
ArrayOf: ArrayWidget,
|
||||||
}.get(datatype.__class__)(datatype, readonly, parent)
|
}.get(datatype.__class__)(datatype, readonly, parent)
|
||||||
# TODO: handle NoneOr
|
# TODO: handle NoneOr
|
||||||
|
|
||||||
|
|
||||||
class msg(QDialog):
|
class msg(QDialog):
|
||||||
def __init__(self, stuff, parent=None):
|
def __init__(self, stuff, parent=None):
|
||||||
super(msg, self).__init__(parent)
|
super(msg, self).__init__(parent)
|
||||||
@ -242,7 +244,7 @@ class msg(QDialog):
|
|||||||
dt = StructOf(i=IntRange(0, 10), f=FloatRange(), b=BoolType())
|
dt = StructOf(i=IntRange(0, 10), f=FloatRange(), b=BoolType())
|
||||||
w = StructWidget(dt)
|
w = StructWidget(dt)
|
||||||
self.gridLayout.addWidget(w, row, 1)
|
self.gridLayout.addWidget(w, row, 1)
|
||||||
row+=1
|
row += 1
|
||||||
|
|
||||||
self.gridLayout.addWidget(QLabel('stuff'), row, 0, 1, 0)
|
self.gridLayout.addWidget(QLabel('stuff'), row, 0, 1, 0)
|
||||||
row += 1 # at pos (0,0) span 2 cols, 1 row
|
row += 1 # at pos (0,0) span 2 cols, 1 row
|
||||||
|
@ -54,8 +54,8 @@ method has to be called explicitly int the write_<parameter> method, if needed.
|
|||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from secop.metaclass import Done
|
|
||||||
from secop.errors import ProgrammingError
|
from secop.errors import ProgrammingError
|
||||||
|
from secop.modules import Done
|
||||||
|
|
||||||
|
|
||||||
class CmdParser:
|
class CmdParser:
|
||||||
@ -202,13 +202,19 @@ class IOHandler(IOHandlerBase):
|
|||||||
:param replyfmt: the format for reading the reply with some scanf like behaviour
|
:param replyfmt: the format for reading the reply with some scanf like behaviour
|
||||||
:param changecmd: the first part of the change command (without values), may be
|
:param changecmd: the first part of the change command (without values), may be
|
||||||
omitted if no write happens
|
omitted if no write happens
|
||||||
|
"""
|
||||||
"""
|
|
||||||
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
|
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
|
||||||
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
|
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
|
||||||
|
|
||||||
def __init__(self, group, querycmd, replyfmt, changecmd=None):
|
def __init__(self, group, querycmd, replyfmt, changecmd=None):
|
||||||
"""initialize the IO handler"""
|
"""initialize the IO handler
|
||||||
|
|
||||||
|
group: the handler group (used for analyze_<group> and change_<group>)
|
||||||
|
querycmd: the command for a query, may contain named formats for cmdargs
|
||||||
|
replyfmt: the format for reading the reply with some scanf like behaviour
|
||||||
|
changecmd: the first part of the change command (without values), may be
|
||||||
|
omitted if no write happens
|
||||||
|
"""
|
||||||
self.group = group
|
self.group = group
|
||||||
self.parameters = set()
|
self.parameters = set()
|
||||||
self._module_class = None
|
self._module_class = None
|
||||||
|
@ -21,13 +21,13 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Define helpers"""
|
"""Define helpers"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
import linecache
|
import linecache
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import importlib
|
from os import environ, path
|
||||||
from os import path, environ
|
|
||||||
|
|
||||||
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
||||||
|
|
||||||
@ -58,6 +58,7 @@ CONFIG['basedir'] = repodir
|
|||||||
|
|
||||||
unset_value = object()
|
unset_value = object()
|
||||||
|
|
||||||
|
|
||||||
class lazy_property:
|
class lazy_property:
|
||||||
"""A property that calculates its value only once."""
|
"""A property that calculates its value only once."""
|
||||||
|
|
||||||
|
@ -28,16 +28,18 @@ support for asynchronous communication, but may be used also for
|
|||||||
synchronous IO (see secop.stringio.StringIO)
|
synchronous IO (see secop.stringio.StringIO)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
|
||||||
import select
|
|
||||||
import time
|
|
||||||
import ast
|
import ast
|
||||||
|
import select
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
from secop.errors import CommunicationFailedError, ConfigError
|
||||||
|
from secop.lib import closeSocket, parseHostPort, tcpSocket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from serial import Serial
|
from serial import Serial
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Serial = None
|
Serial = None
|
||||||
from secop.lib import parseHostPort, tcpSocket, closeSocket
|
|
||||||
from secop.errors import ConfigError, CommunicationFailedError
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionClosed(ConnectionError):
|
class ConnectionClosed(ConnectionError):
|
||||||
@ -60,10 +62,10 @@ class AsynConn:
|
|||||||
except (ValueError, TypeError, AssertionError):
|
except (ValueError, TypeError, AssertionError):
|
||||||
if 'COM' in uri:
|
if 'COM' in uri:
|
||||||
raise ValueError("the correct uri for a COM port is: "
|
raise ValueError("the correct uri for a COM port is: "
|
||||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'" )
|
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
|
||||||
if '/dev' in uri:
|
if '/dev' in uri:
|
||||||
raise ValueError("the correct uri for a serial port is: "
|
raise ValueError("the correct uri for a serial port is: "
|
||||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'" )
|
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
|
||||||
raise ValueError('invalid uri: %s' % uri)
|
raise ValueError('invalid uri: %s' % uri)
|
||||||
iocls = cls.SCHEME_MAP['tcp']
|
iocls = cls.SCHEME_MAP['tcp']
|
||||||
uri = 'tcp://%s:%d' % host_port
|
uri = 'tcp://%s:%d' % host_port
|
||||||
|
@ -20,57 +20,168 @@
|
|||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
from inspect import cleandoc
|
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
|
|
||||||
|
from secop.modules import Command, HasProperties, Module, Parameter, Property
|
||||||
|
|
||||||
|
|
||||||
def indent_description(p):
|
def indent_description(p):
|
||||||
"""indent lines except first one"""
|
"""indent lines except first one"""
|
||||||
return indent(p.description, ' ').replace(' ', '', 1)
|
return indent(p.description, ' ').replace(' ', '', 1)
|
||||||
|
|
||||||
|
|
||||||
def append_to_doc(cls, name, title, attrname, newitems, fmtfunc):
|
def fmt_param(name, param):
|
||||||
|
desc = indent_description(param)
|
||||||
|
if '(' in desc[0:2]:
|
||||||
|
dtinfo = ''
|
||||||
|
else:
|
||||||
|
dtinfo = [short_doc(param.datatype), 'rd' if param.readonly else 'wr',
|
||||||
|
None if param.export else 'hidden']
|
||||||
|
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
|
||||||
|
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_command(name, command):
|
||||||
|
desc = indent_description(command)
|
||||||
|
if '(' in desc[0:2]:
|
||||||
|
dtinfo = '' # note: we expect that desc contains argument list
|
||||||
|
else:
|
||||||
|
dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*')
|
||||||
|
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
|
||||||
|
|
||||||
|
|
||||||
|
def fmt_property(name, prop):
|
||||||
|
desc = indent_description(prop)
|
||||||
|
if '(' in desc[0:2]:
|
||||||
|
dtinfo = ''
|
||||||
|
else:
|
||||||
|
dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden']
|
||||||
|
dtinfo = ', '.join(filter(None, dtinfo))
|
||||||
|
if dtinfo:
|
||||||
|
dtinfo = '*(%s)* ' % dtinfo
|
||||||
|
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||||
|
|
||||||
|
|
||||||
|
SIMPLETYPES = {
|
||||||
|
'FloatRange': 'float',
|
||||||
|
'ScaledInteger': 'float',
|
||||||
|
'IntRange': 'int',
|
||||||
|
'BlobType': 'bytes',
|
||||||
|
'StringType': 'str',
|
||||||
|
'TextType': 'str',
|
||||||
|
'BoolType': 'bool',
|
||||||
|
'StructOf': 'dict',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def short_doc(datatype):
|
||||||
|
# pylint: disable=possibly-unused-variable
|
||||||
|
|
||||||
|
def doc_EnumType(dt):
|
||||||
|
return 'one of %s' % str(tuple(dt._enum.keys()))
|
||||||
|
|
||||||
|
def doc_ArrayOf(dt):
|
||||||
|
return 'array of %s' % short_doc(dt.members)
|
||||||
|
|
||||||
|
def doc_TupleOf(dt):
|
||||||
|
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
|
||||||
|
|
||||||
|
def doc_CommandType(dt):
|
||||||
|
argument = short_doc(dt.argument) if dt.argument else ''
|
||||||
|
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
|
||||||
|
return '(%s)%s' % (argument, result) # return argument list only
|
||||||
|
|
||||||
|
def doc_NoneOr(dt):
|
||||||
|
other = short_doc(dt.other)
|
||||||
|
return '%s or None' % other if other else None
|
||||||
|
|
||||||
|
def doc_OrType(dt):
|
||||||
|
types = [short_doc(t) for t in dt.types]
|
||||||
|
if None in types: # type is anyway broad: no doc
|
||||||
|
return None
|
||||||
|
return ' or '.join(types)
|
||||||
|
|
||||||
|
def doc_Stub(dt):
|
||||||
|
return dt.name.replace('Type', '').replace('Range', '').lower()
|
||||||
|
|
||||||
|
clsname = datatype.__class__.__name__
|
||||||
|
result = SIMPLETYPES.get(clsname)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
fun = locals().get('doc_' + clsname)
|
||||||
|
if fun:
|
||||||
|
return fun(datatype)
|
||||||
|
return None # broad type like ValueType: no doc
|
||||||
|
|
||||||
|
|
||||||
|
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
||||||
"""add information about some items to the doc
|
"""add information about some items to the doc
|
||||||
|
|
||||||
:param cls: the class with the doc string to be extended
|
:param cls: the class with the doc string to be extended
|
||||||
:param name: the name of the attribute dict to be used
|
:param lines: content of the docstring, as lines
|
||||||
:param title: the title to be used
|
:param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
|
||||||
:param newitems: the set of new items defined for this class
|
:param attrname: the name of the attribute dict to look for
|
||||||
|
:param name: the name of the items to be collected (used for the title and for the tags)
|
||||||
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
|
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
|
||||||
or an empty string to suppress output for this item
|
or an empty string to suppress output for this item
|
||||||
:type fmtfunc: function(key, value)
|
:type fmtfunc: function(key, value)
|
||||||
|
|
||||||
|
rules, assuming name='properties':
|
||||||
|
|
||||||
|
- if the docstring contains ``{properties}``, new properties are inserted here
|
||||||
|
- if the docstring contains ``{all properties}``, all properties are inserted here
|
||||||
|
- if the docstring contains ``{no properties}``, no properties are inserted
|
||||||
|
|
||||||
|
only the first appearance of a tag above is considered
|
||||||
"""
|
"""
|
||||||
doc = cleandoc(cls.__doc__ or '')
|
doc = '\n'.join(lines)
|
||||||
|
title = 'SECoP %s' % name.title()
|
||||||
allitems = getattr(cls, attrname, {})
|
allitems = getattr(cls, attrname, {})
|
||||||
fmtdict = {n: fmtfunc(n, p) or ' - **%s** *removed*\n' % n for n, p in allitems.items()}
|
fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)}
|
||||||
head, _, tail = doc.partition('{all %s}' % name)
|
head, _, tail = doc.partition('{all %s}' % name)
|
||||||
|
clsset = set()
|
||||||
if tail: # take all
|
if tail: # take all
|
||||||
inherited = set()
|
fmted = fmtdict.values()
|
||||||
fmted = ''.join(fmtdict.values())
|
|
||||||
else:
|
else:
|
||||||
inherited = {n: p for n, p in allitems.items() if fmtdict.get(n) and n not in newitems}
|
|
||||||
fmted = ''.join(' ' + v for k, v in fmtdict.items() if k in newitems)
|
|
||||||
head, _, tail = doc.partition('{%s}' % name)
|
head, _, tail = doc.partition('{%s}' % name)
|
||||||
if not tail:
|
if not tail:
|
||||||
head, _, tail = doc.partition('{no %s}' % name)
|
head, _, tail = doc.partition('{no %s}' % name)
|
||||||
if tail: # add no information
|
if tail: # add no information
|
||||||
return
|
return
|
||||||
# no tag found: append to the end
|
# no tag found: append to the end
|
||||||
if fmted:
|
|
||||||
clsset = set()
|
fmted = []
|
||||||
for name in inherited:
|
for key, formatted_item in fmtdict.items():
|
||||||
p = allitems[name]
|
if not formatted_item:
|
||||||
refcls = cls
|
continue
|
||||||
|
# find where item is defined or modified
|
||||||
|
refcls = None
|
||||||
for base in cls.__mro__:
|
for base in cls.__mro__:
|
||||||
dp = getattr(base, attrname, {}).get(name)
|
p = getattr(base, attrname, {}).get(key)
|
||||||
if dp:
|
if isinstance(p, itemcls):
|
||||||
if dp == p:
|
if fmtfunc(key, p) == formatted_item:
|
||||||
refcls = base
|
refcls = base
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
clsset.add(refcls)
|
if refcls == cls:
|
||||||
clsset.discard(cls)
|
# definition in cls is new or modified
|
||||||
|
fmted.append(formatted_item)
|
||||||
|
else:
|
||||||
|
# definition of last modification in refcls
|
||||||
|
clsset.add(refcls)
|
||||||
|
if fmted:
|
||||||
if clsset:
|
if clsset:
|
||||||
fmted += ' - see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||||
for c in cls.__mro__ if c in clsset))
|
for c in cls.__mro__ if c in clsset)))
|
||||||
cls.__doc__ = '%s\n\n:%s: %s\n%s' % (head, title, fmted, tail)
|
|
||||||
|
doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail)
|
||||||
|
lines[:] = doc.split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def class_doc_handler(app, what, name, cls, options, lines):
|
||||||
|
if what == 'class':
|
||||||
|
if issubclass(cls, HasProperties):
|
||||||
|
append_to_doc(cls, lines, Property, 'properties', 'propertyDict', fmt_property)
|
||||||
|
if issubclass(cls, Module):
|
||||||
|
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
||||||
|
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
||||||
|
@ -32,6 +32,7 @@ class EnumMember:
|
|||||||
has an int-type value and attributes 'name' and 'value'
|
has an int-type value and attributes 'name' and 'value'
|
||||||
"""
|
"""
|
||||||
__slots__ = ['name', 'value', 'enum']
|
__slots__ = ['name', 'value', 'enum']
|
||||||
|
|
||||||
def __init__(self, enum, name, value):
|
def __init__(self, enum, name, value):
|
||||||
if not isinstance(enum, Enum):
|
if not isinstance(enum, Enum):
|
||||||
raise TypeError('1st Argument must be an instance of class Enum()')
|
raise TypeError('1st Argument must be an instance of class Enum()')
|
||||||
@ -49,7 +50,7 @@ class EnumMember:
|
|||||||
try:
|
try:
|
||||||
other = int(other)
|
other = int(other)
|
||||||
except Exception:
|
except Exception:
|
||||||
#raise TypeError('%r can not be compared to %r!' %(other, self))
|
# raise TypeError('%r can not be compared to %r!' %(other, self))
|
||||||
return -1 # XXX:!
|
return -1 # XXX:!
|
||||||
if self.value < other:
|
if self.value < other:
|
||||||
return -1
|
return -1
|
||||||
@ -59,10 +60,12 @@ class EnumMember:
|
|||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
|
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
|
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, (EnumMember)):
|
if isinstance(other, EnumMember):
|
||||||
return other.value == self.value
|
return other.value == self.value
|
||||||
if isinstance(other, int):
|
if isinstance(other, int):
|
||||||
return other == self.value
|
return other == self.value
|
||||||
@ -72,10 +75,13 @@ class EnumMember:
|
|||||||
return self.name == other
|
return self.name == other
|
||||||
return False
|
return False
|
||||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
|
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
|
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
|
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
|
||||||
|
|
||||||
@ -100,77 +106,105 @@ class EnumMember:
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value)
|
return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value)
|
||||||
|
|
||||||
|
|
||||||
# numeric operations: delegate to int. Do we really need any of those?
|
# numeric operations: delegate to int. Do we really need any of those?
|
||||||
def __add__(self, other):
|
def __add__(self, other):
|
||||||
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __sub__(self, other):
|
def __sub__(self, other):
|
||||||
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __mul__(self, other):
|
def __mul__(self, other):
|
||||||
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __truediv__(self, other):
|
def __truediv__(self, other):
|
||||||
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __floordiv__(self, other):
|
def __floordiv__(self, other):
|
||||||
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __mod__(self, other):
|
def __mod__(self, other):
|
||||||
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __divmod__(self, other):
|
def __divmod__(self, other):
|
||||||
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __pow__(self, other, *args):
|
def __pow__(self, other, *args):
|
||||||
return self.value.__pow__(other, *args)
|
return self.value.__pow__(other, *args)
|
||||||
|
|
||||||
def __lshift__(self, other):
|
def __lshift__(self, other):
|
||||||
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rshift__(self, other):
|
def __rshift__(self, other):
|
||||||
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __radd__(self, other):
|
def __radd__(self, other):
|
||||||
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rsub__(self, other):
|
def __rsub__(self, other):
|
||||||
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rmul__(self, other):
|
def __rmul__(self, other):
|
||||||
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rtruediv__(self, other):
|
def __rtruediv__(self, other):
|
||||||
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rfloordiv__(self, other):
|
def __rfloordiv__(self, other):
|
||||||
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rmod__(self, other):
|
def __rmod__(self, other):
|
||||||
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rdivmod__(self, other):
|
def __rdivmod__(self, other):
|
||||||
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rpow__(self, other, *args):
|
def __rpow__(self, other, *args):
|
||||||
return self.value.__rpow__(other, *args)
|
return self.value.__rpow__(other, *args)
|
||||||
|
|
||||||
def __rlshift__(self, other):
|
def __rlshift__(self, other):
|
||||||
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rrshift__(self, other):
|
def __rrshift__(self, other):
|
||||||
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
# logical operations
|
# logical operations
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __xor__(self, other):
|
def __xor__(self, other):
|
||||||
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rand__(self, other):
|
def __rand__(self, other):
|
||||||
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __rxor__(self, other):
|
def __rxor__(self, other):
|
||||||
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
def __ror__(self, other):
|
def __ror__(self, other):
|
||||||
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
|
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
|
||||||
|
|
||||||
# other stuff
|
# other stuff
|
||||||
def __neg__(self):
|
def __neg__(self):
|
||||||
return self.value.__neg__()
|
return self.value.__neg__()
|
||||||
|
|
||||||
def __pos__(self):
|
def __pos__(self):
|
||||||
return self.value.__pos__()
|
return self.value.__pos__()
|
||||||
|
|
||||||
def __abs__(self):
|
def __abs__(self):
|
||||||
return self.value.__abs__()
|
return self.value.__abs__()
|
||||||
|
|
||||||
def __invert__(self):
|
def __invert__(self):
|
||||||
return self.value.__invert__()
|
return self.value.__invert__()
|
||||||
|
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
return self.value.__int__()
|
return self.value.__int__()
|
||||||
|
|
||||||
def __float__(self):
|
def __float__(self):
|
||||||
return self.value.__float__()
|
return self.value.__float__()
|
||||||
#return NotImplemented # makes no sense
|
|
||||||
def __index__(self):
|
def __index__(self):
|
||||||
return self.value.__index__()
|
return self.value.__index__()
|
||||||
|
|
||||||
@ -206,6 +240,7 @@ class Enum(dict):
|
|||||||
You only can create an extended Enum.
|
You only can create an extended Enum.
|
||||||
"""
|
"""
|
||||||
name = ''
|
name = ''
|
||||||
|
|
||||||
def __init__(self, name='', parent=None, **kwds):
|
def __init__(self, name='', parent=None, **kwds):
|
||||||
super(Enum, self).__init__()
|
super(Enum, self).__init__()
|
||||||
if isinstance(name, (dict, Enum)) and parent is None:
|
if isinstance(name, (dict, Enum)) and parent is None:
|
||||||
@ -217,7 +252,7 @@ class Enum(dict):
|
|||||||
# if name was not given, use that of the parent
|
# if name was not given, use that of the parent
|
||||||
# this means, an extended Enum behaves like the parent
|
# this means, an extended Enum behaves like the parent
|
||||||
# THIS MAY BE CONFUSING SOMETIMES!
|
# THIS MAY BE CONFUSING SOMETIMES!
|
||||||
name=parent.name
|
name = parent.name
|
||||||
# else:
|
# else:
|
||||||
# raise TypeError('Enum instances need a name or an Enum parent!')
|
# raise TypeError('Enum instances need a name or an Enum parent!')
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
@ -225,8 +260,9 @@ class Enum(dict):
|
|||||||
|
|
||||||
names = set()
|
names = set()
|
||||||
values = set()
|
values = set()
|
||||||
|
|
||||||
# pylint: disable=dangerous-default-value
|
# pylint: disable=dangerous-default-value
|
||||||
def add(self, k, v, names = names, value = values):
|
def add(self, k, v, names=names, value=values):
|
||||||
"""helper for creating the enum members"""
|
"""helper for creating the enum members"""
|
||||||
if v is None:
|
if v is None:
|
||||||
# sugar: take the next free number if value was None
|
# sugar: take the next free number if value was None
|
||||||
@ -237,7 +273,7 @@ class Enum(dict):
|
|||||||
if v in names:
|
if v in names:
|
||||||
v = self[v].value
|
v = self[v].value
|
||||||
while v in values:
|
while v in values:
|
||||||
v +=1
|
v += 1
|
||||||
|
|
||||||
# check that the value is an int
|
# check that the value is an int
|
||||||
_v = int(v)
|
_v = int(v)
|
||||||
@ -290,7 +326,6 @@ class Enum(dict):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
|
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
|
||||||
# return '<Enum %r (%d values)>' % (self.name, len(self)//2)
|
|
||||||
|
|
||||||
def __call__(self, key):
|
def __call__(self, key):
|
||||||
return self[key]
|
return self[key]
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Define parsing helpers"""
|
"""Define parsing helpers"""
|
||||||
|
|
||||||
|
# TODO: remove, as currently not used
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
@ -141,7 +141,7 @@ class SequencerMixin:
|
|||||||
return self.read_hw_status()
|
return self.read_hw_status()
|
||||||
return self.Status.IDLE, ''
|
return self.Status.IDLE, ''
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if self.seq_is_alive():
|
if self.seq_is_alive():
|
||||||
self._seq_stopflag = True
|
self._seq_stopflag = True
|
||||||
|
|
||||||
|
@ -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>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
#
|
#
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Define Baseclasses for real Modules implemented in the server"""
|
"""Define base classes for real Modules implemented in the server"""
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
||||||
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
|
IntRange, StatusType, StringType, TextType, TupleOf, get_datatype
|
||||||
from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueError,\
|
from secop.errors import BadValueError, ConfigError, InternalError, \
|
||||||
SilentError, InternalError, secop_error
|
ProgrammingError, SECoPError, SilentError, secop_error
|
||||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.metaclass import ModuleMeta
|
from secop.params import PREDEFINED_ACCESSIBLES, Accessible, Command, Parameter
|
||||||
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
|
from secop.poller import BasicPoller, Poller
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
from secop.poller import Poller, BasicPoller
|
|
||||||
|
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
||||||
|
|
||||||
|
|
||||||
# XXX: connect with 'protocol'-Modules.
|
class HasAccessibles(HasProperties):
|
||||||
# Idea: every Module defined herein is also a 'protocol'-Module,
|
"""base class of module
|
||||||
# all others MUST derive from those, the 'interface'-class is still derived
|
|
||||||
# from these base classes (how to do this?)
|
joining the class's properties, parameters and commands dicts with
|
||||||
|
those of base classes.
|
||||||
|
wrap read_*/write_* methods
|
||||||
|
(so the dispatcher will get notified of changed values)
|
||||||
|
"""
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(cls): # pylint: disable=too-many-branches
|
||||||
|
super().__init_subclass__()
|
||||||
|
# merge accessibles from all sub-classes, treat overrides
|
||||||
|
# for now, allow to use also the old syntax (parameters/commands dict)
|
||||||
|
accessibles = {}
|
||||||
|
for base in reversed(cls.__bases__):
|
||||||
|
accessibles.update(getattr(base, 'accessibles', {}))
|
||||||
|
newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)}
|
||||||
|
for aname, aobj in accessibles.items():
|
||||||
|
value = getattr(cls, aname, None)
|
||||||
|
if not isinstance(value, Accessible): # else override is already done in __set_name__
|
||||||
|
anew = aobj.override(value)
|
||||||
|
newaccessibles[aname] = anew
|
||||||
|
setattr(cls, aname, anew)
|
||||||
|
anew.__set_name__(cls, aname)
|
||||||
|
ordered = {}
|
||||||
|
for aname in cls.__dict__.get('paramOrder', ()):
|
||||||
|
if aname in accessibles:
|
||||||
|
ordered[aname] = accessibles.pop(aname)
|
||||||
|
elif aname in newaccessibles:
|
||||||
|
ordered[aname] = newaccessibles.pop(aname)
|
||||||
|
# ignore unknown names
|
||||||
|
# starting from old accessibles not mentioned, append items from 'order'
|
||||||
|
accessibles.update(ordered)
|
||||||
|
# then new accessibles not mentioned
|
||||||
|
accessibles.update(newaccessibles)
|
||||||
|
cls.accessibles = accessibles
|
||||||
|
|
||||||
|
# Correct naming of EnumTypes
|
||||||
|
for k, v in accessibles.items():
|
||||||
|
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
|
||||||
|
v.datatype.set_name(k)
|
||||||
|
|
||||||
|
# check validity of Parameter entries
|
||||||
|
for pname, pobj in accessibles.items():
|
||||||
|
# XXX: create getters for the units of params ??
|
||||||
|
|
||||||
|
# wrap of reading/writing funcs
|
||||||
|
if isinstance(pobj, Command):
|
||||||
|
# nothing to do for now
|
||||||
|
continue
|
||||||
|
rfunc = cls.__dict__.get('read_' + pname, None)
|
||||||
|
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
||||||
|
if rfunc_handler:
|
||||||
|
if rfunc:
|
||||||
|
raise ProgrammingError("parameter '%s' can not have a handler "
|
||||||
|
"and read_%s" % (pname, pname))
|
||||||
|
rfunc = rfunc_handler
|
||||||
|
|
||||||
|
# create wrapper except when read function is already wrapped
|
||||||
|
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
|
||||||
|
|
||||||
|
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||||
|
if rfunc:
|
||||||
|
self.log.debug("calling %r" % rfunc)
|
||||||
|
try:
|
||||||
|
value = rfunc(self)
|
||||||
|
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
||||||
|
if value is Done: # the setter is already triggered
|
||||||
|
return getattr(self, pname)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
||||||
|
self.announceUpdate(pname, None, e)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# return cached value
|
||||||
|
self.log.debug("rfunc(%s): return cached value" % pname)
|
||||||
|
value = self.accessibles[pname].value
|
||||||
|
setattr(self, pname, value) # important! trigger the setter
|
||||||
|
return value
|
||||||
|
|
||||||
|
if rfunc:
|
||||||
|
wrapped_rfunc.__doc__ = rfunc.__doc__
|
||||||
|
setattr(cls, 'read_' + pname, wrapped_rfunc)
|
||||||
|
wrapped_rfunc.__wrapped__ = True
|
||||||
|
|
||||||
|
if not pobj.readonly:
|
||||||
|
wfunc = getattr(cls, 'write_' + pname, None)
|
||||||
|
if wfunc is None: # ignore the handler, if a write function is present
|
||||||
|
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
|
||||||
|
|
||||||
|
# create wrapper except when write function is already wrapped
|
||||||
|
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
|
||||||
|
|
||||||
|
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||||
|
self.log.debug("check validity of %s = %r" % (pname, value))
|
||||||
|
pobj = self.accessibles[pname]
|
||||||
|
value = pobj.datatype(value)
|
||||||
|
if wfunc:
|
||||||
|
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
||||||
|
returned_value = wfunc(self, value)
|
||||||
|
if returned_value is Done: # the setter is already triggered
|
||||||
|
return getattr(self, pname)
|
||||||
|
if returned_value is not None: # goodie: accept missing return value
|
||||||
|
value = returned_value
|
||||||
|
setattr(self, pname, value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
if wfunc:
|
||||||
|
wrapped_wfunc.__doc__ = wfunc.__doc__
|
||||||
|
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
||||||
|
wrapped_wfunc.__wrapped__ = True
|
||||||
|
|
||||||
|
# check information about Command's
|
||||||
|
for attrname in cls.__dict__:
|
||||||
|
if attrname.startswith('do_'):
|
||||||
|
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||||
|
% (cls.__name__, attrname))
|
||||||
|
|
||||||
|
res = {}
|
||||||
|
# collect info about properties
|
||||||
|
for pn, pv in cls.propertyDict.items():
|
||||||
|
if pv.settable:
|
||||||
|
res[pn] = pv
|
||||||
|
# collect info about parameters and their properties
|
||||||
|
for param, pobj in cls.accessibles.items():
|
||||||
|
res[param] = {}
|
||||||
|
for pn, pv in pobj.getProperties().items():
|
||||||
|
if pv.settable:
|
||||||
|
res[param][pn] = pv
|
||||||
|
cls.configurables = res
|
||||||
|
|
||||||
|
|
||||||
class Module(HasProperties, metaclass=ModuleMeta):
|
class Module(HasAccessibles):
|
||||||
"""basic module
|
"""basic module
|
||||||
|
|
||||||
all SECoP modules derive from this.
|
all SECoP modules derive from this.
|
||||||
@ -58,7 +184,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- the programmer normally should not need to reimplement :meth:`__init__`
|
- the programmer normally should not need to reimplement :meth:`__init__`
|
||||||
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc...
|
- within modules, parameters should only be addressed as ``self.<pname>``,
|
||||||
|
i.e. ``self.value``, ``self.target`` etc...
|
||||||
|
|
||||||
- these are accessing the cached version.
|
- these are accessing the cached version.
|
||||||
- they can also be written to, generating an async update
|
- they can also be written to, generating an async update
|
||||||
@ -77,25 +204,21 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
# note: properties don't change after startup and are usually filled
|
# note: properties don't change after startup and are usually filled
|
||||||
# with data from a cfg file...
|
# with data from a cfg file...
|
||||||
# note: only the properties predefined here are allowed to be set in the cfg file
|
# note: only the properties predefined here are allowed to be set in the cfg file
|
||||||
# note: the names map to a [datatype, value] list, value comes from the cfg file,
|
export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
|
||||||
# datatype is fixed!
|
group = Property('optional group the module belongs to', StringType(), default='', extname='group')
|
||||||
properties = {
|
description = Property('description of the module', TextType(), extname='description', mandatory=True)
|
||||||
'export': Property('flag if this Module is to be exported', BoolType(), default=True, export=False),
|
meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
|
||||||
'group': Property('optional group the Module belongs to', StringType(), default='', extname='group'),
|
default=('', 0), extname='meaning')
|
||||||
'description': Property('description of the module', TextType(), extname='description', mandatory=True),
|
visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
'meaning': Property('dptional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
|
default='user', extname='visibility')
|
||||||
default=('',0), extname='meaning'),
|
implementation = Property('internal name of the implementation class of the module', StringType(),
|
||||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
extname='implementation')
|
||||||
default='user', extname='visibility'),
|
interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||||
'implementation': Property('internal name of the implementation class of the module', StringType(),
|
extname='interface_classes')
|
||||||
extname='implementation'),
|
|
||||||
'interface_classes': Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
|
||||||
extname='interface_classes'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# properties, parameters and commands are auto-merged upon subclassing
|
# properties, parameters and commands are auto-merged upon subclassing
|
||||||
parameters = {} #: definition of parameters
|
parameters = {}
|
||||||
commands = {} #: definition of commands
|
commands = {}
|
||||||
|
|
||||||
# reference to the dispatcher (used for sending async updates)
|
# reference to the dispatcher (used for sending async updates)
|
||||||
DISPATCHER = None
|
DISPATCHER = None
|
||||||
@ -112,14 +235,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
|
|
||||||
# handle module properties
|
# handle module properties
|
||||||
# 1) make local copies of properties
|
# 1) make local copies of properties
|
||||||
super(Module, self).__init__()
|
super().__init__()
|
||||||
|
|
||||||
# 2) check and apply properties specified in cfgdict
|
# 2) check and apply properties specified in cfgdict
|
||||||
# specified as '.<propertyname> = <propertyvalue>'
|
# specified as '.<propertyname> = <propertyvalue>'
|
||||||
# (this is for legacy config files only)
|
# (this is for legacy config files only)
|
||||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||||
if k[0] == '.':
|
if k[0] == '.':
|
||||||
if k[1:] in self.__class__.properties:
|
if k[1:] in self.propertyDict:
|
||||||
self.setProperty(k[1:], cfgdict.pop(k))
|
self.setProperty(k[1:], cfgdict.pop(k))
|
||||||
else:
|
else:
|
||||||
raise ConfigError('Module %r has no property %r' %
|
raise ConfigError('Module %r has no property %r' %
|
||||||
@ -127,20 +250,20 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
|
|
||||||
# 3) check and apply properties specified in cfgdict as
|
# 3) check and apply properties specified in cfgdict as
|
||||||
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
||||||
for k in self.__class__.properties:
|
for k in self.propertyDict:
|
||||||
if k in cfgdict:
|
if k in cfgdict:
|
||||||
self.setProperty(k, cfgdict.pop(k))
|
self.setProperty(k, cfgdict.pop(k))
|
||||||
|
|
||||||
# 4) set automatic properties
|
# 4) set automatic properties
|
||||||
mycls = self.__class__
|
mycls = self.__class__
|
||||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||||
self.properties['implementation'] = myclassname
|
self.implementation = myclassname
|
||||||
# list of all 'secop' modules
|
# list of all 'secop' modules
|
||||||
self.properties['interface_classes'] = [
|
# self.interface_classes = [
|
||||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||||
# list of only the 'highest' secop module class
|
# list of only the 'highest' secop module class
|
||||||
self.properties['interface_classes'] = [[
|
self.interface_classes = [
|
||||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
|
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
|
||||||
|
|
||||||
# handle Features
|
# handle Features
|
||||||
# XXX: todo
|
# XXX: todo
|
||||||
@ -149,7 +272,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
# 1) make local copies of parameter objects
|
# 1) make local copies of parameter objects
|
||||||
# they need to be individual per instance since we use them also
|
# they need to be individual per instance since we use them also
|
||||||
# to cache the current value + qualifiers...
|
# to cache the current value + qualifiers...
|
||||||
accessibles = OrderedDict()
|
accessibles = {}
|
||||||
# conversion from exported names to internal attribute names
|
# conversion from exported names to internal attribute names
|
||||||
accessiblename2attr = {}
|
accessiblename2attr = {}
|
||||||
for aname, aobj in self.accessibles.items():
|
for aname, aobj in self.accessibles.items():
|
||||||
@ -158,31 +281,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
if isinstance(aobj, Parameter):
|
if isinstance(aobj, Parameter):
|
||||||
# fix default properties poll and needscfg
|
# fix default properties poll and needscfg
|
||||||
if aobj.poll is None:
|
if aobj.poll is None:
|
||||||
aobj.properties['poll'] = bool(aobj.handler)
|
aobj.poll = bool(aobj.handler)
|
||||||
if aobj.needscfg is None:
|
if aobj.needscfg is None:
|
||||||
aobj.properties['needscfg'] = not aobj.poll
|
aobj.needscfg = not aobj.poll
|
||||||
|
|
||||||
if not self.export: # do not export parameters of a module not exported
|
if not self.export: # do not export parameters of a module not exported
|
||||||
aobj.properties['export'] = False
|
aobj.export = False
|
||||||
if aobj.export:
|
if aobj.export:
|
||||||
if aobj.export is True:
|
if aobj.export is True:
|
||||||
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
||||||
if predefined_obj:
|
if predefined_obj:
|
||||||
if isinstance(aobj, predefined_obj):
|
if isinstance(aobj, predefined_obj):
|
||||||
aobj.setProperty('export', aname)
|
aobj.export = aname
|
||||||
else:
|
else:
|
||||||
raise ProgrammingError("can not use '%s' as name of a %s" %
|
raise ProgrammingError("can not use '%s' as name of a %s" %
|
||||||
(aname, aobj.__class__.__name__))
|
(aname, aobj.__class__.__name__))
|
||||||
else: # create custom parameter
|
else: # create custom parameter
|
||||||
aobj.setProperty('export', '_' + aname)
|
aobj.export = '_' + aname
|
||||||
accessiblename2attr[aobj.export] = aname
|
accessiblename2attr[aobj.export] = aname
|
||||||
accessibles[aname] = aobj
|
accessibles[aname] = aobj
|
||||||
# do not re-use self.accessibles as this is the same for all instances
|
# do not re-use self.accessibles as this is the same for all instances
|
||||||
self.accessibles = accessibles
|
self.accessibles = accessibles
|
||||||
self.accessiblename2attr = accessiblename2attr
|
self.accessiblename2attr = accessiblename2attr
|
||||||
# provide properties to 'filter' out the parameters/commands
|
# provide properties to 'filter' out the parameters/commands
|
||||||
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter))
|
self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
|
||||||
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command))
|
self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
|
||||||
|
|
||||||
# 2) check and apply parameter_properties
|
# 2) check and apply parameter_properties
|
||||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||||
@ -199,6 +322,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
else:
|
else:
|
||||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||||
(self.name, paramname, propname))
|
(self.name, paramname, propname))
|
||||||
|
else:
|
||||||
|
raise ConfigError('Module %s has no Parameter %r!' %
|
||||||
|
(self.name, paramname))
|
||||||
|
|
||||||
# 3) check config for problems:
|
# 3) check config for problems:
|
||||||
# only accept remaining config items specified in parameters
|
# only accept remaining config items specified in parameters
|
||||||
@ -208,7 +334,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
'Module %s:config Parameter %r '
|
'Module %s:config Parameter %r '
|
||||||
'not understood! (use one of %s)' %
|
'not understood! (use one of %s)' %
|
||||||
(self.name, k, ', '.join(list(self.parameters) +
|
(self.name, k, ', '.join(list(self.parameters) +
|
||||||
list(self.__class__.properties))))
|
list(self.propertyDict))))
|
||||||
|
|
||||||
# 4) complain if a Parameter entry has no default value and
|
# 4) complain if a Parameter entry has no default value and
|
||||||
# is not specified in cfgdict and deal with parameters to be written.
|
# is not specified in cfgdict and deal with parameters to be written.
|
||||||
@ -220,6 +346,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
if pname in cfgdict:
|
if pname in cfgdict:
|
||||||
if not pobj.readonly and pobj.initwrite is not False:
|
if not pobj.readonly and pobj.initwrite is not False:
|
||||||
# parameters given in cfgdict have to call write_<pname>
|
# parameters given in cfgdict have to call write_<pname>
|
||||||
|
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
||||||
try:
|
try:
|
||||||
pobj.value = pobj.datatype(cfgdict[pname])
|
pobj.value = pobj.datatype(cfgdict[pname])
|
||||||
except BadValueError as e:
|
except BadValueError as e:
|
||||||
@ -228,7 +355,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
else:
|
else:
|
||||||
if pobj.default is None:
|
if pobj.default is None:
|
||||||
if pobj.needscfg:
|
if pobj.needscfg:
|
||||||
raise ConfigError('Module %s: Parameter %r has no default '
|
raise ConfigError('Parameter %s.%s has no default '
|
||||||
'value and was not given in config!' %
|
'value and was not given in config!' %
|
||||||
(self.name, pname))
|
(self.name, pname))
|
||||||
# we do not want to call the setter for this parameter for now,
|
# we do not want to call the setter for this parameter for now,
|
||||||
@ -243,9 +370,10 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
except BadValueError as e:
|
except BadValueError as e:
|
||||||
raise ProgrammingError('bad default for %s.%s: %s'
|
raise ProgrammingError('bad default for %s.%s: %s'
|
||||||
% (name, pname, e))
|
% (name, pname, e))
|
||||||
if pobj.initwrite:
|
if pobj.initwrite and not pobj.readonly:
|
||||||
# we will need to call write_<pname>
|
# we will need to call write_<pname>
|
||||||
# if this is not desired, the default must not be given
|
# if this is not desired, the default must not be given
|
||||||
|
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
||||||
pobj.value = value
|
pobj.value = value
|
||||||
self.writeDict[pname] = value
|
self.writeDict[pname] = value
|
||||||
else:
|
else:
|
||||||
@ -312,7 +440,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
for cb in cblist:
|
for cb in cblist:
|
||||||
try:
|
try:
|
||||||
cb(arg)
|
cb(arg)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# print(formatExtendedTraceback())
|
# print(formatExtendedTraceback())
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -358,38 +486,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
modobj.announceUpdate(p, value)
|
modobj.announceUpdate(p, value)
|
||||||
self.valueCallbacks[pname].append(cb)
|
self.valueCallbacks[pname].append(cb)
|
||||||
|
|
||||||
|
|
||||||
def isBusy(self, status=None):
|
def isBusy(self, status=None):
|
||||||
"""helper function for treating substates of BUSY correctly"""
|
"""helper function for treating substates of BUSY correctly"""
|
||||||
# defined even for non drivable (used for dynamic polling)
|
# defined even for non drivable (used for dynamic polling)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
"""may be overriden in derived classes to init stuff
|
# may be overriden in derived classes to init stuff
|
||||||
|
|
||||||
after creating the module (no super call needed)
|
|
||||||
"""
|
|
||||||
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
"""may be overriden to do stuff after all modules are intiialized
|
|
||||||
|
|
||||||
no super call needed
|
|
||||||
"""
|
|
||||||
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
|
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
|
||||||
"""runs after init of all modules
|
|
||||||
|
|
||||||
:param started_callback: argument less function to be called when the thread
|
|
||||||
spawned by startModule has finished its initial work
|
|
||||||
:return: None or a timeout value, if different from default (30 sec)
|
|
||||||
|
|
||||||
override this method for doing stuff during startup, after all modules are
|
|
||||||
initialized. do not forget the super call
|
|
||||||
"""
|
|
||||||
mkthread(self.writeInitParams, started_callback)
|
|
||||||
|
|
||||||
def pollOneParam(self, pname):
|
def pollOneParam(self, pname):
|
||||||
"""poll parameter <pname> with proper error handling"""
|
"""poll parameter <pname> with proper error handling"""
|
||||||
try:
|
try:
|
||||||
@ -421,33 +529,33 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
if started_callback:
|
if started_callback:
|
||||||
started_callback()
|
started_callback()
|
||||||
|
|
||||||
|
def startModule(self, started_callback):
|
||||||
|
"""runs after init of all modules
|
||||||
|
|
||||||
|
started_callback to be called when the thread spawned by startModule
|
||||||
|
has finished its initial work
|
||||||
|
might return a timeout value, if different from default
|
||||||
|
"""
|
||||||
|
mkthread(self.writeInitParams, started_callback)
|
||||||
|
|
||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
"""basic readable module"""
|
"""basic readable module"""
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
Status = Enum('Status',
|
Status = Enum('Status',
|
||||||
IDLE = 100,
|
IDLE=100,
|
||||||
WARN = 200,
|
WARN=200,
|
||||||
UNSTABLE = 270,
|
UNSTABLE=270,
|
||||||
ERROR = 400,
|
ERROR=400,
|
||||||
DISABLED = 0,
|
DISABLED=0,
|
||||||
UNKNOWN = 401,
|
UNKNOWN=401,
|
||||||
) #: status codes
|
) #: status codes
|
||||||
parameters = {
|
|
||||||
'value': Parameter('current value of the Module', readonly=True,
|
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||||
datatype=FloatRange(),
|
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||||
poll=True,
|
default=(Status.IDLE, ''), poll=True)
|
||||||
),
|
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||||
'pollinterval': Parameter('sleeptime between polls', default=5,
|
default=5, readonly=False)
|
||||||
readonly=False,
|
|
||||||
datatype=FloatRange(0.1, 120),
|
|
||||||
),
|
|
||||||
'status': Parameter('*(rd, tuple of (Readable.Status, str))* current status of the Module',
|
|
||||||
default=(Status.IDLE, ''),
|
|
||||||
datatype=TupleOf(EnumType(Status), StringType()),
|
|
||||||
readonly=True, poll=True,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, started_callback):
|
||||||
"""start basic polling thread"""
|
"""start basic polling thread"""
|
||||||
@ -496,30 +604,17 @@ class Readable(Module):
|
|||||||
|
|
||||||
class Writable(Readable):
|
class Writable(Readable):
|
||||||
"""basic writable module"""
|
"""basic writable module"""
|
||||||
parameters = {
|
|
||||||
'target': Parameter('target value of the Module',
|
target = Parameter('target value of the module',
|
||||||
default=0, readonly=False, datatype=FloatRange(),
|
default=0, readonly=False, datatype=FloatRange())
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Drivable(Writable):
|
class Drivable(Writable):
|
||||||
"""basic drivable module"""
|
"""basic drivable module"""
|
||||||
|
|
||||||
Status = Enum(Readable.Status, BUSY=300) #: Status codes
|
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||||
|
|
||||||
commands = {
|
status = Parameter(datatype=StatusType(Status)) # override Readable.status
|
||||||
'stop': Command(
|
|
||||||
'cease driving, go to IDLE state',
|
|
||||||
argument=None,
|
|
||||||
result=None
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
overrides = {
|
|
||||||
'status': Override('*(rd, tuple of (Drivable.Status, str))* current status of the Module',
|
|
||||||
datatype=StatusType(Status)),
|
|
||||||
}
|
|
||||||
|
|
||||||
def isBusy(self, status=None):
|
def isBusy(self, status=None):
|
||||||
"""check for busy, treating substates correctly
|
"""check for busy, treating substates correctly
|
||||||
@ -533,7 +628,6 @@ class Drivable(Writable):
|
|||||||
|
|
||||||
returns True when busy, but not finalizing
|
returns True when busy, but not finalizing
|
||||||
"""
|
"""
|
||||||
""""""
|
|
||||||
return 300 <= (status or self.status)[0] < 390
|
return 300 <= (status or self.status)[0] < 390
|
||||||
|
|
||||||
# improved polling: may poll faster if module is BUSY
|
# improved polling: may poll faster if module is BUSY
|
||||||
@ -554,26 +648,16 @@ class Drivable(Writable):
|
|||||||
self.pollOneParam(pname)
|
self.pollOneParam(pname)
|
||||||
return fastpoll
|
return fastpoll
|
||||||
|
|
||||||
def do_stop(self):
|
@Command(None, result=None)
|
||||||
# default implementation of the stop command
|
def stop(self):
|
||||||
# by default does nothing
|
"""cease driving, go to IDLE state"""
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Communicator(Module):
|
class Communicator(Module):
|
||||||
"""basic communication module
|
"""basic abstract communication module"""
|
||||||
|
|
||||||
providing no parameters, but a 'communicate' command.
|
@Command(StringType(), result=StringType())
|
||||||
"""
|
def communicate(self, command):
|
||||||
|
|
||||||
commands = {
|
|
||||||
"communicate": Command("provides the simplest mean to communication",
|
|
||||||
argument=StringType(),
|
|
||||||
result=StringType()
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_communicate(self, command):
|
|
||||||
"""communicate command
|
"""communicate command
|
||||||
|
|
||||||
:param command: the command to be sent
|
:param command: the command to be sent
|
||||||
@ -583,7 +667,7 @@ class Communicator(Module):
|
|||||||
|
|
||||||
|
|
||||||
class Attached(Property):
|
class Attached(Property):
|
||||||
"""a special property, defining an attached module
|
"""a special property, defining an attached modle
|
||||||
|
|
||||||
assign a module name to this property in the cfg file,
|
assign a module name to this property in the cfg file,
|
||||||
and the server will create an attribute with this module
|
and the server will create an attribute with this module
|
||||||
@ -594,7 +678,8 @@ class Attached(Property):
|
|||||||
# we can not put this to properties.py, as it needs datatypes
|
# we can not put this to properties.py, as it needs datatypes
|
||||||
def __init__(self, attrname=None):
|
def __init__(self, attrname=None):
|
||||||
self.attrname = attrname
|
self.attrname = attrname
|
||||||
super().__init__('attached module', StringType())
|
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
|
||||||
|
super().__init__('attached module', StringType(), mandatory=False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
||||||
|
607
secop/params.py
607
secop/params.py
@ -23,154 +23,243 @@
|
|||||||
"""Define classes for Parameters/Commands and Overriding them"""
|
"""Define classes for Parameters/Commands and Overriding them"""
|
||||||
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
import inspect
|
||||||
from inspect import cleandoc
|
|
||||||
|
|
||||||
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
from secop.datatypes import BoolType, CommandType, DataType, \
|
||||||
NoneOr, TextType, IntRange
|
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
|
||||||
from secop.errors import ProgrammingError, BadValueError
|
StringType, StructOf, TextType, TupleOf, ValueType
|
||||||
|
from secop.errors import BadValueError, ProgrammingError
|
||||||
from secop.properties import HasProperties, Property
|
from secop.properties import HasProperties, Property
|
||||||
|
|
||||||
|
UNSET = object() # an argument not given, not even None
|
||||||
class CountedObj:
|
|
||||||
ctr = [0]
|
|
||||||
def __init__(self):
|
|
||||||
cl = self.__class__.ctr
|
|
||||||
cl[0] += 1
|
|
||||||
self.ctr = cl[0]
|
|
||||||
|
|
||||||
|
|
||||||
class Accessible(HasProperties, CountedObj):
|
class Accessible(HasProperties):
|
||||||
'''base class for Parameter and Command'''
|
"""base class for Parameter and Command"""
|
||||||
|
|
||||||
properties = {}
|
kwds = None # is a dict if it might be used as Override
|
||||||
|
|
||||||
def __init__(self, **kwds):
|
def __init__(self, **kwds):
|
||||||
super(Accessible, self).__init__()
|
super().__init__()
|
||||||
# do not use self.properties.update here, as no invalid values should be
|
self.init(kwds)
|
||||||
|
|
||||||
|
def init(self, kwds):
|
||||||
|
# do not use self.propertyValues.update here, as no invalid values should be
|
||||||
# assigned to properties, even not before checkProperties
|
# assigned to properties, even not before checkProperties
|
||||||
for k,v in kwds.items():
|
for k, v in kwds.items():
|
||||||
self.setProperty(k, v)
|
self.setProperty(k, v)
|
||||||
|
|
||||||
def __repr__(self):
|
def inherit(self, cls, owner):
|
||||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join(
|
for base in owner.__bases__:
|
||||||
['%s=%r' % (k, self.properties.get(k, v.default)) for k, v in sorted(self.__class__.properties.items())]))
|
if hasattr(base, self.name):
|
||||||
|
aobj = getattr(base, 'accessibles', {}).get(self.name)
|
||||||
|
if aobj:
|
||||||
|
if not isinstance(aobj, cls):
|
||||||
|
raise ProgrammingError('%s %s.%s can not inherit from a %s' %
|
||||||
|
(cls.__name__, owner.__name__, self.name, aobj.__class__.__name__))
|
||||||
|
# inherit from aobj
|
||||||
|
for pname, value in aobj.propertyValues.items():
|
||||||
|
if pname not in self.propertyValues:
|
||||||
|
self.propertyValues[pname] = value
|
||||||
|
break
|
||||||
|
|
||||||
|
def as_dict(self):
|
||||||
|
return self.propertyValues
|
||||||
|
|
||||||
|
def override(self, value=UNSET, **kwds):
|
||||||
|
"""return a copy, overridden by a bare attribute
|
||||||
|
|
||||||
|
and/or some properties"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
# return a copy of ourselfs
|
"""return a (deep) copy of ourselfs"""
|
||||||
props = dict(self.properties, ctr=self.ctr)
|
raise NotImplementedError
|
||||||
# deep copy, as datatype might be altered from config
|
|
||||||
props['datatype'] = props['datatype'].copy()
|
|
||||||
return type(self)(**props)
|
|
||||||
|
|
||||||
def for_export(self):
|
def for_export(self):
|
||||||
"""prepare for serialisation"""
|
"""prepare for serialisation"""
|
||||||
return self.exportProperties()
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
props = []
|
||||||
|
for k, prop in sorted(self.propertyDict.items()):
|
||||||
|
v = self.propertyValues.get(k, prop.default)
|
||||||
|
if v != prop.default:
|
||||||
|
props.append('%s=%r' % (k, v))
|
||||||
|
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
|
||||||
|
|
||||||
|
|
||||||
class Parameter(Accessible):
|
class Parameter(Accessible):
|
||||||
"""storage for parameter settings + value + qualifiers"""
|
"""defines a parameter
|
||||||
# poll: meaning for the basicPoller:
|
|
||||||
# - True or 1 (poll this every pollinterval)
|
|
||||||
# - positive int (poll every N(th) pollinterval)
|
|
||||||
# - negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval)
|
|
||||||
# note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
|
|
||||||
|
|
||||||
properties = {
|
:param description: description
|
||||||
'description': Property('mandatory description of the parameter', TextType(),
|
:param datatype: the datatype
|
||||||
extname='description', mandatory=True),
|
:param inherit: whether properties not given should be inherited
|
||||||
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
:param kwds: optional properties
|
||||||
extname='datainfo', mandatory=True),
|
"""
|
||||||
'readonly': Property('not changeable via SECoP (default True)', BoolType(),
|
# storage for Parameter settings + value + qualifiers
|
||||||
extname='readonly', mandatory=True),
|
|
||||||
'group': Property('optional parameter group this parameter belongs to', StringType(),
|
|
||||||
extname='group', default=''),
|
|
||||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
|
||||||
extname='visibility', default=1),
|
|
||||||
'constant': Property('optional constant value for constant parameters', ValueType(),
|
|
||||||
extname='constant', default=None, mandatory=False),
|
|
||||||
'default': Property('[internal] default (startup) value of this parameter '
|
|
||||||
'if it can not be read from the hardware.',
|
|
||||||
ValueType(), export=False, default=None, mandatory=False),
|
|
||||||
'export': Property('''
|
|
||||||
[internal] export settings
|
|
||||||
|
|
||||||
* False: not accessible via SECoP.
|
|
||||||
* True: exported, name automatic.
|
|
||||||
* a string: exported with custom name''',
|
|
||||||
OrType(BoolType(), StringType()), export=False, default=True),
|
|
||||||
'poll': Property('''
|
|
||||||
[internal] polling indicator
|
|
||||||
|
|
||||||
may be:
|
|
||||||
|
|
||||||
* None (omitted): will be converted to True/False if handler is/is not None
|
|
||||||
* False or 0 (never poll this parameter)
|
|
||||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
|
||||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
|
||||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
|
||||||
* 3 (REGULAR), polled with pollperiod
|
|
||||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
|
||||||
else polled with pollperiod
|
|
||||||
''',
|
|
||||||
NoneOr(IntRange()), export=False, default=None),
|
|
||||||
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
|
|
||||||
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
|
|
||||||
settable=False, default=False),
|
|
||||||
'handler': Property('[internal] overload the standard read and write functions',
|
|
||||||
ValueType(), export=False, default=None, mandatory=False, settable=False),
|
|
||||||
'initwrite': Property('[internal] write this parameter on initialization'
|
|
||||||
' (default None: write if given in config)',
|
|
||||||
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, description, datatype, *, ctr=None, unit=None, **kwds):
|
description = Property(
|
||||||
|
'mandatory description of the parameter', TextType(),
|
||||||
|
extname='description', mandatory=True)
|
||||||
|
datatype = Property(
|
||||||
|
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||||
|
extname='datainfo', mandatory=True)
|
||||||
|
readonly = Property(
|
||||||
|
'not changeable via SECoP (default True)', BoolType(),
|
||||||
|
extname='readonly', default=True, export='always')
|
||||||
|
group = Property(
|
||||||
|
'optional parameter group this parameter belongs to', StringType(),
|
||||||
|
extname='group', default='')
|
||||||
|
visibility = Property(
|
||||||
|
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
|
extname='visibility', default=1)
|
||||||
|
constant = Property(
|
||||||
|
'optional constant value for constant parameters', ValueType(),
|
||||||
|
extname='constant', default=None)
|
||||||
|
default = Property(
|
||||||
|
'''[internal] default (startup) value of this parameter
|
||||||
|
|
||||||
if ctr is not None:
|
if it can not be read from the hardware''', ValueType(),
|
||||||
self.ctr = ctr
|
export=False, default=None)
|
||||||
|
export = Property(
|
||||||
|
'''[internal] export settings
|
||||||
|
|
||||||
if not isinstance(datatype, DataType):
|
* False: not accessible via SECoP.
|
||||||
if issubclass(datatype, DataType):
|
* True: exported, name automatic.
|
||||||
# goodie: make an instance from a class (forgotten ()???)
|
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||||
datatype = datatype()
|
export=False, default=True)
|
||||||
else:
|
poll = Property(
|
||||||
raise ProgrammingError(
|
'''[internal] polling indicator
|
||||||
'datatype MUST be derived from class DataType!')
|
|
||||||
|
|
||||||
kwds['description'] = cleandoc(description)
|
may be:
|
||||||
kwds['datatype'] = datatype
|
|
||||||
kwds['readonly'] = kwds.get('readonly', True) # for frappy optional, for SECoP mandatory
|
|
||||||
if unit is not None: # for legacy code only
|
|
||||||
datatype.setProperty('unit', unit)
|
|
||||||
super(Parameter, self).__init__(**kwds)
|
|
||||||
|
|
||||||
if self.initwrite and self.readonly:
|
* None (omitted): will be converted to True/False if handler is/is not None
|
||||||
raise ProgrammingError('can not have both readonly and initwrite!')
|
* False or 0 (never poll this parameter)
|
||||||
|
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||||
|
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||||
|
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||||
|
* 3 (REGULAR), polled with pollperiod
|
||||||
|
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||||
|
else polled with pollperiod
|
||||||
|
''', NoneOr(IntRange()),
|
||||||
|
export=False, default=None)
|
||||||
|
needscfg = Property(
|
||||||
|
'[internal] needs value in config', NoneOr(BoolType()),
|
||||||
|
export=False, default=None)
|
||||||
|
optional = Property(
|
||||||
|
'[internal] is this parameter optional?', BoolType(),
|
||||||
|
export=False, settable=False, default=False)
|
||||||
|
handler = Property(
|
||||||
|
'[internal] overload the standard read and write functions', ValueType(),
|
||||||
|
export=False, default=None, settable=False)
|
||||||
|
initwrite = Property(
|
||||||
|
'''[internal] write this parameter on initialization
|
||||||
|
|
||||||
if self.constant is not None:
|
default None: write if given in config''', NoneOr(BoolType()),
|
||||||
self.properties['readonly'] = True
|
export=False, default=None, settable=False)
|
||||||
|
|
||||||
|
# used on the instance copy only
|
||||||
|
value = None
|
||||||
|
timestamp = 0
|
||||||
|
readerror = None
|
||||||
|
|
||||||
|
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
|
||||||
|
super().__init__(**kwds)
|
||||||
|
if datatype is not None:
|
||||||
|
if not isinstance(datatype, DataType):
|
||||||
|
if isinstance(datatype, type) and issubclass(datatype, DataType):
|
||||||
|
# goodie: make an instance from a class (forgotten ()???)
|
||||||
|
datatype = datatype()
|
||||||
|
else:
|
||||||
|
raise ProgrammingError(
|
||||||
|
'datatype MUST be derived from class DataType!')
|
||||||
|
self.datatype = datatype
|
||||||
|
if 'default' in kwds:
|
||||||
|
self.default = datatype(kwds['default'])
|
||||||
|
|
||||||
|
if description is not None:
|
||||||
|
self.description = inspect.cleandoc(description)
|
||||||
|
|
||||||
|
# save for __set_name__
|
||||||
|
self._inherit = inherit
|
||||||
|
self._unit = unit # for legacy code only
|
||||||
|
self._constant = constant
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
# not used yet
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
return instance.parameters[self.name].value
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
obj.announceUpdate(self.name, value)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
if self._inherit:
|
||||||
|
self.inherit(Parameter, owner)
|
||||||
|
|
||||||
|
# check for completeness
|
||||||
|
missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues]
|
||||||
|
if missing_properties:
|
||||||
|
raise ProgrammingError('Parameter %s.%s needs a %s' %
|
||||||
|
(owner.__name__, name, ' and a '.join(missing_properties)))
|
||||||
|
if self._unit is not None:
|
||||||
|
self.datatype.setProperty('unit', self._unit)
|
||||||
|
|
||||||
|
if self._constant is not None:
|
||||||
|
constant = self.datatype(self._constant)
|
||||||
# The value of the `constant` property should be the
|
# The value of the `constant` property should be the
|
||||||
# serialised version of the constant, or unset
|
# serialised version of the constant, or unset
|
||||||
constant = self.datatype(kwds['constant'])
|
self.constant = self.datatype.export_value(constant)
|
||||||
self.properties['constant'] = self.datatype.export_value(constant)
|
self.readonly = True
|
||||||
|
|
||||||
# internal caching: value and timestamp of last change...
|
if 'default' in self.propertyValues:
|
||||||
self.value = self.default
|
# fixes in case datatype has changed
|
||||||
self.timestamp = 0
|
try:
|
||||||
self.readerror = None # if not None, indicates that last read was not successful
|
self.datatype(self.default)
|
||||||
|
except BadValueError:
|
||||||
|
# clear default, if it does not match datatype
|
||||||
|
self.propertyValues.pop('default')
|
||||||
|
|
||||||
|
if self.export is True:
|
||||||
|
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||||
|
self.export = name
|
||||||
|
else:
|
||||||
|
self.export = '_' + name
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
# deep copy, as datatype might be altered from config
|
||||||
|
res = Parameter()
|
||||||
|
res.name = self.name
|
||||||
|
res.init(self.propertyValues)
|
||||||
|
res.datatype = res.datatype.copy()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def override(self, value=UNSET, **kwds):
|
||||||
|
res = self.copy()
|
||||||
|
res.init(kwds)
|
||||||
|
if value is not UNSET:
|
||||||
|
res.value = res.datatype(value)
|
||||||
|
return res
|
||||||
|
|
||||||
def export_value(self):
|
def export_value(self):
|
||||||
return self.datatype.export_value(self.value)
|
return self.datatype.export_value(self.value)
|
||||||
|
|
||||||
|
def for_export(self):
|
||||||
|
return dict(self.exportProperties(), readonly=self.readonly)
|
||||||
|
|
||||||
def getProperties(self):
|
def getProperties(self):
|
||||||
"""get also properties of datatype"""
|
"""get also properties of datatype"""
|
||||||
superProp = super().getProperties().copy()
|
super_prop = super().getProperties().copy()
|
||||||
superProp.update(self.datatype.getProperties())
|
super_prop.update(self.datatype.getProperties())
|
||||||
return superProp
|
return super_prop
|
||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
"""set also properties of datatype"""
|
"""set also properties of datatype"""
|
||||||
if key in self.__class__.properties:
|
if key in self.propertyDict:
|
||||||
super().setProperty(key, value)
|
super().setProperty(key, value)
|
||||||
else:
|
else:
|
||||||
self.datatype.setProperty(key, value)
|
self.datatype.setProperty(key, value)
|
||||||
@ -179,158 +268,168 @@ class Parameter(Accessible):
|
|||||||
super().checkProperties()
|
super().checkProperties()
|
||||||
self.datatype.checkProperties()
|
self.datatype.checkProperties()
|
||||||
|
|
||||||
def for_export(self):
|
|
||||||
"""prepare for serialisation
|
|
||||||
|
|
||||||
readonly is mandatory for serialisation, but not for declaration in classes
|
|
||||||
"""
|
|
||||||
r = super().for_export()
|
|
||||||
if 'readonly' not in r:
|
|
||||||
r['readonly'] = self.__class__.properties['readonly'].default
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
class UnusedClass:
|
|
||||||
# do not derive anything from this!
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Parameters(OrderedDict):
|
|
||||||
"""class storage for Parameters"""
|
|
||||||
def __init__(self, *args, **kwds):
|
|
||||||
self.exported = {} # only for lookups!
|
|
||||||
super(Parameters, self).__init__(*args, **kwds)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if value.export:
|
|
||||||
if isinstance(value, PREDEFINED_ACCESSIBLES.get(key, UnusedClass)):
|
|
||||||
value.properties['export'] = key
|
|
||||||
else:
|
|
||||||
value.properties['export'] = '_' + key
|
|
||||||
self.exported[value.export] = key
|
|
||||||
super(Parameters, self).__setitem__(key, value)
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
return super(Parameters, self).__getitem__(self.exported.get(item, item))
|
|
||||||
|
|
||||||
|
|
||||||
class ParamValue:
|
|
||||||
__slots__ = ['value', 'timestamp']
|
|
||||||
def __init__(self, value, timestamp=0):
|
|
||||||
self.value = value
|
|
||||||
self.timestamp = timestamp
|
|
||||||
|
|
||||||
|
|
||||||
class Commands(Parameters):
|
|
||||||
"""class storage for Commands"""
|
|
||||||
|
|
||||||
|
|
||||||
class Override(CountedObj):
|
|
||||||
"""Stores the overrides to be applied to a Parameter or Command
|
|
||||||
|
|
||||||
note: overrides are applied by the metaclass during class creating
|
|
||||||
reorder=True: use position of Override instead of inherited for the order
|
|
||||||
"""
|
|
||||||
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
|
|
||||||
super(Override, self).__init__()
|
|
||||||
self.kwds = kwds
|
|
||||||
self.reorder = reorder
|
|
||||||
# allow to override description and datatype without keyword
|
|
||||||
if description:
|
|
||||||
self.kwds['description'] = cleandoc(description)
|
|
||||||
if datatype is not None:
|
|
||||||
self.kwds['datatype'] = datatype
|
|
||||||
# for now, do not use the Override ctr
|
|
||||||
# self.kwds['ctr'] = self.ctr
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
|
|
||||||
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
|
|
||||||
|
|
||||||
def apply(self, obj):
|
|
||||||
if isinstance(obj, Accessible):
|
|
||||||
props = obj.properties.copy()
|
|
||||||
props['datatype'] = props['datatype'].copy()
|
|
||||||
if isinstance(obj, Parameter):
|
|
||||||
if 'constant' in self.kwds:
|
|
||||||
constant = obj.datatype(self.kwds.pop('constant'))
|
|
||||||
self.kwds['constant'] = obj.datatype.export_value(constant)
|
|
||||||
self.kwds['readonly'] = True
|
|
||||||
if 'datatype' in self.kwds and 'default' not in self.kwds:
|
|
||||||
try:
|
|
||||||
self.kwds['datatype'](obj.default)
|
|
||||||
except BadValueError:
|
|
||||||
# clear default, if it does not match datatype
|
|
||||||
props['default'] = None
|
|
||||||
props.update(self.kwds)
|
|
||||||
|
|
||||||
if self.reorder:
|
|
||||||
return type(obj)(**props)
|
|
||||||
return type(obj)(ctr=self.ctr, **props)
|
|
||||||
raise ProgrammingError(
|
|
||||||
"Overrides can only be applied to Accessibles, %r is none!" % obj)
|
|
||||||
|
|
||||||
|
|
||||||
class Command(Accessible):
|
class Command(Accessible):
|
||||||
"""storage for Commands settings (description + call signature...)
|
"""decorator to turn a method into a command
|
||||||
|
|
||||||
|
:param argument: the datatype of the argument or None
|
||||||
|
:param result: the datatype of the result or None
|
||||||
|
:param inherit: whether properties not given should be inherited
|
||||||
|
:param kwds: optional properties
|
||||||
"""
|
"""
|
||||||
properties = {
|
|
||||||
'description': Property('description of the command', TextType(),
|
|
||||||
extname='description', export=True, mandatory=True),
|
|
||||||
'group': Property('optional command group of the command.', StringType(),
|
|
||||||
extname='group', export=True, default=''),
|
|
||||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
|
||||||
extname='visibility', export=True, default=1),
|
|
||||||
'export': Property('''
|
|
||||||
[internal] export settings
|
|
||||||
|
|
||||||
- False: not accessible via SECoP.
|
|
||||||
- True: exported, name automatic.
|
|
||||||
- a string: exported with custom name''',
|
|
||||||
OrType(BoolType(), StringType()), export=False, default=True),
|
|
||||||
'optional': Property('[internal] is the command optional to implement? (vs. mandatory)',
|
|
||||||
BoolType(), export=False, default=False, settable=False),
|
|
||||||
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
|
|
||||||
DataTypeType(), extname='datainfo', mandatory=True),
|
|
||||||
'argument': Property('datatype of the argument to the command, or None.',
|
|
||||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
|
||||||
'result': Property('datatype of the result from the command, or None.',
|
|
||||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, description, ctr=None, **kwds):
|
description = Property(
|
||||||
kwds['description'] = cleandoc(description)
|
'description of the Command', TextType(),
|
||||||
kwds['datatype'] = CommandType(kwds.get('argument', None), kwds.get('result', None))
|
extname='description', export=True, mandatory=True)
|
||||||
super(Command, self).__init__(**kwds)
|
group = Property(
|
||||||
if ctr is not None:
|
'optional command group of the command.', StringType(),
|
||||||
self.ctr = ctr
|
extname='group', export=True, default='')
|
||||||
|
visibility = Property(
|
||||||
|
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
|
extname='visibility', export=True, default=1)
|
||||||
|
export = Property(
|
||||||
|
'''[internal] export settings
|
||||||
|
|
||||||
@property
|
* False: not accessible via SECoP.
|
||||||
def argument(self):
|
* True: exported, name automatic.
|
||||||
return self.datatype.argument
|
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||||
|
export=False, default=True)
|
||||||
|
optional = Property(
|
||||||
|
'[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||||
|
export=False, default=False, settable=False)
|
||||||
|
datatype = Property(
|
||||||
|
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||||
|
DataTypeType(), extname='datainfo', export='always')
|
||||||
|
argument = Property(
|
||||||
|
'datatype of the argument to the command, or None', NoneOr(DataTypeType()),
|
||||||
|
export=False, mandatory=True)
|
||||||
|
result = Property(
|
||||||
|
'datatype of the result from the command, or None', NoneOr(DataTypeType()),
|
||||||
|
export=False, mandatory=True)
|
||||||
|
|
||||||
@property
|
func = None
|
||||||
def result(self):
|
|
||||||
return self.datatype.result
|
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||||
|
super().__init__(**kwds)
|
||||||
|
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||||
|
# normal case
|
||||||
|
if argument is False and result:
|
||||||
|
argument = None
|
||||||
|
if argument is not False:
|
||||||
|
if isinstance(argument, (tuple, list)):
|
||||||
|
# goodie: treat as TupleOf
|
||||||
|
argument = TupleOf(*argument)
|
||||||
|
self.argument = argument
|
||||||
|
self.result = result
|
||||||
|
else:
|
||||||
|
# goodie: allow @Command instead of @Command()
|
||||||
|
self.func = argument # this is the wrapped method!
|
||||||
|
if argument.__doc__:
|
||||||
|
self.description = inspect.cleandoc(argument.__doc__)
|
||||||
|
self.name = self.func.__name__
|
||||||
|
self._inherit = inherit # save for __set_name__
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self.name = name
|
||||||
|
if self.func is None:
|
||||||
|
raise ProgrammingError('Command %s.%s must be used as a method decorator' %
|
||||||
|
(owner.__name__, name))
|
||||||
|
if self._inherit:
|
||||||
|
self.inherit(Command, owner)
|
||||||
|
|
||||||
|
self.datatype = CommandType(self.argument, self.result)
|
||||||
|
if self.export is True:
|
||||||
|
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||||
|
self.export = name
|
||||||
|
else:
|
||||||
|
self.export = '_' + name
|
||||||
|
|
||||||
|
def __get__(self, obj, owner=None):
|
||||||
|
if obj is None:
|
||||||
|
return self
|
||||||
|
if not self.func:
|
||||||
|
raise ProgrammingError('Command %s not properly configured' % self.name)
|
||||||
|
return self.func.__get__(obj, owner)
|
||||||
|
|
||||||
|
def __call__(self, func):
|
||||||
|
if 'description' not in self.propertyValues and func.__doc__:
|
||||||
|
self.description = inspect.cleandoc(func.__doc__)
|
||||||
|
self.func = func
|
||||||
|
return self
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
res = Command()
|
||||||
|
res.name = self.name
|
||||||
|
res.func = self.func
|
||||||
|
res.init(self.propertyValues)
|
||||||
|
if res.argument:
|
||||||
|
res.argument = res.argument.copy()
|
||||||
|
if res.result:
|
||||||
|
res.result = res.result.copy()
|
||||||
|
res.datatype = CommandType(res.argument, res.result)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def override(self, value=UNSET, **kwds):
|
||||||
|
res = self.copy()
|
||||||
|
res.init(kwds)
|
||||||
|
if value is not UNSET:
|
||||||
|
res.func = value
|
||||||
|
return res
|
||||||
|
|
||||||
|
def do(self, module_obj, argument):
|
||||||
|
"""perform function call
|
||||||
|
|
||||||
|
:param module_obj: the module on which the command is to be executed
|
||||||
|
:param argument: the argument from the do command
|
||||||
|
:returns: the return value converted to the result type
|
||||||
|
|
||||||
|
- when the argument type is TupleOf, the function is called with multiple arguments
|
||||||
|
- when the argument type is StructOf, the function is called with keyworded arguments
|
||||||
|
- the validity of the argument/s is/are checked
|
||||||
|
"""
|
||||||
|
func = self.__get__(module_obj)
|
||||||
|
if self.argument:
|
||||||
|
# validate
|
||||||
|
argument = self.argument(argument)
|
||||||
|
if isinstance(self.argument, TupleOf):
|
||||||
|
res = func(*argument)
|
||||||
|
elif isinstance(self.argument, StructOf):
|
||||||
|
res = func(**argument)
|
||||||
|
else:
|
||||||
|
res = func(argument)
|
||||||
|
else:
|
||||||
|
if argument is not None:
|
||||||
|
raise BadValueError('%s.%s takes no arguments' % (module_obj.__class__.__name__, self.name))
|
||||||
|
res = func()
|
||||||
|
if self.result:
|
||||||
|
return self.result(res)
|
||||||
|
return None # silently ignore the result from the method
|
||||||
|
|
||||||
|
def for_export(self):
|
||||||
|
return self.exportProperties()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
result = super().__repr__()
|
||||||
|
return result[:-1] + ', %r)' % self.func if self.func else result
|
||||||
|
|
||||||
|
|
||||||
# list of predefined accessibles with their type
|
# list of predefined accessibles with their type
|
||||||
PREDEFINED_ACCESSIBLES = dict(
|
PREDEFINED_ACCESSIBLES = dict(
|
||||||
value = Parameter,
|
value=Parameter,
|
||||||
status = Parameter,
|
status=Parameter,
|
||||||
target = Parameter,
|
target=Parameter,
|
||||||
pollinterval = Parameter,
|
pollinterval=Parameter,
|
||||||
ramp = Parameter,
|
ramp=Parameter,
|
||||||
user_ramp = Parameter,
|
user_ramp=Parameter,
|
||||||
setpoint = Parameter,
|
setpoint=Parameter,
|
||||||
time_to_target = Parameter,
|
time_to_target=Parameter,
|
||||||
unit = Parameter, # reserved name
|
unit=Parameter, # reserved name
|
||||||
loglevel = Parameter, # reserved name
|
loglevel=Parameter, # reserved name
|
||||||
mode = Parameter, # reserved name
|
mode=Parameter, # reserved name
|
||||||
stop = Command,
|
stop=Command,
|
||||||
reset = Command,
|
reset=Command,
|
||||||
go = Command,
|
go=Command,
|
||||||
abort = Command,
|
abort=Command,
|
||||||
shutdown = Command,
|
shutdown=Command,
|
||||||
communicate = Command,
|
communicate=Command,
|
||||||
)
|
)
|
||||||
|
@ -34,10 +34,11 @@ Usage examples:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from threading import Event
|
|
||||||
from heapq import heapify, heapreplace
|
from heapq import heapify, heapreplace
|
||||||
from secop.lib import mkthread
|
from threading import Event
|
||||||
|
|
||||||
from secop.errors import ProgrammingError
|
from secop.errors import ProgrammingError
|
||||||
|
from secop.lib import mkthread
|
||||||
|
|
||||||
# poll types:
|
# poll types:
|
||||||
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
||||||
@ -166,8 +167,8 @@ class Poller(PollerBase):
|
|||||||
continue # only one poller per handler
|
continue # only one poller per handler
|
||||||
handlers.add(pobj.handler)
|
handlers.add(pobj.handler)
|
||||||
# placeholders 0 are used for due, lastdue and idx
|
# placeholders 0 are used for due, lastdue and idx
|
||||||
self.queues[polltype].append((0, 0,
|
self.queues[polltype].append(
|
||||||
(0, module, pobj, pname, factors[polltype])))
|
(0, 0, (0, module, pobj, pname, factors[polltype])))
|
||||||
|
|
||||||
def poll_next(self, polltype):
|
def poll_next(self, polltype):
|
||||||
"""try to poll next item
|
"""try to poll next item
|
||||||
|
@ -23,11 +23,44 @@
|
|||||||
"""Define validated data types."""
|
"""Define validated data types."""
|
||||||
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
import inspect
|
||||||
from inspect import cleandoc
|
import sys
|
||||||
|
|
||||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||||
from secop.lib.classdoc import append_to_doc, indent_description
|
|
||||||
|
|
||||||
|
class HasDescriptorMeta(type):
|
||||||
|
def __new__(cls, name, bases, attrs):
|
||||||
|
newtype = type.__new__(cls, name, bases, attrs)
|
||||||
|
if sys.version_info < (3, 6):
|
||||||
|
# support older python versions
|
||||||
|
for key, attr in attrs.items():
|
||||||
|
if hasattr(attr, '__set_name__'):
|
||||||
|
attr.__set_name__(newtype, key)
|
||||||
|
newtype.__init_subclass__()
|
||||||
|
return newtype
|
||||||
|
|
||||||
|
|
||||||
|
class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||||
|
@classmethod
|
||||||
|
def __init_subclass__(cls):
|
||||||
|
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||||
|
bad = [k for k, v in cls.__dict__.items()
|
||||||
|
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||||
|
if bad:
|
||||||
|
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filterDescriptors(cls, filter_type):
|
||||||
|
res = {}
|
||||||
|
for name in dir(cls):
|
||||||
|
desc = getattr(cls, name, None)
|
||||||
|
if isinstance(desc, filter_type):
|
||||||
|
res[name] = desc
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
UNSET = object() # an unset value, not even None
|
||||||
|
|
||||||
|
|
||||||
# storage for 'properties of a property'
|
# storage for 'properties of a property'
|
||||||
@ -35,175 +68,148 @@ class Property:
|
|||||||
"""base class holding info about a property
|
"""base class holding info about a property
|
||||||
|
|
||||||
:param description: mandatory
|
:param description: mandatory
|
||||||
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed,
|
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed!
|
||||||
also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc.
|
also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc.
|
||||||
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
||||||
when they match the default
|
when they match the default
|
||||||
:param extname: external name
|
:param extname: external name
|
||||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given
|
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given.
|
||||||
|
special value 'always': export also when matching the default
|
||||||
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
||||||
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
||||||
:param settable: settable from the cfg file
|
:param settable: settable from the cfg file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# note: this is intended to be used on base classes.
|
# note: this is intended to be used on base classes.
|
||||||
# the VALUES of the properties are on the instances!
|
# the VALUES of the properties are on the instances!
|
||||||
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
|
def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None,
|
||||||
|
settable=True, value=UNSET, name=''):
|
||||||
if not callable(datatype):
|
if not callable(datatype):
|
||||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||||
self.description = cleandoc(description)
|
self.description = inspect.cleandoc(description)
|
||||||
self.default = datatype.default if default is None else datatype(default)
|
self.default = datatype.default if default is UNSET else datatype(default)
|
||||||
self.datatype = datatype
|
self.datatype = datatype
|
||||||
self.extname = extname
|
self.extname = extname
|
||||||
self.export = export or bool(extname)
|
self.export = export or bool(extname)
|
||||||
if mandatory is None:
|
if mandatory is None:
|
||||||
mandatory = default is None
|
mandatory = default is UNSET
|
||||||
self.mandatory = mandatory
|
self.mandatory = mandatory
|
||||||
self.settable = settable or mandatory # settable means settable from the cfg file
|
self.settable = settable or mandatory # settable means settable from the cfg file
|
||||||
|
self.value = UNSET if value is UNSET else datatype(value)
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __get__(self, instance, owner):
|
||||||
|
if instance is None:
|
||||||
|
return self
|
||||||
|
return instance.propertyValues.get(self.name, self.default)
|
||||||
|
|
||||||
|
def __set__(self, instance, value):
|
||||||
|
instance.propertyValues[self.name] = self.datatype(value)
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self.name = name
|
||||||
|
if self.export and not self.extname:
|
||||||
|
self.extname = '_' + name
|
||||||
|
if self.description == '_':
|
||||||
|
# the programmer indicates, that the name is already speaking for itself
|
||||||
|
self.description = name.replace('_', ' ')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % (
|
extras = ['default=%s' % repr(self.default)]
|
||||||
self.description, self.datatype, self.default, self.extname, self.export,
|
if self.export:
|
||||||
self.mandatory, self.settable)
|
extras.append('extname=%r' % self.extname)
|
||||||
|
extras.append('export=%r' % self.export)
|
||||||
|
if self.mandatory:
|
||||||
|
extras.append('mandatory=True')
|
||||||
|
if not self.settable:
|
||||||
|
extras.append('settable=False')
|
||||||
|
if self.value is not UNSET:
|
||||||
|
extras.append('value=%s' % repr(self.value))
|
||||||
|
if not self.name:
|
||||||
|
extras.append('name=%r' % self.name)
|
||||||
|
return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras))
|
||||||
|
|
||||||
|
|
||||||
class Properties(OrderedDict):
|
class HasProperties(HasDescriptors):
|
||||||
"""a collection of `Property` objects
|
propertyValues = None
|
||||||
|
|
||||||
checks values upon assignment.
|
|
||||||
You can either assign a Property object, or a value
|
|
||||||
(which must pass the validator of the already existing Property)
|
|
||||||
"""
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if not isinstance(value, Property):
|
|
||||||
raise ProgrammingError('setting property %r on classes is not supported!' % key)
|
|
||||||
# make sure, extname is valid if export is True
|
|
||||||
if not value.extname and value.export:
|
|
||||||
value.extname = '_%s' % key # generate custom key
|
|
||||||
elif value.extname and not value.export:
|
|
||||||
value.export = True
|
|
||||||
OrderedDict.__setitem__(self, key, value)
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
raise ProgrammingError('deleting Properties is not supported!')
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMeta(type):
|
|
||||||
"""Metaclass for HasProperties
|
|
||||||
|
|
||||||
joining the class's properties with those of base classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __new__(cls, name, bases, attrs):
|
|
||||||
newtype = type.__new__(cls, name, bases, attrs)
|
|
||||||
if '__constructed__' in attrs:
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
newtype = cls.__join_properties__(newtype, name, bases, attrs)
|
|
||||||
|
|
||||||
attrs['__constructed__'] = True
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __join_properties__(cls, newtype, name, bases, attrs):
|
|
||||||
# merge properties from all sub-classes
|
|
||||||
properties = Properties()
|
|
||||||
for base in reversed(bases):
|
|
||||||
properties.update(getattr(base, "properties", {}))
|
|
||||||
# update with properties from new class
|
|
||||||
properties.update(attrs.get('properties', {}))
|
|
||||||
newtype.properties = properties
|
|
||||||
|
|
||||||
# generate getters
|
|
||||||
for k, po in properties.items():
|
|
||||||
|
|
||||||
def getter(self, pname=k):
|
|
||||||
val = self.__class__.properties[pname].default
|
|
||||||
return self.properties.get(pname, val)
|
|
||||||
|
|
||||||
if k in attrs and not isinstance(attrs[k], property):
|
|
||||||
if callable(attrs[k]):
|
|
||||||
raise ProgrammingError('%r: property %r collides with method'
|
|
||||||
% (newtype, k))
|
|
||||||
# store the attribute value for putting on the instance later
|
|
||||||
try:
|
|
||||||
# for inheritance reasons, it seems best to store it as a renamed attribute
|
|
||||||
setattr(newtype, '_initProp_' + k, po.datatype(attrs[k]))
|
|
||||||
except BadValueError:
|
|
||||||
raise ProgrammingError('%r: property %r can not be set to %r'
|
|
||||||
% (newtype, k, attrs[k]))
|
|
||||||
setattr(newtype, k, property(getter))
|
|
||||||
|
|
||||||
# add property information to the doc string
|
|
||||||
def fmt_property(name, prop):
|
|
||||||
desc = indent_description(prop)
|
|
||||||
if '(' in desc[0:2]:
|
|
||||||
dtinfo = ''
|
|
||||||
else:
|
|
||||||
dtinfo = [prop.datatype.short_doc(), None if prop.export else 'hidden']
|
|
||||||
dtinfo = ', '.join(filter(None, dtinfo))
|
|
||||||
if dtinfo:
|
|
||||||
dtinfo = '*(%s)* ' % dtinfo
|
|
||||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
|
||||||
|
|
||||||
append_to_doc(newtype, 'properties', 'SECOP Properties',
|
|
||||||
'properties', attrs.get("properties", {}), fmt_property)
|
|
||||||
|
|
||||||
return newtype
|
|
||||||
|
|
||||||
|
|
||||||
class HasProperties(metaclass=PropertyMeta):
|
|
||||||
properties = {}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HasProperties, self).__init__()
|
super(HasProperties, self).__init__()
|
||||||
self.initProperties()
|
|
||||||
|
|
||||||
def initProperties(self):
|
|
||||||
# store property values in the instance, keep descriptors on the class
|
# store property values in the instance, keep descriptors on the class
|
||||||
self.properties = {}
|
self.propertyValues = {}
|
||||||
# pre-init with properties default value (if any)
|
# pre-init
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
value = getattr(self, '_initProp_' + pn, self)
|
if po.value is not UNSET:
|
||||||
if value is not self: # property value was given as attribute
|
self.setProperty(pn, po.value)
|
||||||
self.properties[pn] = value
|
|
||||||
elif not po.mandatory:
|
@classmethod
|
||||||
self.properties[pn] = po.default
|
def __init_subclass__(cls):
|
||||||
|
super().__init_subclass__()
|
||||||
|
# raise an error when an attribute is a tuple with one single descriptor as element
|
||||||
|
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||||
|
bad = [k for k, v in cls.__dict__.items()
|
||||||
|
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||||
|
if bad:
|
||||||
|
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||||
|
properties = {}
|
||||||
|
for base in cls.__bases__:
|
||||||
|
properties.update(getattr(base, 'propertyDict', {}))
|
||||||
|
properties.update(cls.filterDescriptors(Property))
|
||||||
|
cls.propertyDict = properties
|
||||||
|
# treat overriding properties with bare values
|
||||||
|
for pn, po in properties.items():
|
||||||
|
value = cls.__dict__.get(pn, po)
|
||||||
|
if not isinstance(value, Property): # attribute is a bare value
|
||||||
|
po = Property(**po.__dict__)
|
||||||
|
try:
|
||||||
|
po.value = po.datatype(value)
|
||||||
|
except BadValueError:
|
||||||
|
for base in cls.__bases__:
|
||||||
|
if pn in getattr(base, 'propertyDict', {}):
|
||||||
|
if callable(value):
|
||||||
|
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||||
|
(cls.__name__, pn, base.__name__))
|
||||||
|
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||||
|
(cls.__name__, pn, value))
|
||||||
|
cls.propertyDict[pn] = po
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
"""validates properties and checks for min... <= max..."""
|
"""validates properties and checks for min... <= max..."""
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
if po.export and po.mandatory:
|
if po.mandatory:
|
||||||
if pn not in self.properties:
|
if pn not in self.propertyDict:
|
||||||
name = getattr(self, 'name', repr(self))
|
name = getattr(self, 'name', self.__class__.__name__)
|
||||||
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||||
# apply validator (which may complain further)
|
# apply validator (which may complain further)
|
||||||
self.properties[pn] = po.datatype(self.properties[pn])
|
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
if pn.startswith('min'):
|
if pn.startswith('min'):
|
||||||
maxname = 'max' + pn[3:]
|
maxname = 'max' + pn[3:]
|
||||||
minval = self.properties[pn]
|
minval = self.propertyValues.get(pn, po.default)
|
||||||
maxval = self.properties.get(maxname, minval)
|
maxval = self.propertyValues.get(maxname, minval)
|
||||||
if minval > maxval:
|
if minval > maxval:
|
||||||
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
||||||
|
|
||||||
|
|
||||||
def getProperties(self):
|
def getProperties(self):
|
||||||
return self.__class__.properties
|
return self.propertyDict
|
||||||
|
|
||||||
def exportProperties(self):
|
def exportProperties(self):
|
||||||
# export properties which have
|
# export properties which have
|
||||||
# export=True and
|
# export=True and
|
||||||
# mandatory=True or non_default=True
|
# mandatory=True or non_default=True
|
||||||
res = {}
|
res = {}
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.propertyDict.items():
|
||||||
val = self.properties.get(pn, None)
|
val = self.propertyValues.get(pn, po.default)
|
||||||
if po.export and (po.mandatory or val != po.default):
|
if po.export and (po.export == 'always' or val != po.default):
|
||||||
try:
|
try:
|
||||||
val = po.datatype.export_value(val)
|
val = po.datatype.export_value(val)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass # for properties, accept simple datatypes without export_value
|
pass # for properties, accept simple datatypes without export_value
|
||||||
res[po.extname] = val
|
res[po.extname] = val
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def setProperty(self, key, value):
|
def setProperty(self, key, value):
|
||||||
self.properties[key] = self.__class__.properties[key].datatype(value)
|
# this is overwritten by Param.setProperty and DataType.setProperty
|
||||||
|
# in oder to extend setting to inner properties
|
||||||
|
# otherwise direct setting of self.<key> = value is preferred
|
||||||
|
self.propertyValues[key] = self.propertyDict[key].datatype(value)
|
||||||
|
@ -42,8 +42,8 @@ import threading
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from time import time as currenttime
|
from time import time as currenttime
|
||||||
|
|
||||||
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
|
from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError, fmt_error
|
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
|
||||||
from secop.params import Parameter
|
from secop.params import Parameter
|
||||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||||
@ -54,9 +54,9 @@ def make_update(modulename, pobj):
|
|||||||
if pobj.readerror:
|
if pobj.readerror:
|
||||||
return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||||
# error-report !
|
# error-report !
|
||||||
[pobj.readerror.name, fmt_error(pobj.readerror), dict(t=pobj.timestamp)])
|
[pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)])
|
||||||
return (EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
return (EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||||
|
|
||||||
|
|
||||||
class Dispatcher:
|
class Dispatcher:
|
||||||
@ -109,7 +109,7 @@ class Dispatcher:
|
|||||||
self._subscriptions.setdefault(eventname, set()).add(conn)
|
self._subscriptions.setdefault(eventname, set()).add(conn)
|
||||||
|
|
||||||
def unsubscribe(self, conn, eventname):
|
def unsubscribe(self, conn, eventname):
|
||||||
if not ':' in eventname:
|
if ':' not in eventname:
|
||||||
# also remove 'more specific' subscriptions
|
# also remove 'more specific' subscriptions
|
||||||
for k, v in self._subscriptions.items():
|
for k, v in self._subscriptions.items():
|
||||||
if k.startswith('%s:' % eventname):
|
if k.startswith('%s:' % eventname):
|
||||||
@ -177,7 +177,7 @@ class Dispatcher:
|
|||||||
result = {'modules': OrderedDict()}
|
result = {'modules': OrderedDict()}
|
||||||
for modulename in self._export:
|
for modulename in self._export:
|
||||||
module = self.get_module(modulename)
|
module = self.get_module(modulename)
|
||||||
if not module.properties.get('export', False):
|
if not module.export:
|
||||||
continue
|
continue
|
||||||
# some of these need rework !
|
# some of these need rework !
|
||||||
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
||||||
@ -186,7 +186,7 @@ class Dispatcher:
|
|||||||
result['modules'][modulename] = mod_desc
|
result['modules'][modulename] = mod_desc
|
||||||
result['equipment_id'] = self.equipment_id
|
result['equipment_id'] = self.equipment_id
|
||||||
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
||||||
result['version'] = '2019.08'
|
result['version'] = '2021.02'
|
||||||
result.update(self.nodeprops)
|
result.update(self.nodeprops)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -195,40 +195,24 @@ class Dispatcher:
|
|||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||||
|
|
||||||
cmdname = moduleobj.commands.exported.get(exportedname, None)
|
cname = moduleobj.accessiblename2attr.get(exportedname)
|
||||||
if cmdname is None:
|
cobj = moduleobj.commands.get(cname)
|
||||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname))
|
if cobj is None:
|
||||||
cmdspec = moduleobj.commands[cmdname]
|
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||||
if argument is None and cmdspec.datatype.argument is not None:
|
|
||||||
raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname))
|
|
||||||
|
|
||||||
if argument is not None and cmdspec.datatype.argument is None:
|
|
||||||
raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname))
|
|
||||||
|
|
||||||
if cmdspec.datatype.argument:
|
|
||||||
# validate!
|
|
||||||
argument = cmdspec.datatype(argument)
|
|
||||||
|
|
||||||
# now call func
|
# now call func
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
func = getattr(moduleobj, 'do_' + cmdname)
|
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
||||||
res = func() if argument is None else func(argument)
|
|
||||||
|
|
||||||
# pipe through cmdspec.datatype.result
|
|
||||||
if cmdspec.datatype.result:
|
|
||||||
res = cmdspec.datatype.result(res)
|
|
||||||
|
|
||||||
return res, dict(t=currenttime())
|
|
||||||
|
|
||||||
def _setParameterValue(self, modulename, exportedname, value):
|
def _setParameterValue(self, modulename, exportedname, value):
|
||||||
moduleobj = self.get_module(modulename)
|
moduleobj = self.get_module(modulename)
|
||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||||
|
|
||||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||||
if pname is None:
|
pobj = moduleobj.parameters.get(pname)
|
||||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
if pobj is None:
|
||||||
pobj = moduleobj.parameters[pname]
|
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||||
if pobj.constant is not None:
|
if pobj.constant is not None:
|
||||||
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
||||||
% (modulename, pname))
|
% (modulename, pname))
|
||||||
@ -252,10 +236,10 @@ class Dispatcher:
|
|||||||
if moduleobj is None:
|
if moduleobj is None:
|
||||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||||
|
|
||||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||||
if pname is None:
|
pobj = moduleobj.parameters.get(pname)
|
||||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
if pobj is None:
|
||||||
pobj = moduleobj.parameters[pname]
|
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||||
if pobj.constant is not None:
|
if pobj.constant is not None:
|
||||||
# really needed? we could just construct a readreply instead....
|
# really needed? we could just construct a readreply instead....
|
||||||
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||||
@ -321,15 +305,13 @@ class Dispatcher:
|
|||||||
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
|
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
|
||||||
|
|
||||||
def handle_do(self, conn, specifier, data):
|
def handle_do(self, conn, specifier, data):
|
||||||
# XXX: should this be done asyncron? we could just return the reply in
|
|
||||||
# that case
|
|
||||||
modulename, cmd = specifier.split(':', 1)
|
modulename, cmd = specifier.split(':', 1)
|
||||||
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
||||||
|
|
||||||
def handle_ping(self, conn, specifier, data):
|
def handle_ping(self, conn, specifier, data):
|
||||||
if data:
|
if data:
|
||||||
raise ProtocolError('ping requests don\'t take data!')
|
raise ProtocolError('ping requests don\'t take data!')
|
||||||
return (HEARTBEATREPLY, specifier, [None, {'t':currenttime()}])
|
return (HEARTBEATREPLY, specifier, [None, {'t': currenttime()}])
|
||||||
|
|
||||||
def handle_activate(self, conn, specifier, data):
|
def handle_activate(self, conn, specifier, data):
|
||||||
if data:
|
if data:
|
||||||
|
@ -25,6 +25,7 @@ import json
|
|||||||
|
|
||||||
EOL = b'\n'
|
EOL = b'\n'
|
||||||
|
|
||||||
|
|
||||||
def encode_msg_frame(action, specifier=None, data=None):
|
def encode_msg_frame(action, specifier=None, data=None):
|
||||||
""" encode a msg_triple into an msg_frame, ready to be sent
|
""" encode a msg_triple into an msg_frame, ready to be sent
|
||||||
|
|
||||||
|
@ -21,13 +21,12 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""provides tcp interface to the SECoP Server"""
|
"""provides tcp interface to the SECoP Server"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import socket
|
import socket
|
||||||
import socketserver
|
import socketserver
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
|
||||||
|
|
||||||
from secop.datatypes import StringType, BoolType
|
from secop.datatypes import BoolType, StringType
|
||||||
from secop.errors import SECoPError
|
from secop.errors import SECoPError
|
||||||
from secop.lib import formatException, \
|
from secop.lib import formatException, \
|
||||||
formatExtendedStack, formatExtendedTraceback
|
formatExtendedStack, formatExtendedTraceback
|
||||||
@ -36,7 +35,6 @@ from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
|
|||||||
from secop.protocol.messages import ERRORPREFIX, \
|
from secop.protocol.messages import ERRORPREFIX, \
|
||||||
HELPREPLY, HELPREQUEST, HelpMessage
|
HELPREPLY, HELPREQUEST, HelpMessage
|
||||||
|
|
||||||
|
|
||||||
DEF_PORT = 10767
|
DEF_PORT = 10767
|
||||||
MESSAGE_READ_SIZE = 1024
|
MESSAGE_READ_SIZE = 1024
|
||||||
HELP = HELPREQUEST.encode()
|
HELP = HELPREQUEST.encode()
|
||||||
@ -134,7 +132,6 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
|
|||||||
if result[0].startswith(ERRORPREFIX) and not detailed_errors:
|
if result[0].startswith(ERRORPREFIX) and not detailed_errors:
|
||||||
# strip extra information
|
# strip extra information
|
||||||
result[2][2].clear()
|
result[2][2].clear()
|
||||||
result[2][2]['t'] = time.time()
|
|
||||||
self.send_reply(result)
|
self.send_reply(result)
|
||||||
|
|
||||||
def send_reply(self, data):
|
def send_reply(self, data):
|
||||||
|
@ -80,7 +80,6 @@ REQUEST2REPLY = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
HelpMessage = """Try one of the following:
|
HelpMessage = """Try one of the following:
|
||||||
'%s' to query protocol version
|
'%s' to query protocol version
|
||||||
'%s' to read the description
|
'%s' to read the description
|
||||||
|
@ -34,12 +34,12 @@ simplifications:
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import secop.protocol.dispatcher
|
|
||||||
import secop.errors
|
|
||||||
from secop.protocol.messages import DESCRIPTIONREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY,\
|
|
||||||
READREQUEST, WRITEREQUEST, COMMANDREQUEST
|
|
||||||
import secop.client
|
import secop.client
|
||||||
|
import secop.errors
|
||||||
|
import secop.protocol.dispatcher
|
||||||
from secop.lib.multievent import MultiEvent
|
from secop.lib.multievent import MultiEvent
|
||||||
|
from secop.protocol.messages import COMMANDREQUEST, DESCRIPTIONREPLY, \
|
||||||
|
ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, READREQUEST, WRITEREQUEST
|
||||||
|
|
||||||
|
|
||||||
class SecopClient(secop.client.SecopClient):
|
class SecopClient(secop.client.SecopClient):
|
||||||
|
@ -21,21 +21,19 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""SECoP proxy modules"""
|
"""SECoP proxy modules"""
|
||||||
|
|
||||||
from secop.params import Parameter, Command
|
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||||
from secop.modules import Module, Writable, Readable, Drivable
|
|
||||||
from secop.datatypes import StringType
|
from secop.datatypes import StringType
|
||||||
|
from secop.errors import BadValueError, \
|
||||||
|
CommunicationFailedError, ConfigError, make_secop_error
|
||||||
|
from secop.lib import get_class
|
||||||
|
from secop.modules import Drivable, Module, Readable, Writable
|
||||||
|
from secop.params import Command, Parameter
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
from secop.stringio import HasIodev
|
from secop.stringio import HasIodev
|
||||||
from secop.lib import get_class
|
|
||||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
|
||||||
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyModule(HasIodev, Module):
|
class ProxyModule(HasIodev, Module):
|
||||||
properties = {
|
module = Property('remote module name', datatype=StringType(), default='')
|
||||||
'module':
|
|
||||||
Property('remote module name', datatype=StringType(), default=''),
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
_consistency_check_done = False
|
_consistency_check_done = False
|
||||||
@ -55,7 +53,7 @@ class ProxyModule(HasIodev, Module):
|
|||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
if not self.module:
|
if not self.module:
|
||||||
self.properties['module'] = self.name
|
self.module = self.name
|
||||||
self._secnode = self._iodev.secnode
|
self._secnode = self._iodev.secnode
|
||||||
self._secnode.register_callback(self.module, self.updateEvent,
|
self._secnode.register_callback(self.module, self.updateEvent,
|
||||||
self.descriptiveDataChange, self.nodeStateChange)
|
self.descriptiveDataChange, self.nodeStateChange)
|
||||||
@ -90,10 +88,10 @@ class ProxyModule(HasIodev, Module):
|
|||||||
dt.compatible(pobj.datatype)
|
dt.compatible(pobj.datatype)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.warning('remote parameter %s:%s is not fully compatible: %r != %r'
|
self.log.warning('remote parameter %s:%s is not fully compatible: %r != %r'
|
||||||
% (self.module, pname, pobj.datatype, dt))
|
% (self.module, pname, pobj.datatype, dt))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.warning('remote parameter %s:%s has an incompatible datatype: %r != %r'
|
self.log.warning('remote parameter %s:%s has an incompatible datatype: %r != %r'
|
||||||
% (self.module, pname, pobj.datatype, dt))
|
% (self.module, pname, pobj.datatype, dt))
|
||||||
while cmds:
|
while cmds:
|
||||||
cname, cobj = cmds.popitem()
|
cname, cobj = cmds.popitem()
|
||||||
props = remotecmds.get(cname)
|
props = remotecmds.get(cname)
|
||||||
@ -103,9 +101,9 @@ class ProxyModule(HasIodev, Module):
|
|||||||
dt = props['datatype']
|
dt = props['datatype']
|
||||||
try:
|
try:
|
||||||
cobj.datatype.compatible(dt)
|
cobj.datatype.compatible(dt)
|
||||||
except Exception:
|
except BadValueError:
|
||||||
self.log.warning('remote command %s:%s is not compatible: %r != %r'
|
self.log.warning('remote command %s:%s is not compatible: %r != %r'
|
||||||
% (self.module, pname, pobj.datatype, dt))
|
% (self.module, cname, cobj.datatype, dt))
|
||||||
# what to do if descriptive data does not match?
|
# what to do if descriptive data does not match?
|
||||||
# we might raise an exception, but this would lead to a reconnection,
|
# we might raise an exception, but this would lead to a reconnection,
|
||||||
# which might not help.
|
# which might not help.
|
||||||
@ -141,14 +139,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
|||||||
|
|
||||||
|
|
||||||
class SecNode(Module):
|
class SecNode(Module):
|
||||||
properties = {
|
uri = Property('uri of a SEC node', datatype=StringType())
|
||||||
'uri':
|
|
||||||
Property('uri of a SEC node', datatype=StringType()),
|
|
||||||
}
|
|
||||||
commands = {
|
|
||||||
'request':
|
|
||||||
Command('send a request', argument=StringType(), result=StringType())
|
|
||||||
}
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
self.secnode = SecopClient(self.uri, self.log)
|
self.secnode = SecopClient(self.uri, self.log)
|
||||||
@ -156,8 +147,9 @@ class SecNode(Module):
|
|||||||
def startModule(self, started_callback):
|
def startModule(self, started_callback):
|
||||||
self.secnode.spawn_connect(started_callback)
|
self.secnode.spawn_connect(started_callback)
|
||||||
|
|
||||||
def do_request(self, msg):
|
@Command(StringType(), result=StringType())
|
||||||
"""for test purposes"""
|
def request(self, msg):
|
||||||
|
"""send a request, for debugging purposes"""
|
||||||
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
||||||
return encode_msg_frame(*reply).decode('utf-8')
|
return encode_msg_frame(*reply).decode('utf-8')
|
||||||
|
|
||||||
@ -184,17 +176,12 @@ def proxy_class(remote_class, name=None):
|
|||||||
else:
|
else:
|
||||||
raise ConfigError('%r is no SECoP module class' % remote_class)
|
raise ConfigError('%r is no SECoP module class' % remote_class)
|
||||||
|
|
||||||
parameters = {}
|
attrs = rcls.propertyDict.copy()
|
||||||
commands = {}
|
|
||||||
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
|
|
||||||
|
|
||||||
for aname, aobj in rcls.accessibles.items():
|
for aname, aobj in rcls.accessibles.items():
|
||||||
if isinstance(aobj, Parameter):
|
if isinstance(aobj, Parameter):
|
||||||
pobj = aobj.copy()
|
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
||||||
parameters[aname] = pobj
|
attrs[aname] = pobj
|
||||||
pobj.properties['poll'] = False
|
|
||||||
pobj.properties['handler'] = None
|
|
||||||
pobj.properties['needscfg'] = False
|
|
||||||
|
|
||||||
def rfunc(self, pname=aname):
|
def rfunc(self, pname=aname):
|
||||||
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
||||||
@ -216,12 +203,11 @@ def proxy_class(remote_class, name=None):
|
|||||||
|
|
||||||
elif isinstance(aobj, Command):
|
elif isinstance(aobj, Command):
|
||||||
cobj = aobj.copy()
|
cobj = aobj.copy()
|
||||||
commands[aname] = cobj
|
|
||||||
|
|
||||||
def cfunc(self, arg=None, cname=aname):
|
def cfunc(self, arg=None, cname=aname):
|
||||||
return self._secnode.execCommand(self.name, cname, arg)
|
return self._secnode.execCommand(self.name, cname, arg)
|
||||||
|
|
||||||
attrs['do_' + aname] = cfunc
|
attrs[aname] = cobj(cfunc)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
||||||
|
@ -23,13 +23,17 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Define helpers"""
|
"""Define helpers"""
|
||||||
|
|
||||||
import os
|
|
||||||
from os.path import join, exists, dirname, isdir
|
|
||||||
import ast
|
import ast
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import configparser
|
import configparser
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from secop.errors import ConfigError
|
||||||
|
from secop.lib import formatException, get_class, getGeneralConfig
|
||||||
|
from secop.modules import Attached
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from daemon import DaemonContext
|
from daemon import DaemonContext
|
||||||
try:
|
try:
|
||||||
@ -39,10 +43,6 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
DaemonContext = None
|
DaemonContext = None
|
||||||
|
|
||||||
from secop.errors import ConfigError
|
|
||||||
from secop.lib import formatException, get_class, getGeneralConfig
|
|
||||||
from secop.modules import Attached
|
|
||||||
from secop.params import PREDEFINED_ACCESSIBLES
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import systemd.daemon
|
import systemd.daemon
|
||||||
@ -95,7 +95,11 @@ class Server:
|
|||||||
merged_cfg = OrderedDict()
|
merged_cfg = OrderedDict()
|
||||||
ambiguous_sections = set()
|
ambiguous_sections = set()
|
||||||
for cfgfile in cfgfiles.split(','):
|
for cfgfile in cfgfiles.split(','):
|
||||||
cfgdict = self.loadCfgFile(cfgfile)
|
if cfgfile.endswith('.cfg') and os.path.exists(cfgfile):
|
||||||
|
filename = cfgfile
|
||||||
|
else:
|
||||||
|
filename = os.path.join(cfg['confdir'], cfgfile + '.cfg')
|
||||||
|
cfgdict = self.loadCfgFile(filename)
|
||||||
ambiguous_sections |= set(merged_cfg) & set(cfgdict)
|
ambiguous_sections |= set(merged_cfg) & set(cfgdict)
|
||||||
merged_cfg.update(cfgdict)
|
merged_cfg.update(cfgdict)
|
||||||
self.node_cfg = merged_cfg.pop('NODE', {})
|
self.node_cfg = merged_cfg.pop('NODE', {})
|
||||||
@ -112,22 +116,9 @@ class Server:
|
|||||||
if ambiguous_sections:
|
if ambiguous_sections:
|
||||||
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
||||||
self._cfgfiles = cfgfiles
|
self._cfgfiles = cfgfiles
|
||||||
self._pidfile = join(cfg['piddir'], name + '.pid')
|
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
|
||||||
|
|
||||||
def loadCfgFile(self, cfgfile):
|
def loadCfgFile(self, filename):
|
||||||
if not cfgfile.endswith('.cfg'):
|
|
||||||
cfgfile += '.cfg'
|
|
||||||
if '/' in cfgfile: # specified as full path
|
|
||||||
filename = cfgfile if exists(cfgfile) else None
|
|
||||||
else:
|
|
||||||
cfg = getGeneralConfig()
|
|
||||||
for filename in [join(d, cfgfile) for d in cfg['confdir'].split(':')]:
|
|
||||||
if exists(filename):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
filename = None
|
|
||||||
if filename is None:
|
|
||||||
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, cfg['confdir']))
|
|
||||||
self.log.debug('Parse config file %s ...' % filename)
|
self.log.debug('Parse config file %s ...' % filename)
|
||||||
result = OrderedDict()
|
result = OrderedDict()
|
||||||
parser = configparser.ConfigParser()
|
parser = configparser.ConfigParser()
|
||||||
@ -165,8 +156,8 @@ class Server:
|
|||||||
def start(self):
|
def start(self):
|
||||||
if not DaemonContext:
|
if not DaemonContext:
|
||||||
raise ConfigError('can not daemonize, as python-daemon is not installed')
|
raise ConfigError('can not daemonize, as python-daemon is not installed')
|
||||||
piddir = dirname(self._pidfile)
|
piddir = os.path.dirname(self._pidfile)
|
||||||
if not isdir(piddir):
|
if not os.path.isdir(piddir):
|
||||||
os.makedirs(piddir)
|
os.makedirs(piddir)
|
||||||
pidfile = pidlockfile.TimeoutPIDLockFile(self._pidfile)
|
pidfile = pidlockfile.TimeoutPIDLockFile(self._pidfile)
|
||||||
|
|
||||||
@ -238,7 +229,7 @@ class Server:
|
|||||||
# all objs created, now start them up and interconnect
|
# all objs created, now start them up and interconnect
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
self.log.info('registering module %r' % modname)
|
self.log.info('registering module %r' % modname)
|
||||||
self.dispatcher.register_module(modobj, modname, modobj.properties['export'])
|
self.dispatcher.register_module(modobj, modname, modobj.export)
|
||||||
if modobj.pollerClass is not None:
|
if modobj.pollerClass is not None:
|
||||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||||
@ -247,14 +238,16 @@ class Server:
|
|||||||
|
|
||||||
# handle attached modules
|
# handle attached modules
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
for propname, propobj in modobj.__class__.properties.items():
|
for propname, propobj in modobj.propertyDict.items():
|
||||||
if isinstance(propobj, Attached):
|
if isinstance(propobj, Attached):
|
||||||
setattr(modobj, propobj.attrname or '_' + propname,
|
setattr(modobj, propobj.attrname or '_' + propname,
|
||||||
self.dispatcher.get_module(modobj.properties[propname]))
|
self.dispatcher.get_module(getattr(modobj, propname)))
|
||||||
# call init on each module after registering all
|
# call init on each module after registering all
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
modobj.initModule()
|
modobj.initModule()
|
||||||
|
|
||||||
|
if self._testonly:
|
||||||
|
return
|
||||||
start_events = []
|
start_events = []
|
||||||
for modname, modobj in self.modules.items():
|
for modname, modobj in self.modules.items():
|
||||||
event = threading.Event()
|
event = threading.Event()
|
||||||
@ -271,10 +264,3 @@ class Server:
|
|||||||
if not event.wait(timeout=max(0, deadline - time.time())):
|
if not event.wait(timeout=max(0, deadline - time.time())):
|
||||||
self.log.info('WARNING: timeout when starting %s' % name)
|
self.log.info('WARNING: timeout when starting %s' % name)
|
||||||
self.log.info('all modules and pollers started')
|
self.log.info('all modules and pollers started')
|
||||||
history_path = os.environ.get('FRAPPY_HISTORY')
|
|
||||||
if history_path:
|
|
||||||
from secop.histwriter import HistWriter
|
|
||||||
writer = HistWriter(history_path, PREDEFINED_ACCESSIBLES.keys(), self.dispatcher)
|
|
||||||
# treat writer as a connection
|
|
||||||
self.dispatcher.add_connection(writer)
|
|
||||||
writer.init(self.dispatcher.handle_describe(writer, None, None))
|
|
||||||
|
@ -22,12 +22,15 @@
|
|||||||
"""Define Simulation classes"""
|
"""Define Simulation classes"""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: rework after syntax change!
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from secop.datatypes import FloatRange
|
from secop.datatypes import FloatRange
|
||||||
from secop.lib import mkthread
|
from secop.lib import mkthread
|
||||||
from secop.modules import Drivable, Module, Parameter, Readable, Writable, BasicPoller
|
from secop.modules import BasicPoller, Drivable, \
|
||||||
|
Module, Parameter, Readable, Writable
|
||||||
|
|
||||||
|
|
||||||
class SimBase:
|
class SimBase:
|
||||||
|
@ -23,15 +23,18 @@
|
|||||||
implements TCP/IP and is be used as a base for SerialIO
|
implements TCP/IP and is be used as a base for SerialIO
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from secop.datatypes import ArrayOf, BoolType, \
|
||||||
|
FloatRange, StringType, TupleOf, ValueType
|
||||||
|
from secop.errors import CommunicationFailedError, \
|
||||||
|
CommunicationSilentError, ConfigError
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Override
|
from secop.modules import Attached, Command, \
|
||||||
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
|
Communicator, Done, Module, Parameter, Property
|
||||||
from secop.errors import CommunicationFailedError, CommunicationSilentError
|
|
||||||
from secop.poller import REGULAR
|
from secop.poller import REGULAR
|
||||||
from secop.metaclass import Done
|
|
||||||
|
|
||||||
|
|
||||||
class StringIO(Communicator):
|
class StringIO(Communicator):
|
||||||
@ -39,48 +42,22 @@ class StringIO(Communicator):
|
|||||||
|
|
||||||
self healing is assured by polling the parameter 'is_connected'
|
self healing is assured by polling the parameter 'is_connected'
|
||||||
"""
|
"""
|
||||||
properties = {
|
uri = Property('hostname:portnumber', datatype=StringType())
|
||||||
'uri':
|
end_of_line = Property('end_of_line character', datatype=ValueType(),
|
||||||
Property('hostname:portnumber', datatype=StringType()),
|
default='\n', settable=True)
|
||||||
'end_of_line':
|
encoding = Property('used encoding', datatype=StringType(),
|
||||||
Property('end_of_line character', datatype=ValueType(),
|
default='ascii', settable=True)
|
||||||
default='\n', settable=True),
|
identification = Property('''
|
||||||
'encoding':
|
identification
|
||||||
Property('used encoding', datatype=StringType(),
|
|
||||||
default='ascii', settable=True),
|
|
||||||
'identification':
|
|
||||||
Property('identification\n\n'
|
|
||||||
'a list of tuples with commands and expected responses as regexp, '
|
|
||||||
'to be sent on connect',
|
|
||||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
|
|
||||||
}
|
|
||||||
parameters = {
|
|
||||||
'timeout':
|
|
||||||
Parameter('timeout', datatype=FloatRange(0), default=2),
|
|
||||||
'wait_before':
|
|
||||||
Parameter('wait time before sending', datatype=FloatRange(), default=0),
|
|
||||||
'is_connected':
|
|
||||||
Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR),
|
|
||||||
'pollinterval':
|
|
||||||
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
|
|
||||||
}
|
|
||||||
commands = {
|
|
||||||
'communicate':
|
|
||||||
Override('''
|
|
||||||
send a command and receive a reply
|
|
||||||
|
|
||||||
- using end_of_line, encoding and self._lock
|
a list of tuples with commands and expected responses as regexp,
|
||||||
- for commands without reply, the command must be joined with a query command,
|
to be sent on connect''',
|
||||||
- wait_before is respected for end_of_lines within a command
|
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
|
||||||
'''),
|
|
||||||
'multicomm':
|
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||||
Command('''
|
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||||
execute multiple commands in one go
|
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
|
||||||
|
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||||
assuring that no other thread calls commands in between
|
|
||||||
''',
|
|
||||||
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
|
|
||||||
}
|
|
||||||
|
|
||||||
_reconnectCallbacks = None
|
_reconnectCallbacks = None
|
||||||
|
|
||||||
@ -115,11 +92,12 @@ class StringIO(Communicator):
|
|||||||
self._conn = AsynConn(uri, self._eol_read)
|
self._conn = AsynConn(uri, self._eol_read)
|
||||||
self.is_connected = True
|
self.is_connected = True
|
||||||
for command, regexp in self.identification:
|
for command, regexp in self.identification:
|
||||||
reply = self.do_communicate(command)
|
reply = self.communicate(command)
|
||||||
if not re.match(regexp, reply):
|
if not re.match(regexp, reply):
|
||||||
self.closeConnection()
|
self.closeConnection()
|
||||||
raise CommunicationFailedError('bad response: %s does not match %s' %
|
raise CommunicationFailedError('bad response: %s does not match %s' %
|
||||||
(reply, regexp))
|
(reply, regexp))
|
||||||
|
|
||||||
def closeConnection(self):
|
def closeConnection(self):
|
||||||
"""close connection
|
"""close connection
|
||||||
|
|
||||||
@ -135,7 +113,7 @@ class StringIO(Communicator):
|
|||||||
self.is_connected is changed only by self.connectStart or self.closeConnection
|
self.is_connected is changed only by self.connectStart or self.closeConnection
|
||||||
"""
|
"""
|
||||||
if self.is_connected:
|
if self.is_connected:
|
||||||
return Done # no need for intermediate updates
|
return Done # no need for intermediate updates
|
||||||
try:
|
try:
|
||||||
self.connectStart()
|
self.connectStart()
|
||||||
if self._last_error:
|
if self._last_error:
|
||||||
@ -180,9 +158,17 @@ class StringIO(Communicator):
|
|||||||
if removeme:
|
if removeme:
|
||||||
self._reconnectCallbacks.pop(key)
|
self._reconnectCallbacks.pop(key)
|
||||||
|
|
||||||
def do_communicate(self, command):
|
def communicate(self, command):
|
||||||
|
"""send a command and receive a reply
|
||||||
|
|
||||||
|
using end_of_line, encoding and self._lock
|
||||||
|
for commands without reply, the command must be joined with a query command,
|
||||||
|
wait_before is respected for end_of_lines within a command.
|
||||||
|
"""
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
self.read_is_connected() # try to reconnect
|
self.read_is_connected() # try to reconnect
|
||||||
|
if not self._conn:
|
||||||
|
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
# read garbage and wait before send
|
# read garbage and wait before send
|
||||||
@ -214,11 +200,13 @@ class StringIO(Communicator):
|
|||||||
self.log.error(self._last_error)
|
self.log.error(self._last_error)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def do_multicomm(self, commands):
|
@Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||||
|
def multicomm(self, commands):
|
||||||
|
"""communicate multiple request/replies in one row"""
|
||||||
replies = []
|
replies = []
|
||||||
with self._lock:
|
with self._lock:
|
||||||
for cmd in commands:
|
for cmd in commands:
|
||||||
replies.append(self.do_communicate(cmd))
|
replies.append(self.communicate(cmd))
|
||||||
return replies
|
return replies
|
||||||
|
|
||||||
|
|
||||||
@ -227,16 +215,15 @@ class HasIodev(Module):
|
|||||||
|
|
||||||
not only StringIO !
|
not only StringIO !
|
||||||
"""
|
"""
|
||||||
properties = {
|
iodev = Attached()
|
||||||
'iodev': Attached(),
|
uri = Property('uri for automatic creation of the attached communication module',
|
||||||
'uri': Property('uri for automatic creation of the attached communication module', StringType(), default=''),
|
StringType(), default='')
|
||||||
}
|
|
||||||
|
|
||||||
iodevDict = {}
|
iodevDict = {}
|
||||||
|
|
||||||
def __init__(self, name, logger, opts, srv):
|
def __init__(self, name, logger, opts, srv):
|
||||||
iodev = opts.get('iodev')
|
iodev = opts.get('iodev')
|
||||||
super().__init__(name, logger, opts, srv)
|
Module.__init__(self, name, logger, opts, srv)
|
||||||
if self.uri:
|
if self.uri:
|
||||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||||
'export': False}
|
'export': False}
|
||||||
@ -246,7 +233,9 @@ class HasIodev(Module):
|
|||||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
||||||
srv.modules[ioname] = iodev
|
srv.modules[ioname] = iodev
|
||||||
self.iodevDict[self.uri] = ioname
|
self.iodevDict[self.uri] = ioname
|
||||||
self.setProperty('iodev', ioname)
|
self.iodev = ioname
|
||||||
|
elif not self.iodev:
|
||||||
|
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
try:
|
try:
|
||||||
@ -257,4 +246,4 @@ class HasIodev(Module):
|
|||||||
super().initModule()
|
super().initModule()
|
||||||
|
|
||||||
def sendRecv(self, command):
|
def sendRecv(self, command):
|
||||||
return self._iodev.do_communicate(command)
|
return self._iodev.communicate(command)
|
||||||
|
@ -47,7 +47,7 @@ def get_git_version(abbrev=4, cwd=None):
|
|||||||
# mangle version to comply with pep440
|
# mangle version to comply with pep440
|
||||||
if version.count('-'):
|
if version.count('-'):
|
||||||
version, patchcount, githash = version.split('-')
|
version, patchcount, githash = version.split('-')
|
||||||
version += '.post%s+%s' %(patchcount, githash)
|
version += '.post%s+%s' % (patchcount, githash)
|
||||||
return version
|
return version
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
@ -25,20 +25,17 @@ import random
|
|||||||
import time
|
import time
|
||||||
from math import atan
|
from math import atan
|
||||||
|
|
||||||
from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType
|
from secop.datatypes import BoolType, EnumType, FloatRange, StringType, TupleOf
|
||||||
from secop.lib import clamp, mkthread
|
from secop.lib import clamp, mkthread
|
||||||
from secop.modules import Drivable, Override, Parameter
|
from secop.modules import Command, Drivable, Parameter
|
||||||
|
|
||||||
# test custom property (value.test can be changed in config file)
|
# test custom property (value.test can be changed in config file)
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
|
|
||||||
Parameter.properties['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
|
Parameter.propertyDict['test'] = Property('A Property for testing purposes', StringType(), default='', export=True)
|
||||||
|
|
||||||
|
|
||||||
class CryoBase(Drivable):
|
class CryoBase(Drivable):
|
||||||
properties = {
|
is_cryo = Property('private Flag if this is a cryostat', BoolType(), default=True, export=True)
|
||||||
'is_cryo': Property('private Flag if this is a cryostat', BoolType(), default=True, export=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Cryostat(CryoBase):
|
class Cryostat(CryoBase):
|
||||||
@ -49,93 +46,88 @@ class Cryostat(CryoBase):
|
|||||||
- thermal transfer between regulation and samplen
|
- thermal transfer between regulation and samplen
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = dict(
|
jitter = Parameter("amount of random noise on readout values",
|
||||||
jitter=Parameter("amount of random noise on readout values",
|
datatype=FloatRange(0, 1), unit="K",
|
||||||
datatype=FloatRange(0, 1), unit="K",
|
default=0.1, readonly=False, export=False,
|
||||||
default=0.1, readonly=False, export=False,
|
|
||||||
),
|
|
||||||
T_start=Parameter("starting temperature for simulation",
|
|
||||||
datatype=FloatRange(0), default=10,
|
|
||||||
export=False,
|
|
||||||
),
|
|
||||||
looptime=Parameter("timestep for simulation",
|
|
||||||
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
|
||||||
readonly=False, export=False,
|
|
||||||
),
|
),
|
||||||
ramp=Parameter("ramping speed of the setpoint",
|
T_start = Parameter("starting temperature for simulation",
|
||||||
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
datatype=FloatRange(0), default=10,
|
||||||
readonly=False,
|
export=False,
|
||||||
),
|
),
|
||||||
setpoint=Parameter("current setpoint during ramping else target",
|
looptime = Parameter("timestep for simulation",
|
||||||
datatype=FloatRange(), default=1, unit='K',
|
datatype=FloatRange(0.01, 10), unit="s", default=1,
|
||||||
),
|
readonly=False, export=False,
|
||||||
maxpower=Parameter("Maximum heater power",
|
),
|
||||||
datatype=FloatRange(0), default=1, unit="W",
|
ramp = Parameter("ramping speed of the setpoint",
|
||||||
readonly=False,
|
datatype=FloatRange(0, 1e3), unit="K/min", default=1,
|
||||||
group='heater_settings',
|
|
||||||
),
|
|
||||||
heater=Parameter("current heater setting",
|
|
||||||
datatype=FloatRange(0, 100), default=0, unit="%",
|
|
||||||
group='heater_settings',
|
|
||||||
),
|
|
||||||
heaterpower=Parameter("current heater power",
|
|
||||||
datatype=FloatRange(0), default=0, unit="W",
|
|
||||||
group='heater_settings',
|
|
||||||
),
|
|
||||||
target=Override("target temperature",
|
|
||||||
datatype=FloatRange(0), default=0, unit="K",
|
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
),
|
||||||
value=Override("regulation temperature",
|
setpoint = Parameter("current setpoint during ramping else target",
|
||||||
datatype=FloatRange(0), default=0, unit="K",
|
datatype=FloatRange(), default=1, unit='K',
|
||||||
test='TEST',
|
),
|
||||||
|
maxpower = Parameter("Maximum heater power",
|
||||||
|
datatype=FloatRange(0), default=1, unit="W",
|
||||||
|
readonly=False,
|
||||||
|
group='heater_settings',
|
||||||
|
),
|
||||||
|
heater = Parameter("current heater setting",
|
||||||
|
datatype=FloatRange(0, 100), default=0, unit="%",
|
||||||
|
group='heater_settings',
|
||||||
|
),
|
||||||
|
heaterpower = Parameter("current heater power",
|
||||||
|
datatype=FloatRange(0), default=0, unit="W",
|
||||||
|
group='heater_settings',
|
||||||
|
),
|
||||||
|
target = Parameter("target temperature",
|
||||||
|
datatype=FloatRange(0), default=0, unit="K",
|
||||||
|
readonly=False,
|
||||||
|
),
|
||||||
|
value = Parameter("regulation temperature",
|
||||||
|
datatype=FloatRange(0), default=0, unit="K",
|
||||||
|
test='TEST',
|
||||||
|
),
|
||||||
|
pid = Parameter("regulation coefficients",
|
||||||
|
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
||||||
|
FloatRange(0, 100)),
|
||||||
|
default=(40, 10, 2), readonly=False,
|
||||||
|
group='pid',
|
||||||
),
|
),
|
||||||
pid=Parameter("regulation coefficients",
|
# pylint: disable=invalid-name
|
||||||
datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
|
p = Parameter("regulation coefficient 'p'",
|
||||||
FloatRange(0, 100)),
|
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
||||||
default=(40, 10, 2), readonly=False,
|
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
p=Parameter("regulation coefficient 'p'",
|
i = Parameter("regulation coefficient 'i'",
|
||||||
datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
|
datatype=FloatRange(0, 100), default=10, readonly=False,
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
i=Parameter("regulation coefficient 'i'",
|
d = Parameter("regulation coefficient 'd'",
|
||||||
datatype=FloatRange(0, 100), default=10, readonly=False,
|
datatype=FloatRange(0, 100), default=2, readonly=False,
|
||||||
group='pid',
|
group='pid',
|
||||||
),
|
),
|
||||||
d=Parameter("regulation coefficient 'd'",
|
mode = Parameter("mode of regulation",
|
||||||
datatype=FloatRange(0, 100), default=2, readonly=False,
|
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
||||||
group='pid',
|
default='ramp',
|
||||||
),
|
readonly=False,
|
||||||
mode=Parameter("mode of regulation",
|
),
|
||||||
datatype=EnumType('mode', ramp=None, pid=None, openloop=None),
|
pollinterval = Parameter("polling interval",
|
||||||
default='ramp',
|
datatype=FloatRange(0), default=5,
|
||||||
readonly=False,
|
),
|
||||||
),
|
tolerance = Parameter("temperature range for stability checking",
|
||||||
pollinterval=Override("polling interval",
|
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
||||||
datatype=FloatRange(0), default=5,
|
readonly=False,
|
||||||
),
|
group='stability',
|
||||||
tolerance=Parameter("temperature range for stability checking",
|
),
|
||||||
datatype=FloatRange(0, 100), default=0.1, unit='K',
|
window = Parameter("time window for stability checking",
|
||||||
|
datatype=FloatRange(1, 900), default=30, unit='s',
|
||||||
|
readonly=False,
|
||||||
|
group='stability',
|
||||||
|
),
|
||||||
|
timeout = Parameter("max waiting time for stabilisation check",
|
||||||
|
datatype=FloatRange(1, 36000), default=900, unit='s',
|
||||||
readonly=False,
|
readonly=False,
|
||||||
group='stability',
|
group='stability',
|
||||||
),
|
),
|
||||||
window=Parameter("time window for stability checking",
|
|
||||||
datatype=FloatRange(1, 900), default=30, unit='s',
|
|
||||||
readonly=False,
|
|
||||||
group='stability',
|
|
||||||
),
|
|
||||||
timeout=Parameter("max waiting time for stabilisation check",
|
|
||||||
datatype=FloatRange(1, 36000), default=900, unit='s',
|
|
||||||
readonly=False,
|
|
||||||
group='stability',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
commands = dict(
|
|
||||||
stop=Override(
|
|
||||||
"Stop ramping the setpoint\n\nby setting the current setpoint as new target"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._stopflag = False
|
self._stopflag = False
|
||||||
@ -180,11 +172,11 @@ class Cryostat(CryoBase):
|
|||||||
def read_pid(self):
|
def read_pid(self):
|
||||||
return (self.p, self.i, self.d)
|
return (self.p, self.i, self.d)
|
||||||
|
|
||||||
def do_stop(self):
|
@Command()
|
||||||
""""stop the ramp
|
def stop(self):
|
||||||
|
"""Stop ramping the setpoint
|
||||||
|
|
||||||
by setting current setpoint as target
|
by setting the current setpoint as new target"""
|
||||||
"""
|
|
||||||
# XXX: discussion: take setpoint or current value ???
|
# XXX: discussion: take setpoint or current value ???
|
||||||
self.write_target(self.setpoint)
|
self.write_target(self.setpoint)
|
||||||
|
|
||||||
|
@ -28,42 +28,41 @@ import time
|
|||||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||||
FloatRange, IntRange, StringType, StructOf, TupleOf
|
FloatRange, IntRange, StringType, StructOf, TupleOf
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.modules import Drivable, Override, Parameter as SECoP_Parameter, Readable
|
from secop.modules import Drivable
|
||||||
|
from secop.modules import Parameter as SECoP_Parameter
|
||||||
|
from secop.modules import Readable
|
||||||
from secop.properties import Property
|
from secop.properties import Property
|
||||||
|
|
||||||
|
|
||||||
class Parameter(SECoP_Parameter):
|
class Parameter(SECoP_Parameter):
|
||||||
properties = {
|
test = Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test')
|
||||||
'test' : Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test'),
|
|
||||||
}
|
|
||||||
|
|
||||||
PERSIST = 101
|
PERSIST = 101
|
||||||
|
|
||||||
|
|
||||||
class Switch(Drivable):
|
class Switch(Drivable):
|
||||||
"""switch it on or off....
|
"""switch it on or off....
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('current state (on or off)',
|
value = Parameter('current state (on or off)',
|
||||||
|
datatype=EnumType(on=1, off=0), default=0,
|
||||||
|
)
|
||||||
|
target = Parameter('wanted state (on or off)',
|
||||||
datatype=EnumType(on=1, off=0), default=0,
|
datatype=EnumType(on=1, off=0), default=0,
|
||||||
),
|
readonly=False,
|
||||||
'target': Override('wanted state (on or off)',
|
)
|
||||||
datatype=EnumType(on=1, off=0), default=0,
|
switch_on_time = Parameter('seconds to wait after activating the switch',
|
||||||
readonly=False,
|
datatype=FloatRange(0, 60), unit='s',
|
||||||
),
|
default=10, export=False,
|
||||||
'switch_on_time': Parameter('seconds to wait after activating the switch',
|
)
|
||||||
|
switch_off_time = Parameter('cool-down time in seconds',
|
||||||
datatype=FloatRange(0, 60), unit='s',
|
datatype=FloatRange(0, 60), unit='s',
|
||||||
default=10, export=False,
|
default=10, export=False,
|
||||||
),
|
)
|
||||||
'switch_off_time': Parameter('cool-down time in seconds',
|
|
||||||
datatype=FloatRange(0, 60), unit='s',
|
|
||||||
default=10, export=False,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
properties = {
|
description = Property('The description of the Module', StringType(),
|
||||||
'description' : Property('The description of the Module', StringType(),
|
default='no description', mandatory=False, extname='description')
|
||||||
default='no description', mandatory=False, extname='description'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
# could ask HW
|
# could ask HW
|
||||||
@ -109,30 +108,29 @@ class Switch(Drivable):
|
|||||||
class MagneticField(Drivable):
|
class MagneticField(Drivable):
|
||||||
"""a liquid magnet
|
"""a liquid magnet
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('current field in T',
|
value = Parameter('current field in T',
|
||||||
|
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||||
|
)
|
||||||
|
target = Parameter('target field in T',
|
||||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
unit='T', datatype=FloatRange(-15, 15), default=0,
|
||||||
),
|
readonly=False,
|
||||||
'target': Override('target field in T',
|
)
|
||||||
unit='T', datatype=FloatRange(-15, 15), default=0,
|
ramp = Parameter('ramping speed',
|
||||||
readonly=False,
|
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
||||||
),
|
readonly=False,
|
||||||
'ramp': Parameter('ramping speed',
|
)
|
||||||
unit='T/min', datatype=FloatRange(0, 1), default=0.1,
|
mode = Parameter('what to do after changing field',
|
||||||
readonly=False,
|
default=1, datatype=EnumType(persistent=1, hold=0),
|
||||||
),
|
readonly=False,
|
||||||
'mode': Parameter('what to do after changing field',
|
)
|
||||||
default=1, datatype=EnumType(persistent=1, hold=0),
|
heatswitch = Parameter('name of heat switch device',
|
||||||
readonly=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'heatswitch': Parameter('name of heat switch device',
|
|
||||||
datatype=StringType(), export=False,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
|
||||||
overrides = {
|
|
||||||
'status' : Override(datatype=TupleOf(EnumType(Status), StringType())),
|
status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle
|
||||||
@ -202,21 +200,20 @@ class MagneticField(Drivable):
|
|||||||
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
time.sleep(max(0.01, ts + loopdelay - time.time()))
|
||||||
self.log.error(self, 'main thread exited unexpectedly!')
|
self.log.error(self, 'main thread exited unexpectedly!')
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
self.write_target(self.read_value())
|
self.write_target(self.read_value())
|
||||||
|
|
||||||
|
|
||||||
class CoilTemp(Readable):
|
class CoilTemp(Readable):
|
||||||
"""a coil temperature
|
"""a coil temperature
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('Coil temperatur',
|
value = Parameter('Coil temperatur',
|
||||||
unit='K', datatype=FloatRange(), default=0,
|
unit='K', datatype=FloatRange(), default=0,
|
||||||
),
|
)
|
||||||
'sensor': Parameter("Sensor number or calibration id",
|
sensor = Parameter("Sensor number or calibration id",
|
||||||
datatype=StringType(), readonly=True,
|
datatype=StringType(), readonly=True,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return round(2.3 + random.random(), 3)
|
return round(2.3 + random.random(), 3)
|
||||||
@ -225,18 +222,17 @@ class CoilTemp(Readable):
|
|||||||
class SampleTemp(Drivable):
|
class SampleTemp(Drivable):
|
||||||
"""a sample temperature
|
"""a sample temperature
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'value': Override('Sample temperature',
|
value = Parameter('Sample temperature',
|
||||||
unit='K', datatype=FloatRange(), default=10,
|
unit='K', datatype=FloatRange(), default=10,
|
||||||
),
|
)
|
||||||
'sensor': Parameter("Sensor number or calibration id",
|
sensor = Parameter("Sensor number or calibration id",
|
||||||
datatype=StringType(), readonly=True,
|
datatype=StringType(), readonly=True,
|
||||||
),
|
)
|
||||||
'ramp': Parameter('moving speed in K/min',
|
ramp = Parameter('moving speed in K/min',
|
||||||
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
datatype=FloatRange(0, 100), unit='K/min', default=0.1,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
_thread = threading.Thread(target=self._thread)
|
_thread = threading.Thread(target=self._thread)
|
||||||
@ -272,20 +268,19 @@ class Label(Readable):
|
|||||||
of several subdevices. used for demoing connections between
|
of several subdevices. used for demoing connections between
|
||||||
modules.
|
modules.
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'system': Parameter("Name of the magnet system",
|
system = Parameter("Name of the magnet system",
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'subdev_mf': Parameter("name of subdevice for magnet status",
|
subdev_mf = Parameter("name of subdevice for magnet status",
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'subdev_ts': Parameter("name of subdevice for sample temp",
|
subdev_ts = Parameter("name of subdevice for sample temp",
|
||||||
datatype=StringType(), export=False,
|
datatype=StringType(), export=False,
|
||||||
),
|
)
|
||||||
'value': Override("final value of label string", default='',
|
value = Parameter("final value of label string", default='',
|
||||||
datatype=StringType(),
|
datatype=StringType(),
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
strings = [self.system]
|
strings = [self.system]
|
||||||
@ -317,29 +312,25 @@ class Label(Readable):
|
|||||||
class DatatypesTest(Readable):
|
class DatatypesTest(Readable):
|
||||||
"""for demoing all datatypes
|
"""for demoing all datatypes
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9),
|
||||||
readonly=False, default=1),
|
readonly=False, default=1)
|
||||||
'tupleof': Parameter('tuple of int, float and str',
|
tupleof = Parameter('tuple of int, float and str',
|
||||||
datatype=TupleOf(IntRange(), FloatRange(),
|
datatype=TupleOf(IntRange(), FloatRange(),
|
||||||
StringType()),
|
StringType()),
|
||||||
readonly=False, default=(1, 2.3, 'a')),
|
readonly=False, default=(1, 2.3, 'a'))
|
||||||
'arrayof': Parameter('array: 2..3 times bool',
|
arrayof = Parameter('array: 2..3 times bool',
|
||||||
datatype=ArrayOf(BoolType(), 2, 3),
|
datatype=ArrayOf(BoolType(), 2, 3),
|
||||||
readonly=False, default=[1, 0, 1]),
|
readonly=False, default=[1, 0, 1])
|
||||||
'intrange': Parameter('intrange', datatype=IntRange(2, 9),
|
intrange = Parameter('intrange', datatype=IntRange(2, 9),
|
||||||
readonly=False, default=4),
|
readonly=False, default=4)
|
||||||
'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1),
|
floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1),
|
||||||
readonly=False, default=0, ),
|
readonly=False, default=0)
|
||||||
'struct': Parameter('struct(a=str, b=int, c=bool)',
|
struct = Parameter('struct(a=str, b=int, c=bool)',
|
||||||
datatype=StructOf(a=StringType(), b=IntRange(),
|
datatype=StructOf(a=StringType(), b=IntRange(),
|
||||||
c=BoolType()),
|
c=BoolType()))
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ArrayTest(Readable):
|
class ArrayTest(Readable):
|
||||||
parameters = {
|
x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
||||||
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
|
default=100000 * [0])
|
||||||
default = 100000 * [0]),
|
|
||||||
}
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
from secop.datatypes import FloatRange, StringType
|
from secop.datatypes import FloatRange, StringType
|
||||||
from secop.modules import Communicator, Drivable, Parameter, Readable, Override
|
from secop.modules import Communicator, Drivable, Parameter, Readable
|
||||||
from secop.params import Command
|
from secop.params import Command
|
||||||
|
|
||||||
|
|
||||||
@ -45,11 +45,10 @@ class Heater(Drivable):
|
|||||||
class name indicates it to be some heating element,
|
class name indicates it to be some heating element,
|
||||||
but the implementation may do anything
|
but the implementation may do anything
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'maxheaterpower': Parameter('maximum allowed heater power',
|
maxheaterpower = Parameter('maximum allowed heater power',
|
||||||
datatype=FloatRange(0, 100), unit='W',
|
datatype=FloatRange(0, 100), unit='W',
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return round(100 * random.random(), 1)
|
return round(100 * random.random(), 1)
|
||||||
@ -64,22 +63,21 @@ class Temp(Drivable):
|
|||||||
class name indicates it to be some temperature controller,
|
class name indicates it to be some temperature controller,
|
||||||
but the implementation may do anything
|
but the implementation may do anything
|
||||||
"""
|
"""
|
||||||
parameters = {
|
|
||||||
'sensor': Parameter(
|
sensor = Parameter(
|
||||||
"Sensor number or calibration id",
|
"Sensor number or calibration id",
|
||||||
datatype=StringType(
|
datatype=StringType(
|
||||||
8,
|
8,
|
||||||
16),
|
16),
|
||||||
readonly=True,
|
readonly=True,
|
||||||
),
|
)
|
||||||
'target': Override(
|
target = Parameter(
|
||||||
"Target temperature",
|
"Target temperature",
|
||||||
default=300.0,
|
default=300.0,
|
||||||
datatype=FloatRange(0),
|
datatype=FloatRange(0),
|
||||||
readonly=False,
|
readonly=False,
|
||||||
unit='K',
|
unit='K',
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return round(100 * random.random(), 1)
|
return round(100 * random.random(), 1)
|
||||||
@ -90,8 +88,8 @@ class Temp(Drivable):
|
|||||||
|
|
||||||
class Lower(Communicator):
|
class Lower(Communicator):
|
||||||
"""Communicator returning a lowercase version of the request"""
|
"""Communicator returning a lowercase version of the request"""
|
||||||
command = {
|
|
||||||
'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'),
|
@Command(argument=StringType(), result=StringType(), export='communicate')
|
||||||
}
|
def communicate(self, command):
|
||||||
def do_communicate(self, request):
|
"""lowercase a string"""
|
||||||
return str(request).lower()
|
return str(command).lower()
|
||||||
|
@ -58,20 +58,20 @@ except ImportError:
|
|||||||
class EpicsReadable(Readable):
|
class EpicsReadable(Readable):
|
||||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||||
# Commmon parameter for all EPICS devices
|
# Commmon parameter for all EPICS devices
|
||||||
parameters = {
|
|
||||||
'value': Parameter('EPICS generic value',
|
# parameters
|
||||||
datatype=FloatRange(),
|
value = Parameter('EPICS generic value',
|
||||||
default=300.0,),
|
datatype=FloatRange(),
|
||||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
default=300.0,)
|
||||||
datatype=EnumType(v3=3, v4=4),),
|
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||||
# 'private' parameters: not remotely accessible
|
datatype=EnumType(v3=3, v4=4),)
|
||||||
'value_pv': Parameter('EPICS pv_name of value',
|
value_pv = Parameter('EPICS pv_name of value',
|
||||||
datatype=StringType(),
|
datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
'status_pv': Parameter('EPICS pv_name of status',
|
status_pv = Parameter('EPICS pv_name of status',
|
||||||
datatype=StringType(),
|
datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
}
|
|
||||||
|
|
||||||
# Generic read and write functions
|
# Generic read and write functions
|
||||||
def _read_pv(self, pv_name):
|
def _read_pv(self, pv_name):
|
||||||
@ -118,21 +118,21 @@ class EpicsReadable(Readable):
|
|||||||
class EpicsDrivable(Drivable):
|
class EpicsDrivable(Drivable):
|
||||||
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
"""EpicsDrivable handles a Drivable interfacing to EPICS v4"""
|
||||||
# Commmon parameter for all EPICS devices
|
# Commmon parameter for all EPICS devices
|
||||||
parameters = {
|
|
||||||
'target': Parameter('EPICS generic target', datatype=FloatRange(),
|
# parameters
|
||||||
default=300.0, readonly=False),
|
target = Parameter('EPICS generic target', datatype=FloatRange(),
|
||||||
'value': Parameter('EPICS generic value', datatype=FloatRange(),
|
default=300.0, readonly=False)
|
||||||
default=300.0,),
|
value = Parameter('EPICS generic value', datatype=FloatRange(),
|
||||||
'epics_version': Parameter("EPICS version used, v3 or v4",
|
default=300.0,)
|
||||||
datatype=StringType(),),
|
epics_version = Parameter("EPICS version used, v3 or v4",
|
||||||
# 'private' parameters: not remotely accessible
|
datatype=StringType(),)
|
||||||
'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(),
|
target_pv = Parameter('EPICS pv_name of target', datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(),
|
value_pv = Parameter('EPICS pv_name of value', datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(),
|
status_pv = Parameter('EPICS pv_name of status', datatype=StringType(),
|
||||||
default="unset", export=False),
|
default="unset", export=False)
|
||||||
}
|
|
||||||
|
|
||||||
# Generic read and write functions
|
# Generic read and write functions
|
||||||
def _read_pv(self, pv_name):
|
def _read_pv(self, pv_name):
|
||||||
@ -191,17 +191,16 @@ class EpicsDrivable(Drivable):
|
|||||||
|
|
||||||
class EpicsTempCtrl(EpicsDrivable):
|
class EpicsTempCtrl(EpicsDrivable):
|
||||||
|
|
||||||
parameters = {
|
|
||||||
# TODO: restrict possible values with oneof datatype
|
# parameters
|
||||||
'heaterrange': Parameter('Heater range', datatype=StringType(),
|
heaterrange = Parameter('Heater range', datatype=StringType(),
|
||||||
default='Off', readonly=False,),
|
default='Off', readonly=False,)
|
||||||
'tolerance': Parameter('allowed deviation between value and target',
|
tolerance = Parameter('allowed deviation between value and target',
|
||||||
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
datatype=FloatRange(1e-6, 1e6), default=0.1,
|
||||||
readonly=False,),
|
readonly=False,)
|
||||||
# 'private' parameters: not remotely accessible
|
heaterrange_pv = Parameter('EPICS pv_name of heater range',
|
||||||
'heaterrange_pv': Parameter('EPICS pv_name of heater range',
|
datatype=StringType(), default="unset", export=False,)
|
||||||
datatype=StringType(), default="unset", export=False,),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_target(self):
|
def read_target(self):
|
||||||
return self._read_pv(self.target_pv)
|
return self._read_pv(self.target_pv)
|
||||||
|
@ -31,7 +31,7 @@ import math
|
|||||||
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
|
||||||
from secop.errors import ConfigError, DisabledError
|
from secop.errors import ConfigError, DisabledError
|
||||||
from secop.lib.sequence import SequencerMixin, Step
|
from secop.lib.sequence import SequencerMixin, Step
|
||||||
from secop.modules import Drivable, Parameter, BasicPoller
|
from secop.modules import BasicPoller, Drivable, Parameter
|
||||||
|
|
||||||
|
|
||||||
class GarfieldMagnet(SequencerMixin, Drivable):
|
class GarfieldMagnet(SequencerMixin, Drivable):
|
||||||
@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
|
|
||||||
pollerClass = BasicPoller
|
pollerClass = BasicPoller
|
||||||
|
|
||||||
parameters = {
|
|
||||||
'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False),
|
# parameters
|
||||||
'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False),
|
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False)
|
||||||
'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False),
|
subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False)
|
||||||
'subdev_symmetry': Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False),
|
subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False)
|
||||||
'userlimits': Parameter('User defined limits of device value',
|
subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
|
||||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
userlimits = Parameter('User defined limits of device value',
|
||||||
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10),
|
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||||
'abslimits': Parameter('Absolute limits of device value',
|
default=(float('-Inf'), float('+Inf')), readonly=False, poll=10)
|
||||||
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
abslimits = Parameter('Absolute limits of device value',
|
||||||
default=(-0.5, 0.5), poll=True,
|
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')),
|
||||||
),
|
default=(-0.5, 0.5), poll=True,
|
||||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
)
|
||||||
'of stable values from target)',
|
precision = Parameter('Precision of the device value (allowed deviation '
|
||||||
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
|
'of stable values from target)',
|
||||||
),
|
datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False,
|
||||||
'ramp': Parameter('Target rate of field change per minute', readonly=False,
|
)
|
||||||
datatype=FloatRange(unit='$/min'), default=1.0),
|
ramp = Parameter('Target rate of field change per minute', readonly=False,
|
||||||
'calibration': Parameter('Coefficients for calibration '
|
datatype=FloatRange(unit='$/min'), default=1.0)
|
||||||
'function: [c0, c1, c2, c3, c4] calculates '
|
calibration = Parameter('Coefficients for calibration '
|
||||||
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
'function: [c0, c1, c2, c3, c4] calculates '
|
||||||
' in T', poll=1,
|
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
|
||||||
datatype=ArrayOf(FloatRange(), 5, 5),
|
' in T', poll=1,
|
||||||
default=(1.0, 0.0, 0.0, 0.0, 0.0)),
|
datatype=ArrayOf(FloatRange(), 5, 5),
|
||||||
'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting',
|
default=(1.0, 0.0, 0.0, 0.0, 0.0))
|
||||||
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',
|
||||||
short=ArrayOf(
|
datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5),
|
||||||
FloatRange(), 5, 5),
|
short=ArrayOf(
|
||||||
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False),
|
FloatRange(), 5, 5),
|
||||||
}
|
asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False)
|
||||||
|
|
||||||
|
|
||||||
def _current2field(self, current, *coefficients):
|
def _current2field(self, current, *coefficients):
|
||||||
"""Return field in T for given current in A.
|
"""Return field in T for given current in A.
|
||||||
@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
|
|||||||
return self._currentsource.read_status()[0] == 'BUSY'
|
return self._currentsource.read_status()[0] == 'BUSY'
|
||||||
if self._currentsource.status[0] != 'BUSY':
|
if self._currentsource.status[0] != 'BUSY':
|
||||||
if self._enable.status[0] == 'ERROR':
|
if self._enable.status[0] == 'ERROR':
|
||||||
self._enable.do_reset()
|
self._enable.reset()
|
||||||
self._enable.read_status()
|
self._enable.read_status()
|
||||||
self._enable.write_target('On')
|
self._enable.write_target('On')
|
||||||
self._enable._hw_wait()
|
self._enable._hw_wait()
|
||||||
|
@ -30,18 +30,17 @@ MLZ TANGO interface for the respective device classes.
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from time import time as currenttime
|
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from time import time as currenttime
|
||||||
|
|
||||||
import PyTango
|
import PyTango
|
||||||
|
from secop.datatypes import ArrayOf, EnumType, FloatRange, \
|
||||||
from secop.datatypes import ArrayOf, EnumType, \
|
IntRange, LimitsType, StringType, TupleOf
|
||||||
FloatRange, IntRange, StringType, TupleOf, LimitsType
|
|
||||||
from secop.errors import CommunicationFailedError, \
|
from secop.errors import CommunicationFailedError, \
|
||||||
ConfigError, HardwareError, ProgrammingError
|
ConfigError, HardwareError, ProgrammingError
|
||||||
from secop.lib import lazy_property
|
from secop.lib import lazy_property
|
||||||
from secop.modules import Command, Drivable, \
|
from secop.modules import BasicPoller, Command, \
|
||||||
Module, Override, Parameter, Readable, BasicPoller
|
Drivable, Module, Parameter, Readable
|
||||||
|
|
||||||
#####
|
#####
|
||||||
|
|
||||||
@ -160,24 +159,18 @@ class PyTangoDevice(Module):
|
|||||||
|
|
||||||
pollerClass = BasicPoller
|
pollerClass = BasicPoller
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'comtries': Parameter('Maximum retries for communication',
|
comtries = Parameter('Maximum retries for communication',
|
||||||
datatype=IntRange(1, 100), default=3, readonly=False,
|
datatype=IntRange(1, 100), default=3, readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
'comdelay': Parameter('Delay between retries', datatype=FloatRange(0),
|
comdelay = Parameter('Delay between retries', datatype=FloatRange(0),
|
||||||
unit='s', default=0.1, readonly=False,
|
unit='s', default=0.1, readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
|
tangodevice = Parameter('Tango device name',
|
||||||
'tangodevice': Parameter('Tango device name',
|
datatype=StringType(), readonly=True,
|
||||||
datatype=StringType(), readonly=True,
|
# export=True, # for testing only
|
||||||
# export=True, # for testing only
|
export=False,
|
||||||
export=False,
|
)
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'reset': Command('Tango reset command', argument=None, result=None),
|
|
||||||
}
|
|
||||||
|
|
||||||
tango_status_mapping = {
|
tango_status_mapping = {
|
||||||
PyTango.DevState.ON: Drivable.Status.IDLE,
|
PyTango.DevState.ON: Drivable.Status.IDLE,
|
||||||
@ -372,7 +365,9 @@ class PyTangoDevice(Module):
|
|||||||
|
|
||||||
return (myState, tangoStatus)
|
return (myState, tangoStatus)
|
||||||
|
|
||||||
def do_reset(self):
|
@Command(argument=None, result=None)
|
||||||
|
def reset(self):
|
||||||
|
"""Tango reset command"""
|
||||||
self._dev.Reset()
|
self._dev.Reset()
|
||||||
|
|
||||||
|
|
||||||
@ -405,13 +400,9 @@ class Sensor(AnalogInput):
|
|||||||
# note: we don't transport the formula to secop....
|
# note: we don't transport the formula to secop....
|
||||||
# we support the adjust method
|
# we support the adjust method
|
||||||
|
|
||||||
commands = {
|
@Command(argument=FloatRange(), result=None)
|
||||||
'setposition': Command('Set the position to the given value.',
|
def setposition(self, value):
|
||||||
argument=FloatRange(), result=None,
|
"""Set the position to the given value."""
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_setposition(self, value):
|
|
||||||
self._dev.Adjust(value)
|
self._dev.Adjust(value)
|
||||||
|
|
||||||
|
|
||||||
@ -427,29 +418,29 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
controllers, ...
|
controllers, ...
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'userlimits': Parameter('User defined limits of device value',
|
userlimits = Parameter('User defined limits of device value',
|
||||||
datatype=LimitsType(FloatRange(unit='$')),
|
|
||||||
default=(float('-Inf'), float('+Inf')),
|
|
||||||
readonly=False, poll=10,
|
|
||||||
),
|
|
||||||
'abslimits': Parameter('Absolute limits of device value',
|
|
||||||
datatype=LimitsType(FloatRange(unit='$')),
|
datatype=LimitsType(FloatRange(unit='$')),
|
||||||
),
|
default=(float('-Inf'), float('+Inf')),
|
||||||
'precision': Parameter('Precision of the device value (allowed deviation '
|
readonly=False, poll=10,
|
||||||
'of stable values from target)',
|
)
|
||||||
datatype=FloatRange(1e-38, unit='$'),
|
abslimits = Parameter('Absolute limits of device value',
|
||||||
readonly=False, group='stability',
|
datatype=LimitsType(FloatRange(unit='$')),
|
||||||
),
|
)
|
||||||
'window': Parameter('Time window for checking stabilization if > 0',
|
precision = Parameter('Precision of the device value (allowed deviation '
|
||||||
default=60.0, readonly=False,
|
'of stable values from target)',
|
||||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
datatype=FloatRange(1e-38, unit='$'),
|
||||||
),
|
readonly=False, group='stability',
|
||||||
'timeout': Parameter('Timeout for waiting for a stable value (if > 0)',
|
)
|
||||||
default=60.0, readonly=False,
|
window = Parameter('Time window for checking stabilization if > 0',
|
||||||
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
default=60.0, readonly=False,
|
||||||
),
|
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||||
}
|
)
|
||||||
|
timeout = Parameter('Timeout for waiting for a stable value (if > 0)',
|
||||||
|
default=60.0, readonly=False,
|
||||||
|
datatype=FloatRange(0, 900, unit='s'), group='stability',
|
||||||
|
)
|
||||||
|
|
||||||
_history = ()
|
_history = ()
|
||||||
_timeout = None
|
_timeout = None
|
||||||
_moving = False
|
_moving = False
|
||||||
@ -566,7 +557,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
if self.status[0] == self.Status.BUSY:
|
if self.status[0] == self.Status.BUSY:
|
||||||
# changing target value during movement is not allowed by the
|
# changing target value during movement is not allowed by the
|
||||||
# Tango base class state machine. If we are moving, stop first.
|
# Tango base class state machine. If we are moving, stop first.
|
||||||
self.do_stop()
|
self.stop()
|
||||||
self._hw_wait()
|
self._hw_wait()
|
||||||
self._dev.value = value
|
self._dev.value = value
|
||||||
# set meaningful timeout
|
# set meaningful timeout
|
||||||
@ -587,7 +578,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
|
|||||||
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY:
|
||||||
sleep(0.3)
|
sleep(0.3)
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
self._dev.Stop()
|
self._dev.Stop()
|
||||||
|
|
||||||
|
|
||||||
@ -601,21 +592,14 @@ class Actuator(AnalogOutput):
|
|||||||
"""
|
"""
|
||||||
# for secop: support the speed and ramp parameters
|
# for secop: support the speed and ramp parameters
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'speed': Parameter('The speed of changing the value',
|
speed = Parameter('The speed of changing the value',
|
||||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||||
),
|
)
|
||||||
'ramp': Parameter('The speed of changing the value',
|
ramp = Parameter('The speed of changing the value',
|
||||||
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
readonly=False, datatype=FloatRange(0, unit='$/s'),
|
||||||
poll=30,
|
poll=30,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'setposition': Command('Set the position to the given value.',
|
|
||||||
argument=FloatRange(), result=None,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_speed(self):
|
def read_speed(self):
|
||||||
return self._dev.speed
|
return self._dev.speed
|
||||||
@ -630,7 +614,9 @@ class Actuator(AnalogOutput):
|
|||||||
self.write_speed(value / 60.)
|
self.write_speed(value / 60.)
|
||||||
return self.read_speed() * 60
|
return self.read_speed() * 60
|
||||||
|
|
||||||
def do_setposition(self, value=FloatRange()):
|
@Command(FloatRange(), result=None)
|
||||||
|
def setposition(self, value=FloatRange()):
|
||||||
|
"""Set the position to the given value."""
|
||||||
self._dev.Adjust(value)
|
self._dev.Adjust(value)
|
||||||
|
|
||||||
|
|
||||||
@ -641,21 +627,16 @@ class Motor(Actuator):
|
|||||||
It has the ability to move a real object from one place to another place.
|
It has the ability to move a real object from one place to another place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'refpos': Parameter('Reference position',
|
refpos = Parameter('Reference position',
|
||||||
datatype=FloatRange(unit='$'),
|
datatype=FloatRange(unit='$'),
|
||||||
),
|
)
|
||||||
'accel': Parameter('Acceleration',
|
accel = Parameter('Acceleration',
|
||||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||||
),
|
)
|
||||||
'decel': Parameter('Deceleration',
|
decel = Parameter('Deceleration',
|
||||||
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
datatype=FloatRange(unit='$/s^2'), readonly=False,
|
||||||
),
|
)
|
||||||
}
|
|
||||||
|
|
||||||
commands = {
|
|
||||||
'reference': Command('Do a reference run', argument=None, result=None),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_refpos(self):
|
def read_refpos(self):
|
||||||
return float(self._getProperty('refpos'))
|
return float(self._getProperty('refpos'))
|
||||||
@ -672,7 +653,9 @@ class Motor(Actuator):
|
|||||||
def write_decel(self, value):
|
def write_decel(self, value):
|
||||||
self._dev.decel = value
|
self._dev.decel = value
|
||||||
|
|
||||||
def do_reference(self):
|
@Command()
|
||||||
|
def reference(self):
|
||||||
|
"""Do a reference run"""
|
||||||
self._dev.Reference()
|
self._dev.Reference()
|
||||||
return self.read_value()
|
return self.read_value()
|
||||||
|
|
||||||
@ -681,32 +664,29 @@ class TemperatureController(Actuator):
|
|||||||
"""A temperature control loop device.
|
"""A temperature control loop device.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'p': Parameter('Proportional control Parameter', datatype=FloatRange(),
|
# pylint: disable=invalid-name
|
||||||
readonly=False, group='pid',
|
p = Parameter('Proportional control Parameter', datatype=FloatRange(),
|
||||||
),
|
readonly=False, group='pid',
|
||||||
'i': Parameter('Integral control Parameter', datatype=FloatRange(),
|
)
|
||||||
readonly=False, group='pid',
|
i = Parameter('Integral control Parameter', datatype=FloatRange(),
|
||||||
),
|
readonly=False, group='pid',
|
||||||
'd': Parameter('Derivative control Parameter', datatype=FloatRange(),
|
)
|
||||||
readonly=False, group='pid',
|
d = Parameter('Derivative control Parameter', datatype=FloatRange(),
|
||||||
),
|
readonly=False, group='pid',
|
||||||
'pid': Parameter('pid control Parameters',
|
)
|
||||||
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
pid = Parameter('pid control Parameters',
|
||||||
readonly=False, group='pid', poll=30,
|
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()),
|
||||||
),
|
readonly=False, group='pid', poll=30,
|
||||||
'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
)
|
||||||
),
|
setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1,
|
||||||
'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1,
|
)
|
||||||
),
|
heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1,
|
||||||
}
|
)
|
||||||
|
|
||||||
overrides = {
|
# overrides
|
||||||
# We want this to be freely user-settable, and not produce a warning
|
precision = Parameter(default=0.1)
|
||||||
# on startup, so select a usually sensible default.
|
ramp = Parameter(description='Temperature ramp')
|
||||||
'precision': Override(default=0.1),
|
|
||||||
'ramp': Override(description='Temperature ramp'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_ramp(self):
|
def read_ramp(self):
|
||||||
return self._dev.ramp
|
return self._dev.ramp
|
||||||
@ -755,15 +735,14 @@ class PowerSupply(Actuator):
|
|||||||
"""A power supply (voltage and current) device.
|
"""A power supply (voltage and current) device.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'voltage': Parameter('Actual voltage',
|
voltage = Parameter('Actual voltage',
|
||||||
datatype=FloatRange(unit='V'), poll=-5),
|
datatype=FloatRange(unit='V'), poll=-5)
|
||||||
'current': Parameter('Actual current',
|
current = Parameter('Actual current',
|
||||||
datatype=FloatRange(unit='A'), poll=-5),
|
datatype=FloatRange(unit='A'), poll=-5)
|
||||||
}
|
|
||||||
overrides = {
|
# overrides
|
||||||
'ramp': Override(description='Current/voltage ramp'),
|
ramp = Parameter(description='Current/voltage ramp')
|
||||||
}
|
|
||||||
|
|
||||||
def read_ramp(self):
|
def read_ramp(self):
|
||||||
return self._dev.ramp
|
return self._dev.ramp
|
||||||
@ -782,9 +761,8 @@ class DigitalInput(PyTangoDevice, Readable):
|
|||||||
"""A device reading a bitfield.
|
"""A device reading a bitfield.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
overrides = {
|
# overrides
|
||||||
'value': Override(datatype=IntRange()),
|
value = Parameter(datatype=IntRange())
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._dev.value
|
return self._dev.value
|
||||||
@ -794,10 +772,9 @@ class NamedDigitalInput(DigitalInput):
|
|||||||
"""A DigitalInput with numeric values mapped to names.
|
"""A DigitalInput with numeric values mapped to names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
mapping = Parameter('A dictionary mapping state names to integers',
|
||||||
datatype=StringType(), export=False), # XXX:!!!
|
datatype=StringType(), export=False) # XXX:!!!
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(NamedDigitalInput, self).initModule()
|
super(NamedDigitalInput, self).initModule()
|
||||||
@ -821,12 +798,11 @@ class PartialDigitalInput(NamedDigitalInput):
|
|||||||
bit width accessed.
|
bit width accessed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'startbit': Parameter('Number of the first bit',
|
startbit = Parameter('Number of the first bit',
|
||||||
datatype=IntRange(0), default=0),
|
datatype=IntRange(0), default=0)
|
||||||
'bitwidth': Parameter('Number of bits',
|
bitwidth = Parameter('Number of bits',
|
||||||
datatype=IntRange(0), default=1),
|
datatype=IntRange(0), default=1)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(PartialDigitalInput, self).initModule()
|
super(PartialDigitalInput, self).initModule()
|
||||||
@ -844,10 +820,9 @@ class DigitalOutput(PyTangoDevice, Drivable):
|
|||||||
bitfield.
|
bitfield.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
overrides = {
|
# overrides
|
||||||
'value': Override(datatype=IntRange()),
|
value = Parameter(datatype=IntRange())
|
||||||
'target': Override(datatype=IntRange()),
|
target = Parameter(datatype=IntRange())
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self._dev.value # mapping is done by datatype upon export()
|
return self._dev.value # mapping is done by datatype upon export()
|
||||||
@ -865,10 +840,9 @@ class NamedDigitalOutput(DigitalOutput):
|
|||||||
"""A DigitalOutput with numeric values mapped to names.
|
"""A DigitalOutput with numeric values mapped to names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'mapping': Parameter('A dictionary mapping state names to integers',
|
mapping = Parameter('A dictionary mapping state names to integers',
|
||||||
datatype=StringType(), export=False),
|
datatype=StringType(), export=False)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(NamedDigitalOutput, self).initModule()
|
super(NamedDigitalOutput, self).initModule()
|
||||||
@ -894,12 +868,11 @@ class PartialDigitalOutput(NamedDigitalOutput):
|
|||||||
bit width accessed.
|
bit width accessed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'startbit': Parameter('Number of the first bit',
|
startbit = Parameter('Number of the first bit',
|
||||||
datatype=IntRange(0), default=0),
|
datatype=IntRange(0), default=0)
|
||||||
'bitwidth': Parameter('Number of bits',
|
bitwidth = Parameter('Number of bits',
|
||||||
datatype=IntRange(0), default=1),
|
datatype=IntRange(0), default=1)
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
super(PartialDigitalOutput, self).initModule()
|
super(PartialDigitalOutput, self).initModule()
|
||||||
@ -925,17 +898,16 @@ class StringIO(PyTangoDevice, Module):
|
|||||||
receives strings.
|
receives strings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
parameters = {
|
# parameters
|
||||||
'bustimeout': Parameter('Communication timeout',
|
bustimeout = Parameter('Communication timeout',
|
||||||
datatype=FloatRange(unit='s'), readonly=False,
|
datatype=FloatRange(unit='s'), readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
'endofline': Parameter('End of line',
|
endofline = Parameter('End of line',
|
||||||
datatype=StringType(), readonly=False,
|
datatype=StringType(), readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
'startofline': Parameter('Start of line',
|
startofline = Parameter('Start of line',
|
||||||
datatype=StringType(), readonly=False,
|
datatype=StringType(), readonly=False,
|
||||||
group='communication'),
|
group='communication')
|
||||||
}
|
|
||||||
|
|
||||||
def read_bustimeout(self):
|
def read_bustimeout(self):
|
||||||
return self._dev.communicationTimeout
|
return self._dev.communicationTimeout
|
||||||
@ -955,53 +927,48 @@ class StringIO(PyTangoDevice, Module):
|
|||||||
def write_startofline(self, value):
|
def write_startofline(self, value):
|
||||||
self._dev.startOfLine = value
|
self._dev.startOfLine = value
|
||||||
|
|
||||||
commands = {
|
@Command(argument=StringType(), result=StringType())
|
||||||
'communicate': Command('Send a string and return the reply',
|
def communicate(self, value=StringType()):
|
||||||
argument=StringType(),
|
"""Send a string and return the reply"""
|
||||||
result=StringType()),
|
|
||||||
'flush': Command('Flush output buffer',
|
|
||||||
argument=None, result=None),
|
|
||||||
'read': Command('read some characters from input buffer',
|
|
||||||
argument=IntRange(0), result=StringType()),
|
|
||||||
'write': Command('write some chars to output',
|
|
||||||
argument=StringType(), result=None),
|
|
||||||
'readLine': Command('Read sol - a whole line - eol',
|
|
||||||
argument=None, result=StringType()),
|
|
||||||
'writeLine': Command('write sol + a whole line + eol',
|
|
||||||
argument=StringType(), result=None),
|
|
||||||
'availableChars': Command('return number of chars in input buffer',
|
|
||||||
argument=None, result=IntRange(0)),
|
|
||||||
'availableLines': Command('return number of lines in input buffer',
|
|
||||||
argument=None, result=IntRange(0)),
|
|
||||||
'multiCommunicate': Command('perform a sequence of communications',
|
|
||||||
argument=ArrayOf(
|
|
||||||
TupleOf(StringType(), IntRange()), 100),
|
|
||||||
result=ArrayOf(StringType(), 100)),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_communicate(self, value=StringType()):
|
|
||||||
return self._dev.Communicate(value)
|
return self._dev.Communicate(value)
|
||||||
|
|
||||||
def do_flush(self):
|
@Command(argument=None, result=None)
|
||||||
|
def flush(self):
|
||||||
|
"""Flush output buffer"""
|
||||||
self._dev.Flush()
|
self._dev.Flush()
|
||||||
|
|
||||||
def do_read(self, value):
|
@Command(argument=IntRange(0), result=StringType())
|
||||||
|
def read(self, value):
|
||||||
|
"""read some characters from input buffer"""
|
||||||
return self._dev.Read(value)
|
return self._dev.Read(value)
|
||||||
|
|
||||||
def do_write(self, value):
|
@Command(argument=StringType(), result=None)
|
||||||
|
def write(self, value):
|
||||||
|
"""write some chars to output"""
|
||||||
return self._dev.Write(value)
|
return self._dev.Write(value)
|
||||||
|
|
||||||
def do_readLine(self):
|
@Command(argument=None, result=StringType())
|
||||||
|
def readLine(self):
|
||||||
|
"""Read sol - a whole line - eol"""
|
||||||
return self._dev.ReadLine()
|
return self._dev.ReadLine()
|
||||||
|
|
||||||
def do_writeLine(self, value):
|
@Command(argument=StringType(), result=None)
|
||||||
|
def writeLine(self, value):
|
||||||
|
"""write sol + a whole line + eol"""
|
||||||
return self._dev.WriteLine(value)
|
return self._dev.WriteLine(value)
|
||||||
|
|
||||||
def do_multiCommunicate(self, value):
|
@Command(argument=ArrayOf(TupleOf(StringType(), IntRange()), 100),
|
||||||
|
result=ArrayOf(StringType(), 100))
|
||||||
|
def multiCommunicate(self, value):
|
||||||
|
"""perform a sequence of communications"""
|
||||||
return self._dev.MultiCommunicate(value)
|
return self._dev.MultiCommunicate(value)
|
||||||
|
|
||||||
def do_availableChars(self):
|
@Command(argument=None, result=IntRange(0))
|
||||||
|
def availableChars(self):
|
||||||
|
"""return number of chars in input buffer"""
|
||||||
return self._dev.availableChars
|
return self._dev.availableChars
|
||||||
|
|
||||||
def do_availableLines(self):
|
@Command(argument=None, result=IntRange(0))
|
||||||
|
def availableLines(self):
|
||||||
|
"""return number of lines in input buffer"""
|
||||||
return self._dev.availableLines
|
return self._dev.availableLines
|
||||||
|
@ -20,65 +20,53 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""WAVE FUNCTION LECROY XX: SIGNAL GENERATOR"""
|
"""WAVE FUNCTION LECROY XX: SIGNAL GENERATOR"""
|
||||||
|
|
||||||
from secop.core import Readable, Parameter, Override, Command, FloatRange, TupleOf, \
|
from secop.core import Readable, Parameter, FloatRange, \
|
||||||
HasIodev, StringIO, Done, Attached, IntRange, BoolType, EnumType, StringType, Module, \
|
HasIodev, IntRange, BoolType, EnumType, Module, Property
|
||||||
Property
|
|
||||||
|
|
||||||
|
|
||||||
class Channel(Module):
|
class Channel(HasIodev, Module):
|
||||||
properties = {
|
channel = Property('choose channel to manipulate', IntRange(1, 2))
|
||||||
'channel':Property('choose channel to manipulate',IntRange(1,2)),
|
|
||||||
}
|
freq = Parameter('frequency', FloatRange(1e-6, 20e6, unit='Hz'),
|
||||||
parameters = {
|
poll=True, initwrite=True, default=1000)
|
||||||
'freq':
|
amp = Parameter('exc_volt_int', FloatRange(0.00, 5, unit='Vrms'),
|
||||||
Parameter('frequency', FloatRange(1e-6,20e6,unit='Hz'),
|
poll=True, readonly=False, initwrite=True, default=0.1)
|
||||||
poll=True, initwrite=True, default=1000),
|
offset = Parameter('offset_volt_int', FloatRange(0.00, 10, unit='V'),
|
||||||
'amp':
|
poll=True, readonly=False, initwrite=True, default=0.0)
|
||||||
Parameter('exc_volt_int', FloatRange(0.00,5,unit='Vrms'),
|
wave = Parameter('type of wavefunction',
|
||||||
poll=True, readonly=False, initwrite=True, default=0.1),
|
EnumType('WaveFunction', SINE=1, SQUARE=2, RAMP=3, PULSE=4, NOISE=5, ARB=6, DC=7),
|
||||||
'offset':
|
poll=True, readonly=False, default='SINE'),
|
||||||
Parameter('offset_volt_int', FloatRange(0.00,10,unit='V'),
|
phase = Parameter('signal phase', FloatRange(0, 360, unit='deg'),
|
||||||
poll = True, readonly = False, initwrite = True, default = 0.0),
|
poll=True, readonly=False, initwrite=True, default=0)
|
||||||
'wave':
|
enabled = Parameter('enable output channel', datatype=EnumType('OnOff', OFF=0, ON=1),
|
||||||
Parameter ('type of wavefunction',
|
readonly=False, default='OFF')
|
||||||
EnumType('WaveFunction', SINE=1, SQUARE=2, RAMP=3, PULSE=4, NOISE=5, ARB=6, DC=7),
|
symm = Parameter('wavefunction symmetry', FloatRange(0, 100, unit=''),
|
||||||
poll=True, readonly=False, default='SINE'),
|
poll=True, readonly=False, default=0)
|
||||||
'phase':
|
|
||||||
Parameter('signal phase', FloatRange(0,360,unit='deg'),
|
|
||||||
poll=True, readonly=False, initwrite=True, default=0),
|
|
||||||
'enabled':
|
|
||||||
Parameter('enable output channel', datatype=EnumType('OnOff', OFF=0, ON=1),
|
|
||||||
readonly=False, default='OFF'),
|
|
||||||
'symm':
|
|
||||||
Parameter('wavefunction symmetry', FloatRange(0,100, unit=''),
|
|
||||||
poll=True, readonly =False, default=0),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('C%d:BSWV FRQ?' % self.channel)
|
return self.sendRecv('C%d:BSWV FRQ?' % self.channel)
|
||||||
|
|
||||||
|
def write_target(self, value):
|
||||||
def write_target(self,value):
|
|
||||||
self.sendRecv('C%d:BSWV FRQ, %g' % (self.channel, str(value)+'Hz'))
|
self.sendRecv('C%d:BSWV FRQ, %g' % (self.channel, str(value)+'Hz'))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
#signal wavefunction parameter
|
# signal wavefunction parameter
|
||||||
def read_wave(self):
|
def read_wave(self):
|
||||||
return self.sendRecv('C%d:BSWV WVTP?' % self.channel)
|
return self.sendRecv('C%d:BSWV WVTP?' % self.channel)
|
||||||
|
|
||||||
def write_wave(self,value): #string value
|
def write_wave(self, value): # string value
|
||||||
self.sendRecv('C%d:BSWV WVTP, %s' % (self.channel, value.name))
|
self.sendRecv('C%d:BSWV WVTP, %s' % (self.channel, value.name))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
#signal amplitude parameter
|
# signal amplitude parameter
|
||||||
def read_amp(self):
|
def read_amp(self):
|
||||||
return self.sendRecv('C%d:BSWV AMP?' % self.channel)
|
return self.sendRecv('C%d:BSWV AMP?' % self.channel)
|
||||||
|
|
||||||
def write_amp(self,value):
|
def write_amp(self, value):
|
||||||
self.sendRecv('C%d:BSWV AMP, %g' % (self.channel, value))
|
self.sendRecv('C%d:BSWV AMP, %g' % (self.channel, value))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
#offset value parameter
|
# offset value parameter
|
||||||
def read_offset(self):
|
def read_offset(self):
|
||||||
return self.sendRecv('C%d:BSWV OFST?' % self.channel)
|
return self.sendRecv('C%d:BSWV OFST?' % self.channel)
|
||||||
|
|
||||||
@ -86,44 +74,41 @@ class Channel(Module):
|
|||||||
self.sendRecv('C%d:BSWV OFST %g' % (self.channel, value))
|
self.sendRecv('C%d:BSWV OFST %g' % (self.channel, value))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
# channel symmetry
|
||||||
# channel symmetry
|
|
||||||
def read_symm(self):
|
def read_symm(self):
|
||||||
return self.sendRecv('C%d:BSWV SYM?' % self.channel)
|
return self.sendRecv('C%d:BSWV SYM?' % self.channel)
|
||||||
|
|
||||||
def write_symm(self, value):
|
def write_symm(self, value):
|
||||||
self.comm('C%d:BSWV SYM %g' % (self.channel, value))
|
self.sendRecv('C%d:BSWV SYM %g' % (self.channel, value))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
# wave phase parameter
|
# wave phase parameter
|
||||||
def read_phase(self):
|
def read_phase(self):
|
||||||
return self.sendRecv('C%d:BSWV PHSE?' % self.channel)
|
return self.sendRecv('C%d:BSWV PHSE?' % self.channel)
|
||||||
|
|
||||||
def write_phase(self, value):
|
def write_phase(self, value):
|
||||||
self.sendRecv('C%d:BSWV PHSE %g' % (self.channel, str(value)))
|
self.sendRecv('C%d:BSWV PHSE %g' % (self.channel, str(value)))
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
# dis/enable output channel
|
||||||
# dis/enable output channel
|
|
||||||
def read_enabled(self):
|
def read_enabled(self):
|
||||||
return self.sendRecv('C%d: OUTP?' % self.channel)
|
return self.sendRecv('C%d: OUTP?' % self.channel)
|
||||||
|
|
||||||
def write_enabled(self, value):
|
def write_enabled(self, value):
|
||||||
self.sendRecv('C%d: OUTP %s' % (self.channel, value.name))
|
self.sendRecv('C%d: OUTP %s' % (self.channel, value.name))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
# devices are defined as arg less output enable what is defined as arg2
|
# devices are defined as arg less output enable what is defined as arg2
|
||||||
|
|
||||||
class arg(Readable):
|
class arg(Readable):
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=FloatRange(unit='')),
|
value = Parameter(datatype=FloatRange(unit=''))
|
||||||
}
|
|
||||||
|
|
||||||
class arg2(Readable):
|
class arg2(Readable):
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=BoolType(unit='')),
|
value = Parameter(datatype=BoolType())
|
||||||
}
|
|
||||||
|
@ -20,262 +20,29 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""SIGNAL RECOVERY SR7270: lOCKIN AMPLIFIER FOR AC SUSCEPTIBILITY"""
|
"""SIGNAL RECOVERY SR7270: lOCKIN AMPLIFIER FOR AC SUSCEPTIBILITY"""
|
||||||
|
|
||||||
from secop.core import Readable, Parameter, Override, Command, FloatRange, TupleOf, \
|
from secop.core import FloatRange, HasIodev, \
|
||||||
HasIodev, StringIO, Done, Attached, IntRange, BoolType, EnumType
|
Parameter, Readable, StringIO, TupleOf
|
||||||
|
|
||||||
|
|
||||||
class SR7270(StringIO):
|
class SR7270(StringIO):
|
||||||
end_of_line = b'\x00'
|
# end_of_line = '\x00' #termination line from maanual page 6.8
|
||||||
|
end_of_line = '\n'
|
||||||
def do_communicate(self, command): #remove dash from terminator
|
|
||||||
reply = StringIO.do_communicate(self, command)
|
|
||||||
status = self._conn.readbytes(2, 0.1) # get the 2 status bytes
|
class XY(HasIodev, Readable):
|
||||||
# print('comm=',command,'reply=',reply,'status=',status)
|
value = Parameter('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V')))
|
||||||
return reply + ';%d;%d' % tuple(status)
|
freq = Parameter('exc_freq_int', FloatRange(0.001,250e3,unit='Hz'), readonly=False, default=100)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class XY(HasIodev, Readable):
|
|
||||||
properties = {
|
|
||||||
'x': Attached(),
|
|
||||||
'y': Attached(),
|
|
||||||
'freq_arg': Attached(),
|
|
||||||
'amp_arg': Attached(),
|
|
||||||
'tc_arg': Attached(),
|
|
||||||
'phase_arg': Attached(),
|
|
||||||
'dac_arg': Attached(),
|
|
||||||
}#parameters required an initial value but initwrite write the default value for polled parameters
|
|
||||||
parameters = {
|
|
||||||
'value': Override('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V'))),
|
|
||||||
'freq': Parameter('exc_freq_int',
|
|
||||||
FloatRange(0.001,250e3,unit='Hz'),
|
|
||||||
poll=True, readonly=False, initwrite=True, default=1000),
|
|
||||||
'amp': Parameter('exc_volt_int',
|
|
||||||
FloatRange(0.00,5,unit='Vrms'),
|
|
||||||
poll=True, readonly=False, initwrite=True, default=0.1),
|
|
||||||
'range': Parameter('sensitivity value', FloatRange(0.00,1,unit='V'), poll=True, default=1),
|
|
||||||
'irange': Parameter('sensitivity index', IntRange(0,27), poll=True, readonly=False, default=25),
|
|
||||||
'autorange': Parameter('autorange_on', EnumType('autorange', off=0, soft=1, hard=2), readonly=False, default=0, initwrite=True),
|
|
||||||
'tc': Parameter('time constant value', FloatRange(10e-6,100,unit='s'), poll=True, default=0.1),
|
|
||||||
'itc': Parameter('time constant index', IntRange(0,30), poll=True, readonly=False, initwrite=True, default=14),
|
|
||||||
'nm': Parameter ('noise mode',BoolType(), readonly=False, default=0),
|
|
||||||
'phase': Parameter('Reference phase control', FloatRange(-360,360,unit='deg'), poll=True, readonly=False, initwrite=True, default=0),
|
|
||||||
'vmode' : Parameter('Voltage input configuration', IntRange(0,3), readonly=False, default=3),
|
|
||||||
# 'dac': Parameter ('output DAC channel value', datatype=TupleOf(IntRange(1,4), FloatRange(0.00,5000,unit='mV')), poll=True, readonly=False, initwrite=True, default=(3,0)),
|
|
||||||
'dac': Parameter ('output DAC channel value', FloatRange(-10000,10000,unit='mV'), poll=True, readonly=False, initwrite=True, default=0),
|
|
||||||
}
|
|
||||||
commands = {
|
|
||||||
'aphase': Command('auto phase'),
|
|
||||||
}
|
|
||||||
iodevClass = SR7270
|
iodevClass = SR7270
|
||||||
|
|
||||||
|
|
||||||
def comm(self, command):
|
|
||||||
reply, status, overload = self.sendRecv(command).split(';')
|
|
||||||
if overload != '0':
|
|
||||||
self.status = self.Status.WARN, 'overload %s' % overload
|
|
||||||
else:
|
|
||||||
self.status = self.Status.IDLE, ''
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
reply = self.comm('XY.').split(',')
|
reply = self.sendRecv('XY.').split('\x00')[-1]
|
||||||
x = float(reply[0])
|
return reply.split(',')
|
||||||
y = float(reply[1])
|
|
||||||
if self.autorange == 1: # soft
|
|
||||||
if max(abs(x), abs(y)) >= 0.9*self.range and self.irange < 27:
|
|
||||||
self.write_irange(self.irange+1)
|
|
||||||
elif max(abs(x), abs(y)) <= 0.3*self.range and self.irange > 1:
|
|
||||||
self.write_irange(self.irange-1)
|
|
||||||
self._x.value = x # to update X,Y classes which will be the collected data.
|
|
||||||
self._y.value = y
|
|
||||||
# print(x,y)
|
|
||||||
self._freq_arg.value = self.freq
|
|
||||||
self._amp_arg.value = self.amp
|
|
||||||
self._tc_arg.value = self.tc
|
|
||||||
self._phase_arg.value = self.phase
|
|
||||||
self._dac_arg.value = self.dac
|
|
||||||
return x,y
|
|
||||||
|
|
||||||
def read_freq(self):
|
def read_freq(self):
|
||||||
reply = self.comm('OF.')
|
reply = self.sendRecv('OF.').split('\x00')[-1]
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
def write_freq(self,value):
|
def write_freq(self,value):
|
||||||
self.comm('OF. %g' % value)
|
self.sendRecv('OF. %g' % value)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def write_autorange(self, value):
|
|
||||||
if value == 2: # hard
|
|
||||||
self.comm('AS') # put hardware autorange on
|
|
||||||
self.comm('AUTOMATIC. 1')
|
|
||||||
else:
|
|
||||||
self.comm('AUTOMATIC. 0')
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_autorange(self):
|
|
||||||
reply=self.comm('AUTOMATIC')
|
|
||||||
# determine hardware autorange
|
|
||||||
if reply == 1: #"hardware auto range is on":
|
|
||||||
return 2 # hard
|
|
||||||
if self.autorange == 0: # soft
|
|
||||||
return self.autorange() #read autorange
|
|
||||||
return reply # off
|
|
||||||
|
|
||||||
#oscillator amplitude module
|
|
||||||
def read_amp(self):
|
|
||||||
reply = self.comm('OA.')
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def write_amp(self,value):
|
|
||||||
self.comm('OA. %g' % value)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
#external output DAC
|
|
||||||
def read_dac(self):
|
|
||||||
# reply = self.comm('DAC %g' % channel) # failed to add the DAC channel you want to control
|
|
||||||
reply = self.comm('DAC 3') #stack to channel 3
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def write_dac(self,value):
|
|
||||||
#self.comm('DAC %g %g' % channel % value)
|
|
||||||
self.comm('DAC 3 %g' % value)
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
#sensitivity module
|
|
||||||
def read_range(self):
|
|
||||||
reply = self.comm('SEN.')
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
def write_irange(self,value):
|
|
||||||
self.comm('SEN %g' % value)
|
|
||||||
self.read_range()
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_irange(self):
|
|
||||||
reply = self.comm('SEN')
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
#time constant module/ noisemode off or 0 allows to use all the time constant range
|
|
||||||
def read_nm(self):
|
|
||||||
reply = self.comm('NOISEMODE')
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def write_nm(self,value):
|
|
||||||
self.comm('NOISEMODE %d' % int(value))
|
|
||||||
self.read_nm()
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def read_tc(self):
|
|
||||||
reply = self.comm('TC.')
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
|
|
||||||
def write_itc(self,value):
|
|
||||||
self.comm('TC %g' % value)
|
|
||||||
self.read_tc()
|
|
||||||
return value
|
|
||||||
|
|
||||||
def read_itc(self):
|
|
||||||
reply = self.comm('TC')
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
#phase and autophase
|
|
||||||
|
|
||||||
|
|
||||||
def read_phase(self):
|
|
||||||
reply = self.comm('REFP.')
|
|
||||||
|
|
||||||
return reply
|
|
||||||
|
|
||||||
def write_phase(self,value):
|
|
||||||
self.comm('REFP %d' % round(1000*value,0))
|
|
||||||
self.read_phase()
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def do_aphase(self):
|
|
||||||
self.read_phase()
|
|
||||||
reply = self.comm('AQN')
|
|
||||||
self.read_phase()
|
|
||||||
|
|
||||||
#voltage input configuration 0:grounded,1=A,2=B,3=A-B
|
|
||||||
# def read_vmode(self):
|
|
||||||
# reply = self.comm('VMODE')
|
|
||||||
# return reply
|
|
||||||
|
|
||||||
def write_vmode(self,value):
|
|
||||||
self.comm('VMODE %d' % value)
|
|
||||||
# self.read_vmode()
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class Comp(Readable):
|
|
||||||
pollerClass = None
|
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=FloatRange(unit='V')),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class arg(Readable):
|
|
||||||
pollerClass = None
|
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=FloatRange(unit='')),
|
|
||||||
}
|
|
||||||
|
|
||||||
# parameters = {
|
|
||||||
# 'valueX': Override('X, Y', datatype=TupleOf(FloatRange(unit='V'), FloatRange(unit='V'))),
|
|
||||||
#}
|
|
||||||
#iodevClass = SR7270
|
|
||||||
# def read_valueX(self):
|
|
||||||
# reply = self.sendRecv('XY.')
|
|
||||||
# return reply.split(',')[0]
|
|
||||||
# def read_valueY(self):
|
|
||||||
# reply = self.sendRecv('XY.')
|
|
||||||
# return reply.split(',')[1]
|
|
||||||
|
|
||||||
|
|
||||||
#class aphase(self):
|
|
||||||
# reply = self.sendRecv('ASM')
|
|
||||||
# return reply
|
|
||||||
|
|
||||||
# def asens(self):
|
|
||||||
# reply = self.sendRecv('AS')
|
|
||||||
# return reply
|
|
||||||
|
|
||||||
# def write_Fstart(self,value):
|
|
||||||
# self.sendRecv('FSTART. %g' % value)
|
|
||||||
# return value
|
|
||||||
|
|
||||||
# def write_Fstop(self,value):
|
|
||||||
# self.sendRecv('FSTOP. %g' % value)
|
|
||||||
# return value
|
|
||||||
|
|
||||||
# def write_Fstep(self,value):
|
|
||||||
# self.sendRecv('FSTEP. %g' % value)
|
|
||||||
# return value
|
|
||||||
|
|
||||||
# def write_Astart(self,value):
|
|
||||||
# self.sendRecv('ASTART. %g' % value')
|
|
||||||
# return value
|
|
||||||
|
|
||||||
# def write_Astop(self,value):
|
|
||||||
# self.sendRecv('ASTOP. %g' % value)
|
|
||||||
# return value
|
|
||||||
|
|
||||||
# def write_Astep(self,value):
|
|
||||||
# self.sendRecv('ASTEP. %g' % value)
|
|
||||||
# return value
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Andeen Hagerling capacitance bridge"""
|
"""Andeen Hagerling capacitance bridge"""
|
||||||
|
|
||||||
from secop.core import Readable, Parameter, Override, FloatRange, HasIodev, StringIO, Done
|
from secop.core import Done, FloatRange, HasIodev, Parameter, Readable, StringIO
|
||||||
|
|
||||||
|
|
||||||
class Ah2700IO(StringIO):
|
class Ah2700IO(StringIO):
|
||||||
@ -29,12 +29,12 @@ class Ah2700IO(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Capacitance(HasIodev, Readable):
|
class Capacitance(HasIodev, Readable):
|
||||||
parameters = {
|
|
||||||
'value': Override('capacitance', FloatRange(unit='pF'), poll=True),
|
value = Parameter('capacitance', FloatRange(unit='pF'), poll=True)
|
||||||
'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0),
|
freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
|
||||||
'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0),
|
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
|
||||||
'loss': Parameter('loss', FloatRange(unit='deg'), default=0),
|
loss = Parameter('loss', FloatRange(unit='deg'), default=0)
|
||||||
}
|
|
||||||
iodevClass = Ah2700IO
|
iodevClass = Ah2700IO
|
||||||
|
|
||||||
def parse_reply(self, reply):
|
def parse_reply(self, reply):
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Delay generator stanford 645"""
|
"""Delay generator stanford 645"""
|
||||||
|
|
||||||
from secop.core import Module, Parameter, Override, FloatRange, HasIodev, StringIO, Done
|
from secop.core import FloatRange, HasIodev, Module, Parameter, StringIO
|
||||||
|
|
||||||
|
|
||||||
class DG645(StringIO):
|
class DG645(StringIO):
|
||||||
@ -28,12 +28,12 @@ class DG645(StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Delay(HasIodev, Module):
|
class Delay(HasIodev, Module):
|
||||||
parameters = {
|
|
||||||
'on1': Parameter('on delay 1', FloatRange(unit='sec'), readonly=False, default=0),
|
on1 = Parameter('on delay 1', FloatRange(unit='sec'), readonly=False, default=0)
|
||||||
'off1': Parameter('off delay 1', FloatRange(unit='sec'), readonly=False, default=60e-9),
|
off1 = Parameter('off delay 1', FloatRange(unit='sec'), readonly=False, default=60e-9)
|
||||||
'on2': Parameter('on delay 2', FloatRange(unit='sec'), readonly=False, default=0),
|
on2 = Parameter('on delay 2', FloatRange(unit='sec'), readonly=False, default=0)
|
||||||
'off2': Parameter('off delay 2', FloatRange(unit='sec'), readonly=False, default=150e-9),
|
off2 = Parameter('off delay 2', FloatRange(unit='sec'), readonly=False, default=150e-9)
|
||||||
}
|
|
||||||
iodevClass = DG645
|
iodevClass = DG645
|
||||||
|
|
||||||
def read_on1(self):
|
def read_on1(self):
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
|
|
||||||
not tested yet"""
|
not tested yet"""
|
||||||
|
|
||||||
from secop.core import Writable, Module, Parameter, Override, Attached,\
|
from secop.core import Attached, BoolType, EnumType, FloatRange, \
|
||||||
BoolType, FloatRange, EnumType, HasIodev, StringIO
|
HasIodev, Module, Parameter, StringIO, Writable
|
||||||
|
|
||||||
|
|
||||||
class K2601bIO(StringIO):
|
class K2601bIO(StringIO):
|
||||||
@ -42,13 +42,13 @@ SOURCECMDS = {
|
|||||||
|
|
||||||
|
|
||||||
class SourceMeter(HasIodev, Module):
|
class SourceMeter(HasIodev, Module):
|
||||||
parameters = {
|
|
||||||
'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True),
|
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True)
|
||||||
'power': Parameter('readback power', FloatRange(unit='W'), poll=True),
|
power = Parameter('readback power', FloatRange(unit='W'), poll=True)
|
||||||
'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
|
||||||
readonly=False, default=0),
|
readonly=False, default=0)
|
||||||
'active': Parameter('output enable', BoolType(), readonly=False, poll=True),
|
active = Parameter('output enable', BoolType(), readonly=False, poll=True)
|
||||||
}
|
|
||||||
iodevClass = K2601bIO
|
iodevClass = K2601bIO
|
||||||
|
|
||||||
def read_resistivity(self):
|
def read_resistivity(self):
|
||||||
@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module):
|
|||||||
|
|
||||||
|
|
||||||
class Current(HasIodev, Writable):
|
class Current(HasIodev, Writable):
|
||||||
properties = {
|
sourcemeter = Attached()
|
||||||
'sourcemeter': Attached(),
|
|
||||||
}
|
value = Parameter('measured current', FloatRange(unit='A'), poll=True)
|
||||||
parameters = {
|
target = Parameter('set current', FloatRange(unit='A'), poll=True)
|
||||||
'value': Override('measured current', FloatRange(unit='A'), poll=True),
|
active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage
|
||||||
'target': Override('set current', FloatRange(unit='A'), poll=True),
|
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True)
|
||||||
'active': Parameter('current is controlled', BoolType(), default=False), # polled from Current/Voltage
|
|
||||||
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('print(smua.measure.i())')
|
return self.sendRecv('print(smua.measure.i())')
|
||||||
@ -120,15 +117,12 @@ class Current(HasIodev, Writable):
|
|||||||
|
|
||||||
|
|
||||||
class Voltage(HasIodev, Writable):
|
class Voltage(HasIodev, Writable):
|
||||||
properties = {
|
sourcemeter = Attached()
|
||||||
'sourcemeter': Attached(),
|
|
||||||
}
|
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True)
|
||||||
parameters = {
|
target = Parameter('set voltage', FloatRange(unit='V'), poll=True)
|
||||||
'value': Override('measured voltage', FloatRange(unit='V'), poll=True),
|
active = Parameter('voltage is controlled', BoolType(), poll=True)
|
||||||
'target': Override('set voltage', FloatRange(unit='V'), poll=True),
|
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True)
|
||||||
'active': Parameter('voltage is controlled', BoolType(), poll=True),
|
|
||||||
'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
return self.sendRecv('print(smua.measure.v())')
|
return self.sendRecv('print(smua.measure.v())')
|
||||||
@ -159,7 +153,7 @@ class Voltage(HasIodev, Writable):
|
|||||||
def write_active(self, value):
|
def write_active(self, value):
|
||||||
if self._sourcemeter.mode != 2:
|
if self._sourcemeter.mode != 2:
|
||||||
if value:
|
if value:
|
||||||
self._sourcemeter.write_mode(2) # switch to voltage
|
self._sourcemeter.write_mode(2) # switch to voltage
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
return self._sourcemeter.write_active(value)
|
return self._sourcemeter.write_active(value)
|
||||||
|
@ -22,13 +22,13 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from secop.modules import Readable, Drivable, Parameter, Override, Property, Attached
|
|
||||||
from secop.metaclass import Done
|
|
||||||
from secop.datatypes import FloatRange, IntRange, EnumType, BoolType
|
|
||||||
from secop.stringio import HasIodev
|
|
||||||
from secop.poller import Poller, REGULAR
|
|
||||||
from secop.lib import formatStatusBits
|
|
||||||
import secop.iohandler
|
import secop.iohandler
|
||||||
|
from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
|
||||||
|
from secop.lib import formatStatusBits
|
||||||
|
from secop.modules import Attached, Done, \
|
||||||
|
Drivable, Parameter, Property, Readable
|
||||||
|
from secop.poller import REGULAR, Poller
|
||||||
|
from secop.stringio import HasIodev
|
||||||
|
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
|
|
||||||
@ -59,19 +59,18 @@ class StringIO(secop.stringio.StringIO):
|
|||||||
|
|
||||||
|
|
||||||
class Main(HasIodev, Drivable):
|
class Main(HasIodev, Drivable):
|
||||||
parameters = {
|
|
||||||
'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)),
|
value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17))
|
||||||
'target': Override('channel to select', datatype=IntRange(0, 17)),
|
target = Parameter('channel to select', datatype=IntRange(0, 17))
|
||||||
'autoscan':
|
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
|
||||||
Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False),
|
pollinterval = Parameter('sleeptime between polls', default=1)
|
||||||
'pollinterval': Override('sleeptime between polls', default=1),
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerClass = Poller
|
pollerClass = Poller
|
||||||
iodevClass = StringIO
|
iodevClass = StringIO
|
||||||
|
_channel_changed = 0 # time of last channel change
|
||||||
|
_channels = None # dict <channel no> of <module object>
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
self._channel_changed = 0
|
|
||||||
self._channels = {}
|
self._channels = {}
|
||||||
|
|
||||||
def register_channel(self, modobj):
|
def register_channel(self, modobj):
|
||||||
@ -85,10 +84,8 @@ class Main(HasIodev, Drivable):
|
|||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
channel, auto = scan.send_command(self)
|
channel, auto = scan.send_command(self)
|
||||||
# response = self.sendRecv('SCAN?').strip().split(',')
|
|
||||||
# channel, auto = (int(s) for s in response)
|
|
||||||
if channel not in self._channels:
|
if channel not in self._channels:
|
||||||
return channel
|
return channel
|
||||||
if not self._channels[channel].enabled:
|
if not self._channels[channel].enabled:
|
||||||
# channel was disabled recently, but still selected
|
# channel was disabled recently, but still selected
|
||||||
nextchannel = 0
|
nextchannel = 0
|
||||||
@ -129,61 +126,42 @@ class ResChannel(HasIodev, Readable):
|
|||||||
|
|
||||||
RES_RANGE = {key: i+1 for i, key in list(
|
RES_RANGE = {key: i+1 for i, key in list(
|
||||||
enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm']
|
enumerate(mag % val for mag in ['%gmOhm', '%gOhm', '%gkOhm', '%gMOhm']
|
||||||
for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]}
|
for val in [2, 6.32, 20, 63.2, 200, 632]))[:-2]}
|
||||||
RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7, 16)] # RES_SCALE[0] is not used
|
RES_SCALE = [2 * 10 ** (0.5 * i) for i in range(-7, 16)] # RES_SCALE[0] is not used
|
||||||
CUR_RANGE = {key: i + 1 for i, key in list(
|
CUR_RANGE = {key: i + 1 for i, key in list(
|
||||||
enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA']
|
enumerate(mag % val for mag in ['%gpA', '%gnA', '%guA', '%gmA']
|
||||||
for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]}
|
for val in [1, 3.16, 10, 31.6, 100, 316]))[:-2]}
|
||||||
VOLT_RANGE = {key: i + 1 for i, key in list(
|
VOLT_RANGE = {key: i + 1 for i, key in list(
|
||||||
enumerate(mag % val for mag in ['%guV', '%gmV']
|
enumerate(mag % val for mag in ['%guV', '%gmV']
|
||||||
for val in [2, 6.32, 20, 63.2, 200, 632]))}
|
for val in [2, 6.32, 20, 63.2, 200, 632]))}
|
||||||
|
|
||||||
pollerClass = Poller
|
pollerClass = Poller
|
||||||
iodevClass = StringIO
|
iodevClass = StringIO
|
||||||
|
_main = None # main module
|
||||||
|
_last_range_change = 0 # time of last range change
|
||||||
|
|
||||||
properties = {
|
channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False)
|
||||||
'channel':
|
main = Attached()
|
||||||
Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False),
|
|
||||||
'main':
|
|
||||||
Attached()
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='Ohm'))
|
||||||
'value':
|
pollinterval = Parameter(visibility=3)
|
||||||
Override(datatype=FloatRange(unit='Ohm')),
|
range = Parameter('reading range', readonly=False,
|
||||||
'pollinterval':
|
datatype=EnumType(**RES_RANGE), handler=rdgrng)
|
||||||
Override(visibility=3),
|
minrange = Parameter('minimum range for software autorange', readonly=False, default=1,
|
||||||
'range':
|
datatype=EnumType(**RES_RANGE))
|
||||||
Parameter('reading range', readonly=False,
|
autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
||||||
datatype=EnumType(**RES_RANGE), handler=rdgrng),
|
readonly=False, handler=rdgrng, default=2)
|
||||||
'minrange':
|
iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng)
|
||||||
Parameter('minimum range for software autorange', readonly=False, default=1,
|
vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng)
|
||||||
datatype=EnumType(**RES_RANGE)),
|
enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset)
|
||||||
'autorange':
|
pause = Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset)
|
||||||
Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2),
|
dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset)
|
||||||
readonly=False, handler=rdgrng, default=2),
|
filter = Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl)
|
||||||
'iexc':
|
|
||||||
Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng),
|
|
||||||
'vexc':
|
|
||||||
Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng),
|
|
||||||
'enabled':
|
|
||||||
Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset),
|
|
||||||
'pause':
|
|
||||||
Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset),
|
|
||||||
'dwell':
|
|
||||||
Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset),
|
|
||||||
'filter':
|
|
||||||
Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl),
|
|
||||||
}
|
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._main = self.DISPATCHER.get_module(self.main)
|
self._main = self.DISPATCHER.get_module(self.main)
|
||||||
self._main.register_channel(self)
|
self._main.register_channel(self)
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
|
||||||
self._last_range_change = 0
|
|
||||||
super().startModule(started_callback)
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
if self.channel != self._main.value:
|
if self.channel != self._main.value:
|
||||||
return Done
|
return Done
|
||||||
@ -195,7 +173,7 @@ class ResChannel(HasIodev, Readable):
|
|||||||
if self.autorange == 'soft':
|
if self.autorange == 'soft':
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if now > self._last_range_change + self.pause:
|
if now > self._last_range_change + self.pause:
|
||||||
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
rng = int(max(self.minrange, self.range)) # convert from enum to int
|
||||||
if self.status[1] == '':
|
if self.status[1] == '':
|
||||||
if abs(result) > self.RES_SCALE[rng]:
|
if abs(result) > self.RES_SCALE[rng]:
|
||||||
if rng < 22:
|
if rng < 22:
|
||||||
@ -236,8 +214,6 @@ class ResChannel(HasIodev, Readable):
|
|||||||
result = dict(range=rng)
|
result = dict(range=rng)
|
||||||
if autorange:
|
if autorange:
|
||||||
result['autorange'] = 'hard'
|
result['autorange'] = 'hard'
|
||||||
#elif self.autorange == 'hard':
|
|
||||||
# result['autorange'] = 'soft'
|
|
||||||
# else: do not change autorange
|
# else: do not change autorange
|
||||||
self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange))
|
||||||
if excoff:
|
if excoff:
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
from secop.modules import Communicator
|
from secop.modules import Communicator
|
||||||
|
|
||||||
|
|
||||||
class Ls370Sim(Communicator):
|
class Ls370Sim(Communicator):
|
||||||
CHANNEL_COMMANDS = [
|
CHANNEL_COMMANDS = [
|
||||||
('RDGR?%d', '1.0'),
|
('RDGR?%d', '1.0'),
|
||||||
@ -32,9 +33,8 @@ class Ls370Sim(Communicator):
|
|||||||
]
|
]
|
||||||
OTHER_COMMANDS = [
|
OTHER_COMMANDS = [
|
||||||
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
('*IDN?', 'LSCI,MODEL370,370184,05302003'),
|
||||||
('SCAN?', '1,1'),
|
('SCAN?', '3,1'),
|
||||||
]
|
]
|
||||||
channel = [None]
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
self._data = dict(self.OTHER_COMMANDS)
|
self._data = dict(self.OTHER_COMMANDS)
|
||||||
@ -43,7 +43,7 @@ class Ls370Sim(Communicator):
|
|||||||
self._data[fmt % chan] = v
|
self._data[fmt % chan] = v
|
||||||
# mkthread(self.run)
|
# mkthread(self.run)
|
||||||
|
|
||||||
def do_communicate(self, command):
|
def communicate(self, command):
|
||||||
# simulation part, time independent
|
# simulation part, time independent
|
||||||
for channel in range(1,17):
|
for channel in range(1,17):
|
||||||
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
_, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',')
|
||||||
|
@ -31,20 +31,19 @@ Polling of value and status is done commonly for all modules. For each registere
|
|||||||
<module>.update_value_status() is called in order to update their value and status.
|
<module>.update_value_status() is called in order to update their value and status.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
from secop.modules import Module, Readable, Drivable, Parameter, Override,\
|
|
||||||
Communicator, Property, Attached
|
|
||||||
from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\
|
|
||||||
BoolType, StatusType
|
|
||||||
from secop.lib.enum import Enum
|
|
||||||
from secop.lib import clamp
|
|
||||||
from secop.errors import HardwareError
|
|
||||||
from secop.poller import Poller
|
|
||||||
import secop.iohandler
|
import secop.iohandler
|
||||||
|
from secop.datatypes import BoolType, EnumType, \
|
||||||
|
FloatRange, IntRange, StatusType, StringType
|
||||||
|
from secop.errors import HardwareError
|
||||||
|
from secop.lib import clamp
|
||||||
|
from secop.lib.enum import Enum
|
||||||
|
from secop.modules import Attached, Communicator, Done, \
|
||||||
|
Drivable, Parameter, Property, Readable
|
||||||
|
from secop.poller import Poller
|
||||||
from secop.stringio import HasIodev
|
from secop.stringio import HasIodev
|
||||||
from secop.metaclass import Done
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import secop_psi.ppmswindows as ppmshw
|
import secop_psi.ppmswindows as ppmshw
|
||||||
@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler):
|
|||||||
class Main(Communicator):
|
class Main(Communicator):
|
||||||
"""ppms communicator module"""
|
"""ppms communicator module"""
|
||||||
|
|
||||||
parameters = {
|
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2)
|
||||||
'pollinterval': Parameter('poll interval', readonly=False,
|
data = Parameter('internal', StringType(), poll=True, export=True, # export for test only
|
||||||
datatype=FloatRange(), default=2),
|
default="", readonly=True)
|
||||||
'communicate': Override('GBIP command'),
|
|
||||||
'data': Parameter('internal', poll=True, export=True, # export for test only
|
|
||||||
default="", readonly=True, datatype=StringType()),
|
|
||||||
}
|
|
||||||
properties = {
|
|
||||||
'class_id': Property('Quantum Design class id', export=False,
|
|
||||||
datatype=StringType()),
|
|
||||||
}
|
|
||||||
|
|
||||||
_channel_names = ['packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2',
|
class_id = Property('Quantum Design class id', StringType(), export=False)
|
||||||
|
|
||||||
|
_channel_names = [
|
||||||
|
'packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2',
|
||||||
'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2',
|
'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2',
|
||||||
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
|
'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29']
|
||||||
assert len(_channel_names) == 30
|
assert len(_channel_names) == 30
|
||||||
@ -102,7 +96,8 @@ class Main(Communicator):
|
|||||||
def register(self, other):
|
def register(self, other):
|
||||||
self.modules[other.channel] = other
|
self.modules[other.channel] = other
|
||||||
|
|
||||||
def do_communicate(self, command):
|
def communicate(self, command):
|
||||||
|
"""GPIB command"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
reply = self._ppms_device.send(command)
|
reply = self._ppms_device.send(command)
|
||||||
self.log.debug("%s|%s", command, reply)
|
self.log.debug("%s|%s", command, reply)
|
||||||
@ -114,7 +109,7 @@ class Main(Communicator):
|
|||||||
if channel.enabled:
|
if channel.enabled:
|
||||||
mask |= 1 << self._channel_to_index.get(channelname, 0)
|
mask |= 1 << self._channel_to_index.get(channelname, 0)
|
||||||
# send, read and convert to floats and ints
|
# send, read and convert to floats and ints
|
||||||
data = self.do_communicate('GETDAT? %d' % mask)
|
data = self.communicate('GETDAT? %d' % mask)
|
||||||
reply = data.split(',')
|
reply = data.split(',')
|
||||||
mask = int(reply.pop(0))
|
mask = int(reply.pop(0))
|
||||||
reply.pop(0) # pop timestamp
|
reply.pop(0) # pop timestamp
|
||||||
@ -133,23 +128,23 @@ class Main(Communicator):
|
|||||||
return data # return data as string
|
return data # return data as string
|
||||||
|
|
||||||
|
|
||||||
class PpmsMixin(HasIodev, Module):
|
class PpmsBase(HasIodev, Readable):
|
||||||
"""common methods for ppms modules"""
|
"""common base for all ppms modules"""
|
||||||
|
iodev = Attached()
|
||||||
parameters = {
|
|
||||||
'pollinterval': None,
|
|
||||||
}
|
|
||||||
|
|
||||||
pollerClass = Poller
|
pollerClass = Poller
|
||||||
enabled = True # default, if no parameter enable is defined
|
enabled = True # default, if no parameter enable is defined
|
||||||
_last_settings = None # used by several modules
|
_last_settings = None # used by several modules
|
||||||
slow_pollfactor = 1
|
slow_pollfactor = 1
|
||||||
|
|
||||||
|
# as this pollinterval affects only the polling of settings
|
||||||
|
# it would be confusing to export it.
|
||||||
|
pollinterval = Parameter(export=False)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._iodev.register(self)
|
self._iodev.register(self)
|
||||||
|
|
||||||
def startModule(self, started_callback):
|
def startModule(self, started_callback):
|
||||||
""""""
|
|
||||||
# no polls except on main module
|
# no polls except on main module
|
||||||
started_callback()
|
started_callback()
|
||||||
|
|
||||||
@ -160,8 +155,8 @@ class PpmsMixin(HasIodev, Module):
|
|||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
# polling is done by the main module
|
# polling is done by the main module
|
||||||
# and PPMS does not deliver really fresh status values anyway:
|
# and PPMS does not deliver really fresh status values anyway: the status is not
|
||||||
# e.g. the status is not changed immediately after a target change!
|
# changed immediately after a target change!
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
@ -177,29 +172,22 @@ class PpmsMixin(HasIodev, Module):
|
|||||||
self.status = (self.Status.IDLE, '')
|
self.status = (self.Status.IDLE, '')
|
||||||
|
|
||||||
|
|
||||||
class Channel(PpmsMixin, Readable):
|
class Channel(PpmsBase):
|
||||||
"""channel base class"""
|
"""channel base class"""
|
||||||
|
|
||||||
parameters = {
|
value = Parameter('main value of channels', poll=True)
|
||||||
'value':
|
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||||
Override('main value of channels', poll=True),
|
datatype=BoolType(), default=False)
|
||||||
'enabled':
|
|
||||||
Parameter('is this channel used?', readonly=False, poll=False,
|
channel = Property('channel name',
|
||||||
datatype=BoolType(), default=False),
|
datatype=StringType(), export=False, default='')
|
||||||
}
|
no = Property('channel number',
|
||||||
properties = {
|
datatype=IntRange(1, 4), export=False)
|
||||||
'channel':
|
|
||||||
Property('channel name',
|
|
||||||
datatype=StringType(), export=False, default=''),
|
|
||||||
'no':
|
|
||||||
Property('channel number',
|
|
||||||
datatype=IntRange(1, 4), export=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
Readable.earlyInit(self)
|
Readable.earlyInit(self)
|
||||||
if not self.channel:
|
if not self.channel:
|
||||||
self.properties['channel'] = self.name
|
self.channel = self.name
|
||||||
|
|
||||||
def get_settings(self, pname):
|
def get_settings(self, pname):
|
||||||
return ''
|
return ''
|
||||||
@ -208,15 +196,12 @@ class Channel(PpmsMixin, Readable):
|
|||||||
class UserChannel(Channel):
|
class UserChannel(Channel):
|
||||||
"""user channel"""
|
"""user channel"""
|
||||||
|
|
||||||
properties = {
|
# pollinterval = Parameter(visibility=3)
|
||||||
'no':
|
|
||||||
Property('*(unused)*',
|
|
||||||
datatype=IntRange(0, 0), export=False, default=0),
|
|
||||||
'linkenable':
|
|
||||||
Property('name of linked channel for enabling',
|
|
||||||
datatype=StringType(), export=False, default=''),
|
|
||||||
|
|
||||||
}
|
no = Property('channel number',
|
||||||
|
datatype=IntRange(0, 0), export=False, default=0)
|
||||||
|
linkenable = Property('name of linked channel for enabling',
|
||||||
|
datatype=StringType(), export=False, default='')
|
||||||
|
|
||||||
def write_enabled(self, enabled):
|
def write_enabled(self, enabled):
|
||||||
other = self._iodev.modules.get(self.linkenable, None)
|
other = self._iodev.modules.get(self.linkenable, None)
|
||||||
@ -230,14 +215,11 @@ class DriverChannel(Channel):
|
|||||||
|
|
||||||
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g')
|
||||||
|
|
||||||
parameters = {
|
current = Parameter('driver current', readonly=False, handler=drvout,
|
||||||
'current':
|
datatype=FloatRange(0., 5000., unit='uA'))
|
||||||
Parameter('driver current', readonly=False, handler=drvout,
|
powerlimit = Parameter('power limit', readonly=False, handler=drvout,
|
||||||
datatype=FloatRange(0., 5000., unit='uA')),
|
datatype=FloatRange(0., 1000., unit='uW'))
|
||||||
'powerlimit':
|
# pollinterval = Parameter(visibility=3)
|
||||||
Parameter('power limit', readonly=False, handler=drvout,
|
|
||||||
datatype=FloatRange(0., 1000., unit='uW')),
|
|
||||||
}
|
|
||||||
|
|
||||||
def analyze_drvout(self, no, current, powerlimit):
|
def analyze_drvout(self, no, current, powerlimit):
|
||||||
if self.no != no:
|
if self.no != no:
|
||||||
@ -255,25 +237,19 @@ class BridgeChannel(Channel):
|
|||||||
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g')
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2)
|
||||||
parameters = {
|
|
||||||
'enabled':
|
enabled = Parameter(handler=bridge)
|
||||||
Override(handler=bridge),
|
excitation = Parameter('excitation current', readonly=False, handler=bridge,
|
||||||
'excitation':
|
datatype=FloatRange(0.01, 5000., unit='uA'))
|
||||||
Parameter('excitation current', readonly=False, handler=bridge,
|
powerlimit = Parameter('power limit', readonly=False, handler=bridge,
|
||||||
datatype=FloatRange(0.01, 5000., unit='uA')),
|
datatype=FloatRange(0.001, 1000., unit='uW'))
|
||||||
'powerlimit':
|
dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
||||||
Parameter('power limit', readonly=False, handler=bridge,
|
datatype=BoolType())
|
||||||
datatype=FloatRange(0.001, 1000., unit='uW')),
|
readingmode = Parameter('reading mode', readonly=False, handler=bridge,
|
||||||
'dcflag':
|
datatype=EnumType(ReadingMode))
|
||||||
Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge,
|
voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge,
|
||||||
datatype=BoolType()),
|
datatype=FloatRange(0.0001, 100., unit='mV'))
|
||||||
'readingmode':
|
# pollinterval = Parameter(visibility=3)
|
||||||
Parameter('reading mode', readonly=False, handler=bridge,
|
|
||||||
datatype=EnumType(ReadingMode)),
|
|
||||||
'voltagelimit':
|
|
||||||
Parameter('voltage limit', readonly=False, handler=bridge,
|
|
||||||
datatype=FloatRange(0.0001, 100., unit='mV')),
|
|
||||||
}
|
|
||||||
|
|
||||||
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit):
|
||||||
if self.no != no:
|
if self.no != no:
|
||||||
@ -294,23 +270,22 @@ class BridgeChannel(Channel):
|
|||||||
return self.no, 0, 0, change.dcflag, change.readingmode, 0
|
return self.no, 0, 0, change.dcflag, change.readingmode, 0
|
||||||
|
|
||||||
|
|
||||||
class Level(PpmsMixin, Readable):
|
class Level(PpmsBase):
|
||||||
"""helium level"""
|
"""helium level"""
|
||||||
|
|
||||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='%'), handler=level)
|
||||||
'value': Override(datatype=FloatRange(unit='%'), handler=level),
|
status = Parameter(handler=level)
|
||||||
'status': Override(handler=level),
|
# pollinterval = Parameter(visibility=3)
|
||||||
}
|
|
||||||
|
|
||||||
channel = 'level'
|
channel = 'level'
|
||||||
|
|
||||||
def update_value_status(self, value, packed_status):
|
def update_value_status(self, value, packed_status):
|
||||||
|
pass
|
||||||
# must be a no-op
|
# must be a no-op
|
||||||
# when called from Main.read_data, value is always None
|
# when called from Main.read_data, value is always None
|
||||||
# value and status is polled via settings
|
# value and status is polled via settings
|
||||||
pass
|
|
||||||
|
|
||||||
def analyze_level(self, level, status):
|
def analyze_level(self, level, status):
|
||||||
# ignore 'old reading' state of the flag, as this happens only for a short time
|
# ignore 'old reading' state of the flag, as this happens only for a short time
|
||||||
@ -318,7 +293,7 @@ class Level(PpmsMixin, Readable):
|
|||||||
return dict(value=level, status=(self.Status.IDLE, ''))
|
return dict(value=level, status=(self.Status.IDLE, ''))
|
||||||
|
|
||||||
|
|
||||||
class Chamber(PpmsMixin, Drivable):
|
class Chamber(PpmsBase, Drivable):
|
||||||
"""sample chamber handling
|
"""sample chamber handling
|
||||||
|
|
||||||
value is an Enum, which is redundant with the status text
|
value is an Enum, which is redundant with the status text
|
||||||
@ -351,14 +326,13 @@ class Chamber(PpmsMixin, Drivable):
|
|||||||
venting_continuously=9,
|
venting_continuously=9,
|
||||||
general_failure=15,
|
general_failure=15,
|
||||||
)
|
)
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter(description='chamber state', handler=chamber,
|
||||||
Override(description='chamber state', handler=chamber,
|
datatype=EnumType(StatusCode))
|
||||||
datatype=EnumType(StatusCode)),
|
target = Parameter(description='chamber command', handler=chamber,
|
||||||
'target':
|
datatype=EnumType(Operation))
|
||||||
Override(description='chamber command', handler=chamber,
|
# pollinterval = Parameter(visibility=3)
|
||||||
datatype=EnumType(Operation)),
|
|
||||||
}
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'),
|
||||||
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'),
|
||||||
@ -387,44 +361,40 @@ class Chamber(PpmsMixin, Drivable):
|
|||||||
return dict(target=target)
|
return dict(target=target)
|
||||||
|
|
||||||
def change_chamber(self, change):
|
def change_chamber(self, change):
|
||||||
|
# write settings, combining <pname>=<value> and current attributes
|
||||||
|
# and request updated settings
|
||||||
if change.target == self.Operation.noop:
|
if change.target == self.Operation.noop:
|
||||||
return None
|
return None
|
||||||
return (change.target,)
|
return (change.target,)
|
||||||
|
|
||||||
|
|
||||||
class Temp(PpmsMixin, Drivable):
|
class Temp(PpmsBase, Drivable):
|
||||||
"""temperature"""
|
"""temperature"""
|
||||||
|
|
||||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
||||||
Status = Enum(Drivable.Status,
|
Status = Enum(
|
||||||
RAMPING = 370,
|
Drivable.Status,
|
||||||
STABILIZING = 380,
|
RAMPING=370,
|
||||||
|
STABILIZING=380,
|
||||||
)
|
)
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1)
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter(datatype=FloatRange(unit='K'), poll=True)
|
||||||
Override(datatype=FloatRange(unit='K'), poll=True),
|
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||||
'status':
|
target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False)
|
||||||
Override(datatype=StatusType(Status), poll=True),
|
setpoint = Parameter('intermediate set point',
|
||||||
'target':
|
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp)
|
||||||
Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False),
|
ramp = Parameter('ramping speed', readonly=False, default=0,
|
||||||
'setpoint':
|
datatype=FloatRange(0, 20, unit='K/min'))
|
||||||
Parameter('intermediate set point',
|
workingramp = Parameter('intermediate ramp value',
|
||||||
datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp),
|
datatype=FloatRange(0, 20, unit='K/min'), handler=temp)
|
||||||
'ramp':
|
approachmode = Parameter('how to approach target!', readonly=False, handler=temp,
|
||||||
Parameter('ramping speed', readonly=False, default=0,
|
datatype=EnumType(ApproachMode))
|
||||||
datatype=FloatRange(0, 20, unit='K/min')),
|
# pollinterval = Parameter(visibility=3)
|
||||||
'workingramp':
|
timeout = Parameter('drive timeout, in addition to ramp time', readonly=False,
|
||||||
Parameter('intermediate ramp value',
|
datatype=FloatRange(0, unit='sec'), default=3600)
|
||||||
datatype=FloatRange(0, 20, unit='K/min'), handler=temp),
|
|
||||||
'approachmode':
|
|
||||||
Parameter('how to approach target!', readonly=False, handler=temp,
|
|
||||||
datatype=EnumType(ApproachMode)),
|
|
||||||
'timeout':
|
|
||||||
Parameter('drive timeout, in addition to ramp time', readonly=False,
|
|
||||||
datatype=FloatRange(0, unit='sec'), default=3600),
|
|
||||||
}
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
TempStatus = Enum(
|
TempStatus = Enum(
|
||||||
'TempStatus',
|
'TempStatus',
|
||||||
@ -449,17 +419,14 @@ class Temp(PpmsMixin, Drivable):
|
|||||||
14: (Status.ERROR, 'can not complete'),
|
14: (Status.ERROR, 'can not complete'),
|
||||||
15: (Status.ERROR, 'general failure'),
|
15: (Status.ERROR, 'general failure'),
|
||||||
}
|
}
|
||||||
properties = {
|
general_stop = Property('respect general stop', datatype=BoolType(),
|
||||||
'general_stop': Property('respect general stop', datatype=BoolType(),
|
default=True, value=False)
|
||||||
export=True, default=True)
|
|
||||||
}
|
|
||||||
|
|
||||||
channel = 'temp'
|
channel = 'temp'
|
||||||
_stopped = False
|
_stopped = False
|
||||||
_expected_target_time = 0
|
_expected_target_time = 0
|
||||||
_last_change = 0 # 0 means no target change is pending
|
_last_change = 0 # 0 means no target change is pending
|
||||||
_last_target = None # last reached target
|
_last_target = None # last reached target
|
||||||
general_stop = False
|
|
||||||
_cool_deadline = 0
|
_cool_deadline = 0
|
||||||
_wait_at10 = False
|
_wait_at10 = False
|
||||||
_ramp_at_limit = False
|
_ramp_at_limit = False
|
||||||
@ -573,7 +540,7 @@ class Temp(PpmsMixin, Drivable):
|
|||||||
def calc_expected(self, target, ramp):
|
def calc_expected(self, target, ramp):
|
||||||
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
if self.status[0] != self.Status.STABILIZING:
|
if self.status[0] != self.Status.STABILIZING:
|
||||||
@ -586,37 +553,31 @@ class Temp(PpmsMixin, Drivable):
|
|||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
class Field(PpmsMixin, Drivable):
|
class Field(PpmsBase, Drivable):
|
||||||
"""magnetic field"""
|
"""magnetic field"""
|
||||||
|
|
||||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
||||||
Status = Enum(Drivable.Status,
|
Status = Enum(
|
||||||
PREPARED = 150,
|
Drivable.Status,
|
||||||
PREPARING = 340,
|
PREPARED=150,
|
||||||
RAMPING = 370,
|
PREPARING=340,
|
||||||
FINALIZING = 390,
|
RAMPING=370,
|
||||||
|
FINALIZING=390,
|
||||||
)
|
)
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
PersistentMode = Enum('PersistentMode', persistent=0, driven=1)
|
||||||
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2)
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='T'), poll=True)
|
||||||
'value':
|
status = Parameter(datatype=StatusType(Status), poll=True)
|
||||||
Override(datatype=FloatRange(unit='T'), poll=True),
|
target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field)
|
||||||
'status':
|
ramp = Parameter('ramping speed', readonly=False, handler=field,
|
||||||
Override(datatype=StatusType(Status), poll=True),
|
datatype=FloatRange(0.064, 1.19, unit='T/min'))
|
||||||
'target':
|
approachmode = Parameter('how to approach target', readonly=False, handler=field,
|
||||||
Override(datatype=FloatRange(-15, 15, unit='T'), handler=field),
|
datatype=EnumType(ApproachMode))
|
||||||
'ramp':
|
persistentmode = Parameter('what to do after changing field', readonly=False, handler=field,
|
||||||
Parameter('ramping speed', readonly=False, handler=field,
|
datatype=EnumType(PersistentMode))
|
||||||
datatype=FloatRange(0.064, 1.19, unit='T/min')),
|
# pollinterval = Parameter(visibility=3)
|
||||||
'approachmode':
|
|
||||||
Parameter('how to approach target', readonly=False, handler=field,
|
|
||||||
datatype=EnumType(ApproachMode)),
|
|
||||||
'persistentmode':
|
|
||||||
Parameter('what to do after changing field', readonly=False, handler=field,
|
|
||||||
datatype=EnumType(PersistentMode)),
|
|
||||||
}
|
|
||||||
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'persistent mode'),
|
1: (Status.IDLE, 'persistent mode'),
|
||||||
@ -652,7 +613,7 @@ class Field(PpmsMixin, Drivable):
|
|||||||
else:
|
else:
|
||||||
status = (self.Status.WARN, 'timeout when ramping leads')
|
status = (self.Status.WARN, 'timeout when ramping leads')
|
||||||
elif now > self._last_change + 5:
|
elif now > self._last_change + 5:
|
||||||
self._last_change = 0 # give up waiting for driving
|
self._last_change = 0 # give up waiting for driving
|
||||||
elif self.isDriving(status) and status != self._status_before_change:
|
elif self.isDriving(status) and status != self._status_before_change:
|
||||||
self._last_change = 0
|
self._last_change = 0
|
||||||
self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
|
self.log.debug('time needed to change to busy: %.3g', now - self._last_change)
|
||||||
@ -718,7 +679,7 @@ class Field(PpmsMixin, Drivable):
|
|||||||
return Done
|
return Done
|
||||||
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
return None # do not execute FIELD command, as this would trigger a ramp up of leads current
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
@ -729,23 +690,20 @@ class Field(PpmsMixin, Drivable):
|
|||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
|
|
||||||
class Position(PpmsMixin, Drivable):
|
class Position(PpmsBase, Drivable):
|
||||||
"""rotator position"""
|
"""rotator position"""
|
||||||
|
|
||||||
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
move = IOHandler('move', 'MOVE?', '%g,%g,%g')
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
parameters = {
|
|
||||||
'value':
|
value = Parameter(datatype=FloatRange(unit='deg'), poll=True)
|
||||||
Override(datatype=FloatRange(unit='deg'), poll=True),
|
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move)
|
||||||
'target':
|
enabled = Parameter('is this channel used?', readonly=False, poll=False,
|
||||||
Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move),
|
datatype=BoolType(), default=True)
|
||||||
'enabled':
|
speed = Parameter('motor speed', readonly=False, handler=move,
|
||||||
Parameter('is this channel used?', readonly=False, poll=False,
|
datatype=FloatRange(0.8, 12, unit='deg/sec'))
|
||||||
datatype=BoolType(), default=True),
|
# pollinterval = Parameter(visibility=3)
|
||||||
'speed':
|
|
||||||
Parameter('motor speed', readonly=False, handler=move,
|
|
||||||
datatype=FloatRange(0.8, 12, unit='deg/sec')),
|
|
||||||
}
|
|
||||||
STATUS_MAP = {
|
STATUS_MAP = {
|
||||||
1: (Status.IDLE, 'at target'),
|
1: (Status.IDLE, 'at target'),
|
||||||
5: (Status.BUSY, 'moving'),
|
5: (Status.BUSY, 'moving'),
|
||||||
@ -824,7 +782,7 @@ class Position(PpmsMixin, Drivable):
|
|||||||
self.speed = value
|
self.speed = value
|
||||||
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
return None # do not execute MOVE command, as this would trigger an unnecessary move
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
if not self.isDriving():
|
if not self.isDriving():
|
||||||
return
|
return
|
||||||
newtarget = clamp(self._last_target, self.value, self.target)
|
newtarget = clamp(self._last_target, self.value, self.target)
|
||||||
|
@ -20,9 +20,9 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""PPMS mf proxy"""
|
"""PPMS mf proxy"""
|
||||||
|
|
||||||
from secop.core import Enum, FloatRange, EnumType, Override, Parameter, Drivable
|
|
||||||
from secop.datatypes import StatusType
|
|
||||||
import secop_psi.ppms
|
import secop_psi.ppms
|
||||||
|
from secop.core import Drivable, Enum, EnumType, FloatRange, Override, Parameter
|
||||||
|
from secop.datatypes import StatusType
|
||||||
from secop.proxy import proxy_class
|
from secop.proxy import proxy_class
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,9 +18,10 @@
|
|||||||
# Module authors:
|
# Module authors:
|
||||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
import time
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
def num(string):
|
def num(string):
|
||||||
return json.loads(string)
|
return json.loads(string)
|
||||||
|
@ -32,21 +32,21 @@ t1:raw tt t1/raw /tt/t1/raw tt t1 raw /tt/t1
|
|||||||
rx:bla rx bla /some/rx_a/bla rx bla /some/rx_a
|
rx:bla rx bla /some/rx_a/bla rx bla /some/rx_a
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import json
|
from os.path import expanduser, join
|
||||||
from os.path import join, expanduser
|
|
||||||
|
|
||||||
from secop.modules import Module, Parameter, Command, Override, Drivable, Readable, Writable, Property, Attached
|
|
||||||
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, IntRange, EnumType
|
|
||||||
from secop.lib import mkthread, getGeneralConfig
|
|
||||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
|
||||||
from secop.metaclass import ModuleMeta, Done
|
|
||||||
from secop.errors import HardwareError, secop_error, ConfigError
|
|
||||||
from secop.client import ProxyClient
|
from secop.client import ProxyClient
|
||||||
|
from secop.datatypes import ArrayOf, BoolType, \
|
||||||
|
EnumType, FloatRange, IntRange, StringType
|
||||||
|
from secop.errors import ConfigError, HardwareError, secop_error
|
||||||
|
from secop.lib import getGeneralConfig, mkthread
|
||||||
|
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||||
|
from secop.modules import Attached, Command, Done, Drivable, \
|
||||||
|
Module, Parameter, Property, Readable, Writable
|
||||||
from secop.protocol.dispatcher import make_update
|
from secop.protocol.dispatcher import make_update
|
||||||
|
|
||||||
|
|
||||||
CFG_HEADER = """[NODE]
|
CFG_HEADER = """[NODE]
|
||||||
id = %(samenv)s.psi.ch
|
id = %(samenv)s.psi.ch
|
||||||
description = %(samenv)s over SEA
|
description = %(samenv)s over SEA
|
||||||
@ -76,7 +76,7 @@ def get_sea_port(instance):
|
|||||||
for line in f:
|
for line in f:
|
||||||
linesplit = line.split()
|
linesplit = line.split()
|
||||||
if len(linesplit) == 3:
|
if len(linesplit) == 3:
|
||||||
cmd, var, value = line.split()
|
_, var, value = line.split()
|
||||||
if var == 'serverport':
|
if var == 'serverport':
|
||||||
return value
|
return value
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
@ -87,23 +87,10 @@ def get_sea_port(instance):
|
|||||||
class SeaClient(ProxyClient, Module):
|
class SeaClient(ProxyClient, Module):
|
||||||
"""connection to SEA"""
|
"""connection to SEA"""
|
||||||
|
|
||||||
properties = {
|
json_path = Property('path to SEA json descriptors', StringType())
|
||||||
'json_path': Property('path to SEA json descriptors',
|
|
||||||
datatype=StringType(),
|
uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000')
|
||||||
default=join(expanduser('~'), 'sea/tcl/json'))
|
timeout = Parameter('timeout', datatype=FloatRange(0), default=10)
|
||||||
}
|
|
||||||
parameters = {
|
|
||||||
'uri':
|
|
||||||
Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000'),
|
|
||||||
'timeout':
|
|
||||||
Parameter('timeout', datatype=FloatRange(0), default=10),
|
|
||||||
}
|
|
||||||
commands = {
|
|
||||||
'communicate':
|
|
||||||
Command('send a command to SEA', argument=StringType(), result=StringType()),
|
|
||||||
'describe':
|
|
||||||
Command('save objects (and sub-objects) description', result=StringType()),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, name, log, opts, srv):
|
def __init__(self, name, log, opts, srv):
|
||||||
instance = srv.node_cfg['name'].rsplit('_', 1)[0]
|
instance = srv.node_cfg['name'].rsplit('_', 1)[0]
|
||||||
@ -198,7 +185,7 @@ class SeaClient(ProxyClient, Module):
|
|||||||
if msg.startswith('_E '):
|
if msg.startswith('_E '):
|
||||||
try:
|
try:
|
||||||
_, path, readerror = msg.split(None, 2)
|
_, path, readerror = msg.split(None, 2)
|
||||||
except Exception as e:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@ -241,11 +228,15 @@ class SeaClient(ProxyClient, Module):
|
|||||||
# do not update unchanged values within 0.1 sec
|
# do not update unchanged values within 0.1 sec
|
||||||
self.updateValue(module, param, value, now, readerror)
|
self.updateValue(module, param, value, now, readerror)
|
||||||
|
|
||||||
def do_communicate(self, command):
|
@Command
|
||||||
|
def communicate(self, command):
|
||||||
|
"""send a command to SEA"""
|
||||||
reply = self.request(command)
|
reply = self.request(command)
|
||||||
return reply
|
return reply
|
||||||
|
|
||||||
def do_describe(self):
|
@Command(result=StringType())
|
||||||
|
def describe(self):
|
||||||
|
"""save objects (and sub-objects) description"""
|
||||||
reply = self.request('describe_all')
|
reply = self.request('describe_all')
|
||||||
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
|
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
|
||||||
samenv, reply = json.loads(reply)
|
samenv, reply = json.loads(reply)
|
||||||
@ -288,9 +279,7 @@ def get_datatype(paramdesc):
|
|||||||
|
|
||||||
|
|
||||||
class SeaModule(Module):
|
class SeaModule(Module):
|
||||||
properties = {
|
iodev = Attached()
|
||||||
'iodev': Attached(),
|
|
||||||
}
|
|
||||||
|
|
||||||
# pollerClass=None
|
# pollerClass=None
|
||||||
path2param = None
|
path2param = None
|
||||||
@ -329,8 +318,7 @@ class SeaModule(Module):
|
|||||||
else: # take all
|
else: # take all
|
||||||
main = ''
|
main = ''
|
||||||
path2param = {}
|
path2param = {}
|
||||||
parameters = {}
|
attributes = dict(sea_object=sea_object, path2param=path2param)
|
||||||
attributes = dict(sea_object=sea_object, path2param=path2param, parameters=parameters)
|
|
||||||
for paramdesc in descr:
|
for paramdesc in descr:
|
||||||
path = paramdesc['path']
|
path = paramdesc['path']
|
||||||
readonly = paramdesc.get('readonly', True)
|
readonly = paramdesc.get('readonly', True)
|
||||||
@ -351,6 +339,7 @@ class SeaModule(Module):
|
|||||||
else:
|
else:
|
||||||
kwds['group'] = pathlist[-2]
|
kwds['group'] = pathlist[-2]
|
||||||
# flatten path to parameter name
|
# flatten path to parameter name
|
||||||
|
key = None
|
||||||
for i in reversed(range(len(pathlist))):
|
for i in reversed(range(len(pathlist))):
|
||||||
key = '_'.join(pathlist[i:])
|
key = '_'.join(pathlist[i:])
|
||||||
if not key in cls.accessibles:
|
if not key in cls.accessibles:
|
||||||
@ -361,12 +350,12 @@ class SeaModule(Module):
|
|||||||
if key in cls.accessibles:
|
if key in cls.accessibles:
|
||||||
if key == 'target':
|
if key == 'target':
|
||||||
kwds['readonly'] = False
|
kwds['readonly'] = False
|
||||||
pobj = Override(**kwds)
|
pobj = cls.accessibles[key].override(**kwds)
|
||||||
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
|
datatype = kwds.get('datatype', cls.accessibles[key].datatype)
|
||||||
else:
|
else:
|
||||||
pobj = Parameter(**kwds)
|
pobj = Parameter(**kwds)
|
||||||
datatype = pobj.datatype
|
datatype = pobj.datatype
|
||||||
parameters[key] = pobj
|
attributes[key] = pobj
|
||||||
if not hasattr(cls, 'read_' + key):
|
if not hasattr(cls, 'read_' + key):
|
||||||
def rfunc(self, cmd='hval /sics/%s/%s' % (sea_object, path)):
|
def rfunc(self, cmd='hval /sics/%s/%s' % (sea_object, path)):
|
||||||
print('READ', cmd)
|
print('READ', cmd)
|
||||||
@ -395,19 +384,20 @@ class SeaModule(Module):
|
|||||||
return Done
|
return Done
|
||||||
|
|
||||||
attributes['write_' + key] = wfunc
|
attributes['write_' + key] = wfunc
|
||||||
|
|
||||||
# create standard parameters like value and status, if not yet there
|
# create standard parameters like value and status, if not yet there
|
||||||
for pname, pobj in cls.accessibles.items():
|
for pname, pobj in cls.accessibles.items():
|
||||||
if pname == 'pollinterval':
|
if pname == 'pollinterval':
|
||||||
parameters[pname] = Override(export=False)
|
attributes[pname] = pobj.override(export=False)
|
||||||
elif pname not in parameters and isinstance(pobj, Parameter):
|
elif pname not in attributes and isinstance(pobj, Parameter):
|
||||||
parameters[pname] = Override(poll=False, needscfg=False)
|
attributes[pname] = pobj.override(poll=False, needscfg=False)
|
||||||
|
|
||||||
classname = '%s_%s' % (cls.__name__, sea_object)
|
classname = '%s_%s' % (cls.__name__, sea_object)
|
||||||
newcls = ModuleMeta.__new__(ModuleMeta, classname, (cls,), attributes)
|
newcls = type(classname, (cls,), attributes)
|
||||||
return Module.__new__(newcls)
|
return Module.__new__(newcls)
|
||||||
|
|
||||||
def __init__(self, name, logger, cfgdict, dispatcher):
|
# def __init__(self, name, logger, cfgdict, dispatcher):
|
||||||
Module.__init__(self, name, logger, cfgdict, dispatcher)
|
# Module.__init__(self, name, logger, cfgdict, dispatcher)
|
||||||
|
|
||||||
def updateEvent(self, module, parameter, value, timestamp, readerror):
|
def updateEvent(self, module, parameter, value, timestamp, readerror):
|
||||||
upd = getattr(self, 'update_' + parameter, None)
|
upd = getattr(self, 'update_' + parameter, None)
|
||||||
@ -442,9 +432,9 @@ class SeaReadable(SeaModule, Readable):
|
|||||||
if readerror:
|
if readerror:
|
||||||
value = repr(readerror)
|
value = repr(readerror)
|
||||||
if value == '':
|
if value == '':
|
||||||
self.status = [self.Status.IDLE, '']
|
self.status = (self.Status.IDLE, '')
|
||||||
else:
|
else:
|
||||||
self.status = [self.Status.ERROR, value]
|
self.status = (self.Status.ERROR, value)
|
||||||
|
|
||||||
def read_status(self):
|
def read_status(self):
|
||||||
return self.status
|
return self.status
|
||||||
@ -485,11 +475,11 @@ class SeaDrivable(SeaModule, Drivable):
|
|||||||
|
|
||||||
def updateStatus(self):
|
def updateStatus(self):
|
||||||
if self._sea_status:
|
if self._sea_status:
|
||||||
self.status = [self.Status.ERROR, self._sea_status]
|
self.status = (self.Status.ERROR, self._sea_status)
|
||||||
elif self._is_running:
|
elif self._is_running:
|
||||||
self.status = [self.Status.BUSY, 'driving']
|
self.status = (self.Status.BUSY, 'driving')
|
||||||
else:
|
else:
|
||||||
self.status = [self.Status.IDLE, '']
|
self.status = (self.Status.IDLE, '')
|
||||||
|
|
||||||
def updateTarget(self, module, parameter, value, timestamp, readerror):
|
def updateTarget(self, module, parameter, value, timestamp, readerror):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
@ -21,33 +21,33 @@
|
|||||||
"""senis hall sensor"""
|
"""senis hall sensor"""
|
||||||
|
|
||||||
|
|
||||||
import time
|
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from serial import Serial
|
from serial import Serial
|
||||||
from secop.core import Property, Parameter, Override, Readable, BoolType, \
|
|
||||||
FloatRange, TupleOf, StringType, IntRange, Attached
|
from secop.core import Attached, BoolType, FloatRange, IntRange, \
|
||||||
|
Parameter, Property, Readable, StringType, TupleOf
|
||||||
|
|
||||||
|
|
||||||
class Temperature(Readable):
|
class Temperature(Readable):
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=FloatRange(unit='degC')),
|
value = Parameter(datatype=FloatRange(unit='degC'))
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Bcomp(Readable):
|
class Bcomp(Readable):
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=FloatRange(unit='T')),
|
value = Parameter(datatype=FloatRange(unit='T'))
|
||||||
'range': Parameter('working range', FloatRange(unit='T'), default=0),
|
range = Parameter('working range', FloatRange(unit='T'), default=0)
|
||||||
}
|
|
||||||
|
|
||||||
class Raw(Readable):
|
class Raw(Readable):
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
parameters = {
|
|
||||||
'value': Override(datatype=FloatRange()),
|
value = Parameter(datatype=FloatRange())
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TeslameterBase(Readable):
|
class TeslameterBase(Readable):
|
||||||
@ -58,18 +58,15 @@ class TeslameterBase(Readable):
|
|||||||
|
|
||||||
the B components (and temperatures for 3MH6) are implemented as separate modules
|
the B components (and temperatures for 3MH6) are implemented as separate modules
|
||||||
"""
|
"""
|
||||||
properties = {
|
x = Attached()
|
||||||
'x': Attached(),
|
y = Attached()
|
||||||
'y': Attached(),
|
z = Attached()
|
||||||
'z': Attached(),
|
|
||||||
}
|
value = Parameter('B vector', poll=True,
|
||||||
parameters = {
|
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T')))
|
||||||
'value': Override('B vector', poll=True,
|
usb = Parameter('usb device', StringType(), readonly=False)
|
||||||
datatype=TupleOf(FloatRange(unit='T'), FloatRange(unit='T'), FloatRange(unit='T'))),
|
enabled = Parameter('enable data acq', datatype=BoolType(), readonly=False, default=True)
|
||||||
'usb': Parameter('usb device', StringType(), readonly=False),
|
nsample = Parameter('number of samples for average', datatype=IntRange(1, 1000), readonly=False, default=1)
|
||||||
'enabled': Parameter('enable data acq', datatype=BoolType(), readonly=False, default=True),
|
|
||||||
'nsample': Parameter('number of samples for average', datatype=IntRange(1, 1000), readonly=False, default=1),
|
|
||||||
}
|
|
||||||
|
|
||||||
def init_serial(self, baud):
|
def init_serial(self, baud):
|
||||||
self._conn = Serial(self.usb, baud, timeout=0.1)
|
self._conn = Serial(self.usb, baud, timeout=0.1)
|
||||||
@ -103,9 +100,7 @@ class Teslameter3MH3(TeslameterBase):
|
|||||||
remark: no query for the sample rate is possible, therefore set always to
|
remark: no query for the sample rate is possible, therefore set always to
|
||||||
a default rate (therefore initwrite=True on the rate parameter)
|
a default rate (therefore initwrite=True on the rate parameter)
|
||||||
"""
|
"""
|
||||||
properties = {
|
range = Property('full scale', datatype=FloatRange(), default=2)
|
||||||
'range': Property('full scale', datatype=FloatRange(), default=2),
|
|
||||||
}
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
self.init_serial(115200)
|
self.init_serial(115200)
|
||||||
@ -122,7 +117,7 @@ class Teslameter3MH3(TeslameterBase):
|
|||||||
s.timeout = 0.1 + 0.02 * self.nsample
|
s.timeout = 0.1 + 0.02 * self.nsample
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
self.write_bytes(b'B')
|
self.write_bytes(b'B')
|
||||||
t = time.time()
|
# t = time.time()
|
||||||
reply = self.read_bytes(8 * self.nsample)
|
reply = self.read_bytes(8 * self.nsample)
|
||||||
s.timeout = 0.1
|
s.timeout = 0.1
|
||||||
self.stop_reading()
|
self.stop_reading()
|
||||||
@ -147,21 +142,19 @@ class Teslameter3MH3(TeslameterBase):
|
|||||||
|
|
||||||
class Teslameter3MH6(TeslameterBase):
|
class Teslameter3MH6(TeslameterBase):
|
||||||
"""luxury model with probe and box temperature and autorange"""
|
"""luxury model with probe and box temperature and autorange"""
|
||||||
properties = {
|
x_direct = Attached()
|
||||||
'x_direct': Attached(),
|
y_direct = Attached()
|
||||||
'y_direct': Attached(),
|
z_direct = Attached()
|
||||||
'z_direct': Attached(),
|
probe_temp = Attached()
|
||||||
'probe_temp': Attached(),
|
box_temp = Attached()
|
||||||
'box_temp': Attached(),
|
probe_temp_direct = Attached()
|
||||||
'probe_temp_direct': Attached(),
|
box_temp_direct = Attached()
|
||||||
'box_temp_direct': Attached(),
|
|
||||||
}
|
range = Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0)
|
||||||
parameters = {
|
rate = Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
|
||||||
'range': Parameter('range or 0 for autorange', FloatRange(0, 20, unit='T'), readonly=False, default=0),
|
readonly=False, poll=True)
|
||||||
'rate': Parameter('sampling rate', datatype=FloatRange(10, 15000, unit='Hz'),
|
avtime = Parameter('data acquisition time', FloatRange(), default=0)
|
||||||
readonly=False, poll=True),
|
|
||||||
'avtime': Parameter('data acquisition time', FloatRange(), default=0),
|
|
||||||
}
|
|
||||||
SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000,
|
SAMPLING_RATES = {0xe0: 15000, 0xd0: 7500, 0xc0: 3750, 0xb0: 2000, 0xa1: 1000,
|
||||||
0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10}
|
0x92: 500, 0x82: 100, 0x72: 60, 0x63: 50, 0x53: 30, 0x23: 10}
|
||||||
RANGES = dict(zip(b'1234', [0.1, 0.5, 2, 20]))
|
RANGES = dict(zip(b'1234', [0.1, 0.5, 2, 20]))
|
||||||
@ -183,7 +176,7 @@ class Teslameter3MH6(TeslameterBase):
|
|||||||
chk = np.frombuffer(reply, dtype='i1,23i1,i1')
|
chk = np.frombuffer(reply, dtype='i1,23i1,i1')
|
||||||
if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0):
|
if not np.all(np.sum(chk['f1'], axis=1) % 256 == 0):
|
||||||
status = 'checksum error'
|
status = 'checksum error'
|
||||||
continue
|
continue
|
||||||
# first byte must be 'B' and last byte must be CR
|
# first byte must be 'B' and last byte must be CR
|
||||||
if np.all(chk['f0'] == ord(b'B')) and np.all(chk['f2'] == 13):
|
if np.all(chk['f0'] == ord(b'B')) and np.all(chk['f2'] == 13):
|
||||||
break
|
break
|
||||||
@ -219,7 +212,7 @@ class Teslameter3MH6(TeslameterBase):
|
|||||||
self._z.value = mean['z'] * 0.001
|
self._z.value = mean['z'] * 0.001
|
||||||
self._probe_temp.value = mean['thc']
|
self._probe_temp.value = mean['thc']
|
||||||
self._box_temp.value = mean['tec']
|
self._box_temp.value = mean['tec']
|
||||||
|
|
||||||
self.write_bytes(b'D') # put into NONcalibrated mode
|
self.write_bytes(b'D') # put into NONcalibrated mode
|
||||||
if self.read_bytes(1) != b'd':
|
if self.read_bytes(1) != b'd':
|
||||||
self.log.error('missing response to D command')
|
self.log.error('missing response to D command')
|
||||||
|
@ -20,13 +20,14 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Software calibration"""
|
"""Software calibration"""
|
||||||
|
|
||||||
import os
|
|
||||||
from os.path import join, exists, basename
|
|
||||||
import math
|
import math
|
||||||
import numpy as np
|
import os
|
||||||
from scipy.interpolate import splrep, splev # pylint: disable=import-error
|
from os.path import basename, exists, join
|
||||||
|
|
||||||
from secop.core import Readable, Parameter, Override, Attached, StringType, BoolType
|
import numpy as np
|
||||||
|
from scipy.interpolate import splev, splrep # pylint: disable=import-error
|
||||||
|
|
||||||
|
from secop.core import Attached, BoolType, Parameter, Readable, StringType
|
||||||
|
|
||||||
|
|
||||||
def linear(x):
|
def linear(x):
|
||||||
@ -102,6 +103,7 @@ class CalCurve:
|
|||||||
sensopt = calibspec.split(',')
|
sensopt = calibspec.split(',')
|
||||||
calibname = sensopt.pop(0)
|
calibname = sensopt.pop(0)
|
||||||
_, dot, ext = basename(calibname).rpartition('.')
|
_, dot, ext = basename(calibname).rpartition('.')
|
||||||
|
kind = None
|
||||||
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','):
|
||||||
# first try without adding kind
|
# first try without adding kind
|
||||||
filename = join(path.strip(), calibname)
|
filename = join(path.strip(), calibname)
|
||||||
@ -109,8 +111,8 @@ class CalCurve:
|
|||||||
kind = ext if dot else None
|
kind = ext if dot else None
|
||||||
break
|
break
|
||||||
# then try adding all kinds as extension
|
# then try adding all kinds as extension
|
||||||
for kind in KINDS:
|
for nam in calibname, calibname.upper(), calibname.lower():
|
||||||
for nam in {calibname, calibname.upper(), calibname.lower()}:
|
for kind in KINDS:
|
||||||
filename = join(path.strip(), '%s.%s' % (nam, kind))
|
filename = join(path.strip(), '%s.%s' % (nam, kind))
|
||||||
if exists(filename):
|
if exists(filename):
|
||||||
break
|
break
|
||||||
@ -150,16 +152,14 @@ class CalCurve:
|
|||||||
|
|
||||||
|
|
||||||
class Sensor(Readable):
|
class Sensor(Readable):
|
||||||
properties = {
|
rawsensor = Attached()
|
||||||
'rawsensor': Attached(),
|
|
||||||
}
|
calib = Parameter('calibration name', datatype=StringType(), readonly=False)
|
||||||
parameters = {
|
abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True)
|
||||||
'calib': Parameter('calibration name', datatype=StringType(), readonly=False),
|
value = Parameter(unit='K')
|
||||||
'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True),
|
pollinterval = Parameter(export=False)
|
||||||
'value': Override(unit='K'),
|
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||||
'pollinterval': Override(export=False),
|
|
||||||
'status': Override(default=(Readable.Status.ERROR, 'unintialized'))
|
|
||||||
}
|
|
||||||
pollerClass = None
|
pollerClass = None
|
||||||
description = 'a calibrated sensor value'
|
description = 'a calibrated sensor value'
|
||||||
_value_error = None
|
_value_error = None
|
||||||
@ -179,7 +179,7 @@ class Sensor(Readable):
|
|||||||
self._value_error = None
|
self._value_error = None
|
||||||
|
|
||||||
def error_update_value(self, err):
|
def error_update_value(self, err):
|
||||||
if self.abs and str(err) == 'R_UNDER':
|
if self.abs and str(err) == 'R_UNDER': # hack: ignore R_UNDER from ls370
|
||||||
self._value_error = None
|
self._value_error = None
|
||||||
return None
|
return None
|
||||||
self._value_error = repr(err)
|
self._value_error = repr(err)
|
||||||
|
@ -20,40 +20,38 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Test command arguments"""
|
"""Test command arguments"""
|
||||||
|
|
||||||
from secop.core import Module, Parameter, Command, FloatRange, StringType, BoolType, TupleOf, StructOf, ArrayOf
|
from secop.core import ArrayOf, BoolType, Command, FloatRange, \
|
||||||
|
Module, Parameter, StringType, StructOf, TupleOf
|
||||||
|
|
||||||
|
|
||||||
class TestCmd(Module):
|
class TestCmd(Module):
|
||||||
|
|
||||||
commands = {
|
struct = Parameter('struct', StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
|
||||||
'arg':
|
readonly=False, default=dict(a='', c=True))
|
||||||
Command('5 args',
|
array = Parameter('array', ArrayOf(BoolType()),
|
||||||
argument=TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())),
|
readonly=False, default=[])
|
||||||
result=StringType()),
|
tuple = Parameter('tuple', TupleOf(StringType(), FloatRange(), BoolType(),
|
||||||
'keyed':
|
TupleOf(BoolType()), StructOf(a=StringType())),
|
||||||
Command('keyworded arg', argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']), result=StringType()),
|
readonly=False, default=('', 0, False, (False,), dict(a='')))
|
||||||
'one':
|
|
||||||
Command('1 arg', argument=FloatRange(), result=StringType()),
|
|
||||||
'none':
|
|
||||||
Command('no arg', result=StringType()),
|
|
||||||
}
|
|
||||||
parameters = {
|
|
||||||
'struct': Parameter('struct', StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
|
|
||||||
readonly=False, default=dict(a='',c=True)),
|
|
||||||
'array': Parameter('array', ArrayOf(BoolType()),
|
|
||||||
readonly=False, default=[]),
|
|
||||||
'tuple': Parameter('tuple', TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())),
|
|
||||||
readonly=False, default=('',0,False,(False,),dict(a=''))),
|
|
||||||
}
|
|
||||||
|
|
||||||
def do_arg(self, arg):
|
@Command(argument=TupleOf(StringType(), FloatRange(), BoolType(), TupleOf(BoolType()), StructOf(a=StringType())),
|
||||||
|
result=StringType())
|
||||||
|
def arg(self, *arg):
|
||||||
|
"""5 args"""
|
||||||
return repr(arg)
|
return repr(arg)
|
||||||
|
|
||||||
def do_keyed(self, arg):
|
@Command(argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
|
||||||
|
result=StringType())
|
||||||
|
def keyed(self, **arg):
|
||||||
|
"""keyworded arg"""
|
||||||
return repr(arg)
|
return repr(arg)
|
||||||
|
|
||||||
def do_one(self, arg):
|
@Command(argument=FloatRange(), result=StringType())
|
||||||
|
def one(self, arg):
|
||||||
|
"""1 arg"""
|
||||||
return repr(arg)
|
return repr(arg)
|
||||||
|
|
||||||
def do_none(self):
|
@Command(result=StringType())
|
||||||
|
def none(self):
|
||||||
|
"""no arg"""
|
||||||
return repr(None)
|
return repr(None)
|
||||||
|
@ -20,31 +20,23 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""Temp"""
|
"""Temp"""
|
||||||
|
|
||||||
from secop.modules import Readable, Drivable, Parameter, Override
|
|
||||||
from secop.datatypes import FloatRange, IntRange, StringType
|
from secop.datatypes import FloatRange, IntRange, StringType
|
||||||
|
from secop.modules import Drivable, Parameter, Readable
|
||||||
from secop.stringio import HasIodev
|
from secop.stringio import HasIodev
|
||||||
|
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
|
|
||||||
class TempLoop(HasIodev, Drivable):
|
|
||||||
'''temperature channel on Lakeshore 336'''
|
|
||||||
|
|
||||||
parameters = {
|
class TempLoop(HasIodev, Drivable):
|
||||||
'value':
|
"""temperature channel on Lakeshore 336"""
|
||||||
Override(datatype=FloatRange(unit='K'), default=0, poll=True),
|
|
||||||
'status':
|
value = Parameter(datatype=FloatRange(unit='K'), default=0, poll=True)
|
||||||
Override(poll=False),
|
status = Parameter(poll=False)
|
||||||
'target':
|
target = Parameter(datatype=FloatRange(1.0, 402.0, unit='K'), default=1.3, poll=True)
|
||||||
Override(datatype=FloatRange(1.0, 402.0, unit='K'), default=1.3, poll=True),
|
tolerance = Parameter('the tolerance', FloatRange(-400, 400), default=1, readonly=False)
|
||||||
'tolerance':
|
pollinterval = Parameter(visibility=3)
|
||||||
Parameter('the tolerance', FloatRange(-400,400), default=1, readonly=False),
|
channel = Parameter('the Lakeshore channel', datatype=StringType(), export=False)
|
||||||
'pollinterval':
|
loop = Parameter('the Lakeshore loop number', datatype=IntRange(1, 3), export=False)
|
||||||
Override(visibility=3),
|
|
||||||
'channel':
|
|
||||||
Parameter('the Lakeshore channel', datatype=StringType(), export=False),
|
|
||||||
'loop':
|
|
||||||
Parameter('the Lakeshore loop number', datatype=IntRange(1,3), export=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
super(TempLoop, self).earlyInit()
|
super(TempLoop, self).earlyInit()
|
||||||
@ -67,24 +59,18 @@ class TempLoop(HasIodev, Drivable):
|
|||||||
float('x')
|
float('x')
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def do_stop(self):
|
def stop(self):
|
||||||
self.target = self.value
|
self.target = self.value
|
||||||
self.status = [Status.IDLE, 'stopped']
|
self.status = [Status.IDLE, 'stopped']
|
||||||
|
|
||||||
|
|
||||||
class TempChannel(HasIodev, Readable):
|
class TempChannel(HasIodev, Readable):
|
||||||
'''temperature channel on Lakeshore 336'''
|
"""temperature channel on Lakeshore 336"""
|
||||||
|
|
||||||
parameters = {
|
value = Parameter(datatype=FloatRange(unit='K'), default=0, poll=True)
|
||||||
'value':
|
status = Parameter(poll=False, constant=[Status.IDLE, 'idle'])
|
||||||
Override(datatype=FloatRange(unit='K'), default=0, poll=True),
|
pollinterval = Parameter(visibility=3)
|
||||||
'status':
|
channel = Parameter('the Lakeshore channel', datatype=StringType(), export=False)
|
||||||
Override(poll=False, constant=[Status.IDLE, 'idle']),
|
|
||||||
'pollinterval':
|
|
||||||
Override(visibility=3),
|
|
||||||
'channel':
|
|
||||||
Parameter('the Lakeshore channel', datatype=StringType(), export=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def read_value(self):
|
def read_value(self):
|
||||||
result = self.sendRecv('KRDG?%s' % self.channel)
|
result = self.sendRecv('KRDG?%s' % self.channel)
|
||||||
|
@ -20,17 +20,19 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""frappy support for ultrasound"""
|
"""frappy support for ultrasound"""
|
||||||
|
|
||||||
|
import math
|
||||||
#import serial
|
#import serial
|
||||||
import os
|
import os
|
||||||
import math
|
|
||||||
import time
|
import time
|
||||||
from secop.core import Readable, Parameter, Override, FloatRange, BoolType, StringIO, \
|
|
||||||
Done, Attached, TupleOf, StringType, IntRange, EnumType, HasIodev, Module
|
|
||||||
from secop.properties import Property
|
|
||||||
from adq_mr import Adq
|
|
||||||
import iqplot
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
import iqplot
|
||||||
|
from adq_mr import Adq
|
||||||
|
from secop.core import Attached, BoolType, Done, FloatRange, HasIodev, \
|
||||||
|
IntRange, Module, Parameter, Readable, StringIO, StringType
|
||||||
|
from secop.properties import Property
|
||||||
|
|
||||||
|
|
||||||
def fname_from_time(t, extension):
|
def fname_from_time(t, extension):
|
||||||
tm = time.localtime(t)
|
tm = time.localtime(t)
|
||||||
@ -43,32 +45,27 @@ def fname_from_time(t, extension):
|
|||||||
|
|
||||||
|
|
||||||
class Roi(Readable):
|
class Roi(Readable):
|
||||||
properties = {
|
main = Attached()
|
||||||
'main': Attached(),
|
|
||||||
}
|
value = Parameter('amplitude', FloatRange(), default=0)
|
||||||
parameters = {
|
phase = Parameter('phase', FloatRange(unit='deg'), default=0)
|
||||||
'value': Override('amplitude', FloatRange(), default=0),
|
i = Parameter('in phase', FloatRange(), default=0)
|
||||||
'phase': Parameter('phase', FloatRange(unit='deg'), default=0),
|
q = Parameter('out of phase', FloatRange(), default=0)
|
||||||
'i': Parameter('in phase', FloatRange(), default=0),
|
time = Parameter('start time', FloatRange(unit='nsec'), readonly=False)
|
||||||
'q': Parameter('out of phase', FloatRange(), default=0),
|
size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False)
|
||||||
'time': Parameter('start time', FloatRange(unit='nsec'),
|
enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True)
|
||||||
readonly=False),
|
#status = Parameter(export=False)
|
||||||
'size': Parameter('interval (symmetric around time)', FloatRange(unit='nsec'),
|
pollinterval = Parameter(export=False)
|
||||||
readonly=False),
|
|
||||||
'enable': Parameter('calculate this roi', BoolType(), readonly=False, default=True),
|
|
||||||
#'status': Override(export=False),
|
|
||||||
'pollinterval': Override(export=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
interval = (0,0)
|
interval = (0,0)
|
||||||
|
|
||||||
def initModule(self):
|
def initModule(self):
|
||||||
self._main.register_roi(self)
|
self._main.register_roi(self)
|
||||||
self.calc_interval()
|
self.calc_interval()
|
||||||
|
|
||||||
def calc_interval(self):
|
def calc_interval(self):
|
||||||
self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size)
|
self.interval = (self.time - 0.5 * self.size, self.time + 0.5 * self.size)
|
||||||
|
|
||||||
def write_time(self, value):
|
def write_time(self, value):
|
||||||
self.time = value
|
self.time = value
|
||||||
self.calc_interval()
|
self.calc_interval()
|
||||||
@ -83,53 +80,49 @@ class Roi(Readable):
|
|||||||
class Pars(Module):
|
class Pars(Module):
|
||||||
description = 'relevant parameters from SEA'
|
description = 'relevant parameters from SEA'
|
||||||
|
|
||||||
parameters = {
|
timestamp = Parameter('unix timestamp', StringType(), default='0', readonly=False)
|
||||||
'timestamp': Parameter('unix timestamp', StringType(), default='0', readonly=False),
|
temperature = Parameter('T', FloatRange(unit='K'), default=0, readonly=False)
|
||||||
'temperature': Parameter('T', FloatRange(unit='K'), default=0, readonly=False),
|
mf = Parameter('field', FloatRange(unit='T'), default=0, readonly=False)
|
||||||
'mf': Parameter('field', FloatRange(unit='T'), default=0, readonly=False),
|
sr = Parameter('rotaion angle', FloatRange(unit='deg'), default=0, readonly=False)
|
||||||
'sr': Parameter('rotaion angle', FloatRange(unit='deg'), default=0, readonly=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class FreqStringIO(StringIO):
|
class FreqStringIO(StringIO):
|
||||||
end_of_line = '\r'
|
end_of_line = '\r'
|
||||||
|
|
||||||
|
|
||||||
class Frequency(HasIodev, Readable):
|
class Frequency(HasIodev, Readable):
|
||||||
properties = {
|
pars = Attached()
|
||||||
'pars': Attached(),
|
sr = Property('samples per record', datatype=IntRange(), default=16384)
|
||||||
'sr': Property('samples per record', datatype=IntRange(), default=16384),
|
maxy = Property('plot y scale', datatype=FloatRange(), default=0.5)
|
||||||
'maxy': Property('plot y scale', datatype=FloatRange(), default=0.5),
|
|
||||||
}
|
value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0)
|
||||||
parameters = {
|
basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False)
|
||||||
'value': Override('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0),
|
nr = Parameter('number of records', datatype=IntRange(1,10000), default=500)
|
||||||
'basefreq': Parameter('base frequency', FloatRange(unit='Hz'), readonly=False),
|
freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False, poll=True)
|
||||||
'nr': Parameter('number of records', datatype=IntRange(1,10000), default=500),
|
amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False, poll=True)
|
||||||
'freq': Parameter('target frequency', FloatRange(unit='Hz'), readonly=False, poll=True),
|
control = Parameter('control loop on?', BoolType(), readonly=False, default=True)
|
||||||
'amp': Parameter('amplitude', FloatRange(unit='dBm'), readonly=False, poll=True),
|
time = Parameter('pulse start time', FloatRange(unit='nsec'),
|
||||||
'control': Parameter('control loop on?', BoolType(), readonly=False, default=True),
|
readonly=False)
|
||||||
'time': Parameter('pulse start time', FloatRange(unit='nsec'),
|
size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
||||||
readonly=False),
|
readonly=False)
|
||||||
'size': Parameter('pulse length (starting from time)', FloatRange(unit='nsec'),
|
pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1)
|
||||||
readonly=False),
|
maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
||||||
'pulselen': Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1),
|
default=10000)
|
||||||
'maxstep': Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,
|
minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
||||||
default=10000),
|
readonly=False, default=4000)
|
||||||
'minstep': Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'),
|
slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
||||||
readonly=False, default=4000),
|
default=1e6)
|
||||||
'slope': Parameter('inphase/frequency slope', FloatRange(), readonly=False,
|
plot = Parameter('create plot images', BoolType(), readonly=False, default=True)
|
||||||
default=1e6),
|
save = Parameter('save data', BoolType(), readonly=False, default=True)
|
||||||
'plot': Parameter('create plot images', BoolType(), readonly=False, default=True),
|
pollinterval = Parameter(datatype=FloatRange(0,120))
|
||||||
'save': Parameter('save data', BoolType(), readonly=False, default=True),
|
|
||||||
'pollinterval': Override(datatype=FloatRange(0,120)),
|
|
||||||
}
|
|
||||||
iodevClass = FreqStringIO
|
iodevClass = FreqStringIO
|
||||||
|
|
||||||
lastfreq = None
|
lastfreq = None
|
||||||
old = None
|
old = None
|
||||||
starttime = None
|
starttime = None
|
||||||
interval = (0,0)
|
interval = (0,0)
|
||||||
|
|
||||||
def earlyInit(self):
|
def earlyInit(self):
|
||||||
#assert self.iodev.startswith('serial:')
|
#assert self.iodev.startswith('serial:')
|
||||||
#self._iodev = serial.Serial(self.iodev[7:])
|
#self._iodev = serial.Serial(self.iodev[7:])
|
||||||
@ -142,30 +135,30 @@ class Frequency(HasIodev, Readable):
|
|||||||
|
|
||||||
def calc_interval(self):
|
def calc_interval(self):
|
||||||
self.interval = (self.time, self.time + self.size)
|
self.interval = (self.time, self.time + self.size)
|
||||||
|
|
||||||
def write_time(self, value):
|
def write_time(self, value):
|
||||||
self.time = value
|
self.time = value
|
||||||
self.calc_interval()
|
self.calc_interval()
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_size(self, value):
|
def write_size(self, value):
|
||||||
self.size = value
|
self.size = value
|
||||||
self.calc_interval()
|
self.calc_interval()
|
||||||
return Done
|
return Done
|
||||||
|
|
||||||
def write_nr(self, value):
|
def write_nr(self, value):
|
||||||
# self.pollinterval = value * 0.0001
|
# self.pollinterval = value * 0.0001
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def register_roi(self, roi):
|
def register_roi(self, roi):
|
||||||
self.roilist.append(roi)
|
self.roilist.append(roi)
|
||||||
|
|
||||||
def set_freq(self):
|
def set_freq(self):
|
||||||
freq = self.freq + self.basefreq
|
freq = self.freq + self.basefreq
|
||||||
reply = self.sendRecv('FREQ %.15g;FREQ?' % freq)
|
self.sendRecv('FREQ %.15g;FREQ?' % freq)
|
||||||
#self._iodev.readline().decode('ascii')
|
#self._iodev.readline().decode('ascii')
|
||||||
return freq
|
return freq
|
||||||
|
|
||||||
def write_amp(self, amp):
|
def write_amp(self, amp):
|
||||||
reply = self.sendRecv('AMPR %g;AMPR?' % amp)
|
reply = self.sendRecv('AMPR %g;AMPR?' % amp)
|
||||||
return float(reply)
|
return float(reply)
|
||||||
@ -173,11 +166,11 @@ class Frequency(HasIodev, Readable):
|
|||||||
def read_amp(self):
|
def read_amp(self):
|
||||||
reply = self.sendRecv('AMPR?')
|
reply = self.sendRecv('AMPR?')
|
||||||
return float(reply)
|
return float(reply)
|
||||||
|
|
||||||
def write_freq(self, value):
|
def write_freq(self, value):
|
||||||
self.skipctrl = 2 # suppress control for the 2 next steps
|
self.skipctrl = 2 # suppress control for the 2 next steps
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def read_freq(self):
|
def read_freq(self):
|
||||||
"""used as main polling loop body"""
|
"""used as main polling loop body"""
|
||||||
if self.lastfreq is None:
|
if self.lastfreq is None:
|
||||||
@ -197,7 +190,7 @@ class Frequency(HasIodev, Readable):
|
|||||||
self.adq.start() # start next acq
|
self.adq.start() # start next acq
|
||||||
times.append(('start',time.time()))
|
times.append(('start',time.time()))
|
||||||
roilist = [r for r in self.roilist if r.enable]
|
roilist = [r for r in self.roilist if r.enable]
|
||||||
|
|
||||||
gates = self.adq.gates_and_curves(data, freq, self.interval,
|
gates = self.adq.gates_and_curves(data, freq, self.interval,
|
||||||
[r.interval for r in roilist])
|
[r.interval for r in roilist])
|
||||||
if self.save:
|
if self.save:
|
||||||
|
@ -24,10 +24,11 @@
|
|||||||
# no fixtures needed
|
# no fixtures needed
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.basic_validators import FloatProperty, PositiveFloatProperty, \
|
from secop.basic_validators import BoolProperty, EnumProperty, FloatProperty, \
|
||||||
NonNegativeFloatProperty, IntProperty, PositiveIntProperty, \
|
FmtStrProperty, IntProperty, NoneOr, NonNegativeFloatProperty, \
|
||||||
NonNegativeIntProperty, BoolProperty, StringProperty, UnitProperty, \
|
NonNegativeIntProperty, OneOfProperty, PositiveFloatProperty, \
|
||||||
FmtStrProperty, OneOfProperty, NoneOr, EnumProperty, TupleProperty
|
PositiveIntProperty, StringProperty, TupleProperty, UnitProperty
|
||||||
|
|
||||||
|
|
||||||
class unprintable:
|
class unprintable:
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -26,8 +26,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||||
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
|
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
|
||||||
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
IntRange, ProgrammingError, ScaledInteger, StatusType, \
|
||||||
|
StringType, StructOf, TextType, TupleOf, get_datatype
|
||||||
|
|
||||||
|
|
||||||
def copytest(dt):
|
def copytest(dt):
|
||||||
@ -359,6 +360,7 @@ def test_BoolType():
|
|||||||
# pylint: disable=unexpected-keyword-arg
|
# pylint: disable=unexpected-keyword-arg
|
||||||
BoolType(unit='K')
|
BoolType(unit='K')
|
||||||
|
|
||||||
|
|
||||||
def test_ArrayOf():
|
def test_ArrayOf():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -478,6 +480,14 @@ def test_Command():
|
|||||||
'result':{'type': 'int', 'min':-3, 'max':3}}
|
'result':{'type': 'int', 'min':-3, 'max':3}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_StatusType():
|
||||||
|
status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400)
|
||||||
|
dt = StatusType(status_codes)
|
||||||
|
assert dt.IDLE == status_codes.IDLE
|
||||||
|
assert dt.ERROR == status_codes.ERROR
|
||||||
|
assert dt._enum == status_codes
|
||||||
|
|
||||||
|
|
||||||
def test_get_datatype():
|
def test_get_datatype():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
get_datatype(1)
|
get_datatype(1)
|
||||||
|
@ -23,10 +23,11 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from secop.datatypes import FloatRange, IntRange, Property, StringType
|
||||||
|
from secop.errors import ProgrammingError
|
||||||
from secop.iohandler import CmdParser, IOHandler
|
from secop.iohandler import CmdParser, IOHandler
|
||||||
from secop.modules import Module, Parameter
|
from secop.modules import Module, Parameter
|
||||||
from secop.datatypes import FloatRange, StringType, IntRange, Property
|
|
||||||
from secop.errors import ProgrammingError
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('fmt, text, values, text2', [
|
@pytest.mark.parametrize('fmt, text, values, text2', [
|
||||||
('%d,%d', '2,3', [2,3], None),
|
('%d,%d', '2,3', [2,3], None),
|
||||||
@ -107,15 +108,11 @@ def test_IOHandler():
|
|||||||
|
|
||||||
|
|
||||||
class Module1(Module):
|
class Module1(Module):
|
||||||
properties = {
|
channel = Property('the channel', IntRange(), default=3)
|
||||||
'channel': Property('the channel', IntRange(), default=3),
|
loop = Property('the loop', IntRange(), default=2)
|
||||||
'loop': Property('the loop', IntRange(), default=2),
|
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
|
||||||
}
|
real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False)
|
||||||
parameters = {
|
text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False)
|
||||||
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
|
|
||||||
'real': Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False),
|
|
||||||
'text': Parameter('a string value', StringType(), default='x', handler=group2, readonly=False),
|
|
||||||
}
|
|
||||||
|
|
||||||
def sendRecv(self, command):
|
def sendRecv(self, command):
|
||||||
assert data.pop('command') == command
|
assert data.pop('command') == command
|
||||||
@ -196,6 +193,4 @@ def test_IOHandler():
|
|||||||
with pytest.raises(ProgrammingError): # can not use a handler for different modules
|
with pytest.raises(ProgrammingError): # can not use a handler for different modules
|
||||||
# pylint: disable=unused-variable
|
# pylint: disable=unused-variable
|
||||||
class Module2(Module):
|
class Module2(Module):
|
||||||
parameters = {
|
simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1)
|
||||||
'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1),
|
|
||||||
}
|
|
||||||
|
@ -22,13 +22,14 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
"""test data types."""
|
"""test data types."""
|
||||||
|
|
||||||
# no fixtures needed
|
|
||||||
#import pytest
|
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import BoolType, FloatRange, StringType
|
from secop.datatypes import BoolType, FloatRange, StringType
|
||||||
|
from secop.errors import ProgrammingError
|
||||||
from secop.modules import Communicator, Drivable, Module
|
from secop.modules import Communicator, Drivable, Module
|
||||||
from secop.params import Command, Override, Parameter
|
from secop.params import Command, Parameter
|
||||||
from secop.poller import BasicPoller
|
from secop.poller import BasicPoller
|
||||||
|
|
||||||
|
|
||||||
@ -64,30 +65,27 @@ def test_Communicator():
|
|||||||
assert event.is_set() # event should be set immediately
|
assert event.is_set() # event should be set immediately
|
||||||
|
|
||||||
|
|
||||||
def test_ModuleMeta():
|
def test_ModuleMagic():
|
||||||
class Newclass1(Drivable):
|
class Newclass1(Drivable):
|
||||||
parameters = {
|
param1 = Parameter('param1', datatype=BoolType(), default=False)
|
||||||
'pollinterval': Override(reorder=True),
|
param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True)
|
||||||
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
|
||||||
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
|
@Command(argument=BoolType(), result=BoolType())
|
||||||
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
def cmd(self, arg):
|
||||||
}
|
"""stuff"""
|
||||||
commands = {
|
return not arg
|
||||||
# intermixing parameters with commands is not recommended,
|
|
||||||
# but acceptable for influencing the order
|
a1 = Parameter('a1', datatype=BoolType(), default=False)
|
||||||
'a1': Parameter('a1', datatype=BoolType(), default=False),
|
a2 = Parameter('a2', datatype=BoolType(), default=True)
|
||||||
'a2': Parameter('a2', datatype=BoolType(), default=True),
|
value = Parameter(datatype=StringType(), default='first')
|
||||||
'value': Override(datatype=StringType(), default='first'),
|
|
||||||
'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()),
|
@Command(argument=BoolType(), result=BoolType())
|
||||||
}
|
def cmd2(self, arg):
|
||||||
|
"""another stuff"""
|
||||||
|
return not arg
|
||||||
|
|
||||||
pollerClass = BasicPoller
|
pollerClass = BasicPoller
|
||||||
|
|
||||||
def do_cmd(self, arg):
|
|
||||||
return not arg
|
|
||||||
|
|
||||||
def do_cmd2(self, arg):
|
|
||||||
return not arg
|
|
||||||
|
|
||||||
def read_param1(self):
|
def read_param1(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -103,19 +101,31 @@ def test_ModuleMeta():
|
|||||||
def read_value(self):
|
def read_value(self):
|
||||||
return 'second'
|
return 'second'
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
class Mod1(Module): # pylint: disable=unused-variable
|
||||||
|
def do_this(self): # old style command
|
||||||
|
pass
|
||||||
|
|
||||||
# first inherited accessibles, then Overrides with reorder=True and new accessibles
|
with pytest.raises(ProgrammingError):
|
||||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
class Mod2(Module): # pylint: disable=unused-variable
|
||||||
|
param = Parameter(), # pylint: disable=trailing-comma-tuple
|
||||||
|
|
||||||
|
|
||||||
|
# first inherited accessibles
|
||||||
|
sortcheck1 = ['value', 'status', 'pollinterval', 'target', 'stop',
|
||||||
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
||||||
|
|
||||||
class Newclass2(Newclass1):
|
class Newclass2(Newclass1):
|
||||||
parameters = {
|
paramOrder = 'param1', 'param2', 'cmd', 'value'
|
||||||
'cmd2': Override('another stuff'),
|
|
||||||
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
|
@Command(description='another stuff')
|
||||||
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False),
|
def cmd2(self, arg):
|
||||||
'b2': Parameter('<b2>', datatype=BoolType(), default=True,
|
return arg
|
||||||
poll=True, readonly=False, initwrite=True),
|
|
||||||
}
|
value = Parameter(datatype=FloatRange(unit='deg'))
|
||||||
|
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
|
||||||
|
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
|
||||||
|
poll=True, readonly=False, initwrite=True)
|
||||||
|
|
||||||
def write_a1(self, value):
|
def write_a1(self, value):
|
||||||
self._a1_written = value
|
self._a1_written = value
|
||||||
@ -128,14 +138,15 @@ def test_ModuleMeta():
|
|||||||
def read_value(self):
|
def read_value(self):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
sortcheck2 = ['value', 'status', 'target', 'pollinterval',
|
# first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones
|
||||||
'param1', 'param2', 'cmd', 'a2', 'cmd2', 'a1', 'b2']
|
sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
|
||||||
|
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
|
||||||
|
|
||||||
logger = LoggerStub()
|
logger = LoggerStub()
|
||||||
updates = {}
|
updates = {}
|
||||||
srv = ServerStub(updates)
|
srv = ServerStub(updates)
|
||||||
|
|
||||||
params_found = set() # set of instance accessibles
|
params_found = set() # set of instance accessibles
|
||||||
objects = []
|
objects = []
|
||||||
|
|
||||||
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
|
for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]:
|
||||||
@ -143,16 +154,11 @@ def test_ModuleMeta():
|
|||||||
o2 = newclass('o2', logger, {'.description':''}, srv)
|
o2 = newclass('o2', logger, {'.description':''}, srv)
|
||||||
for obj in [o1, o2]:
|
for obj in [o1, o2]:
|
||||||
objects.append(obj)
|
objects.append(obj)
|
||||||
ctr_found = set()
|
for o in obj.accessibles.values():
|
||||||
for n, o in obj.accessibles.items():
|
|
||||||
# check that instance accessibles are unique objects
|
# check that instance accessibles are unique objects
|
||||||
assert o not in params_found
|
assert o not in params_found
|
||||||
params_found.add(o)
|
params_found.add(o)
|
||||||
assert o.ctr not in ctr_found
|
assert list(obj.accessibles) == sortcheck
|
||||||
ctr_found.add(o.ctr)
|
|
||||||
check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck]
|
|
||||||
# HACK: atm. disabled to fix all other problems first.
|
|
||||||
assert check_order + sorted(check_order)
|
|
||||||
|
|
||||||
# check for inital updates working properly
|
# check for inital updates working properly
|
||||||
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
o1 = Newclass1('o1', logger, {'.description':''}, srv)
|
||||||
@ -214,7 +220,7 @@ def test_ModuleMeta():
|
|||||||
assert acs is not None
|
assert acs is not None
|
||||||
else: # do not check object or mixin
|
else: # do not check object or mixin
|
||||||
acs = {}
|
acs = {}
|
||||||
for n, o in acs.items():
|
for o in acs.values():
|
||||||
# check that class accessibles are not reused as instance accessibles
|
# check that class accessibles are not reused as instance accessibles
|
||||||
assert o not in params_found
|
assert o not in params_found
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.protocol.interface import encode_msg_frame, decode_msg
|
|
||||||
import secop.protocol.messages as m
|
import secop.protocol.messages as m
|
||||||
|
from secop.protocol.interface import decode_msg, encode_msg_frame
|
||||||
|
|
||||||
# args are: msg tuple, msg bytes
|
# args are: msg tuple, msg bytes
|
||||||
MSG = [
|
MSG = [
|
||||||
|
@ -25,66 +25,78 @@
|
|||||||
# no fixtures needed
|
# no fixtures needed
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import BoolType, IntRange
|
from secop.datatypes import BoolType, FloatRange, IntRange
|
||||||
from secop.params import Command, Override, Parameter, Parameters
|
|
||||||
from secop.errors import ProgrammingError
|
from secop.errors import ProgrammingError
|
||||||
|
from secop.modules import HasAccessibles
|
||||||
|
from secop.params import Command, Parameter
|
||||||
|
|
||||||
|
|
||||||
def test_Command():
|
def test_Command():
|
||||||
cmd = Command('do_something')
|
class Mod(HasAccessibles):
|
||||||
assert cmd.description == 'do_something'
|
@Command()
|
||||||
assert cmd.ctr
|
def cmd(self):
|
||||||
assert cmd.argument is None
|
"""do something"""
|
||||||
assert cmd.result is None
|
@Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing')
|
||||||
assert cmd.for_export() == {'datainfo': {'type': 'command'},
|
def cmd2(self):
|
||||||
'description': 'do_something'}
|
pass
|
||||||
|
|
||||||
cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1))
|
assert Mod.cmd.description == 'do something'
|
||||||
assert cmd.description
|
assert Mod.cmd.argument is None
|
||||||
assert isinstance(cmd.argument, IntRange)
|
assert Mod.cmd.result is None
|
||||||
assert isinstance(cmd.result, IntRange)
|
assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'},
|
||||||
assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9},
|
'description': 'do something'}
|
||||||
'result': {'type': 'int', 'min':-1, 'max':1}},
|
|
||||||
'description': 'do_something'}
|
assert Mod.cmd2.description == 'do some other thing'
|
||||||
assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
|
assert isinstance(Mod.cmd2.argument, IntRange)
|
||||||
'result': {'type': 'int', 'max': 1, 'min': -1}},
|
assert isinstance(Mod.cmd2.result, IntRange)
|
||||||
'description': 'do_something'}
|
assert Mod.cmd2.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min': -9, 'max': 9},
|
||||||
|
'result': {'type': 'int', 'min': -1, 'max': 1}},
|
||||||
|
'description': 'do some other thing'}
|
||||||
|
assert Mod.cmd2.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9},
|
||||||
|
'result': {'type': 'int', 'max': 1, 'min': -1}},
|
||||||
|
'description': 'do some other thing'}
|
||||||
|
|
||||||
|
|
||||||
def test_Parameter():
|
def test_Parameter():
|
||||||
p1 = Parameter('description1', datatype=IntRange(), default=0)
|
class Mod(HasAccessibles):
|
||||||
p2 = Parameter('description2', datatype=IntRange(), constant=1)
|
p1 = Parameter('desc1', datatype=FloatRange(), default=0)
|
||||||
assert p1 != p2
|
p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True)
|
||||||
assert p1.ctr != p2.ctr
|
p3 = Parameter('desc3', datatype=FloatRange(), default=0, readonly=False)
|
||||||
|
p4 = Parameter('desc4', datatype=FloatRange(), constant=1)
|
||||||
|
assert repr(Mod.p1) != repr(Mod.p3)
|
||||||
|
assert id(Mod.p1.datatype) != id(Mod.p2.datatype)
|
||||||
|
assert Mod.p1.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc1', 'readonly': True}
|
||||||
|
assert Mod.p2.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc2', 'readonly': True}
|
||||||
|
assert Mod.p3.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc3', 'readonly': False}
|
||||||
|
assert Mod.p4.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc4', 'readonly': True,
|
||||||
|
'constant': 1.0}
|
||||||
|
p3 = Mod.p1.copy()
|
||||||
|
assert id(p3) != id(Mod.p1)
|
||||||
|
assert repr(Mod.p1) == repr(p3)
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(ProgrammingError):
|
||||||
Parameter(None, datatype=float)
|
Parameter(None, datatype=float, inherit=False)
|
||||||
p3 = p1.copy()
|
|
||||||
assert p1.ctr != p3.ctr
|
|
||||||
p3.ctr = p1.ctr # manipulate ctr for next line
|
|
||||||
assert repr(p1) == repr(p3)
|
|
||||||
assert p1.datatype != p2.datatype
|
|
||||||
|
|
||||||
|
|
||||||
def test_Override():
|
def test_Override():
|
||||||
p = Parameter('description1', datatype=BoolType, default=False)
|
class Base(HasAccessibles):
|
||||||
o = Override(default=True, reorder=True)
|
p1 = Parameter('description1', datatype=BoolType, default=False)
|
||||||
assert o.ctr != p.ctr
|
p2 = Parameter('description1', datatype=BoolType, default=False)
|
||||||
q = o.apply(p)
|
p3 = Parameter('description1', datatype=BoolType, default=False)
|
||||||
assert q.ctr != o.ctr # override shall be useable to influence the order, hence copy the ctr value
|
|
||||||
assert q.ctr != p.ctr
|
|
||||||
assert o.ctr != p.ctr
|
|
||||||
assert q != p
|
|
||||||
|
|
||||||
p2 = Parameter('description2', datatype=BoolType, default=False)
|
class Mod(Base):
|
||||||
o2 = Override(default=True)
|
p1 = Parameter(default=True)
|
||||||
assert o2.ctr != p2.ctr
|
p2 = Parameter() # override without change
|
||||||
q2 = o2.apply(p2)
|
|
||||||
assert q2.ctr != o2.ctr
|
|
||||||
assert q2.ctr != p2.ctr # EVERY override makes a new parameter object -> ctr++
|
|
||||||
assert o2.ctr != p2.ctr
|
|
||||||
assert q2 != p2
|
|
||||||
|
|
||||||
def test_Parameters():
|
assert Mod.p1 != Base.p1
|
||||||
ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True)))
|
assert Mod.p2 != Base.p2
|
||||||
ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True)
|
assert Mod.p3 == Base.p3
|
||||||
assert ps['_p2'].export == '_p2'
|
|
||||||
|
assert id(Mod.p2) != id(Base.p2) # must be a new object
|
||||||
|
assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
|
||||||
|
|
||||||
|
|
||||||
|
def test_Export():
|
||||||
|
class Mod:
|
||||||
|
param = Parameter('description1', datatype=BoolType, default=False)
|
||||||
|
assert Mod.param.export == '_param'
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
"""test data types."""
|
"""test data types."""
|
||||||
|
|
||||||
|
|
||||||
from collections import OrderedDict
|
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -23,9 +23,12 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.modules import Drivable
|
from secop.modules import Drivable
|
||||||
from secop.poller import Poller, REGULAR, DYNAMIC, SLOW
|
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller
|
||||||
|
|
||||||
Status = Drivable.Status
|
Status = Drivable.Status
|
||||||
|
|
||||||
class Time:
|
class Time:
|
||||||
|
@ -23,39 +23,59 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
from secop.datatypes import FloatRange, IntRange, StringType, ValueType
|
||||||
from secop.errors import ProgrammingError, ConfigError
|
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||||
from secop.properties import Property, Properties, HasProperties
|
from secop.properties import HasProperties, Property
|
||||||
|
|
||||||
# args are: datatype, default, extname, export, mandatory, settable
|
|
||||||
|
def Prop(*args, name=None, **kwds):
|
||||||
|
# collect the args for Property
|
||||||
|
return name, args, kwds
|
||||||
|
|
||||||
|
|
||||||
|
# Property(description, datatype, default, ...)
|
||||||
V_test_Property = [
|
V_test_Property = [
|
||||||
[(StringType(), 'default', 'extname', False, False),
|
[Prop(StringType(), 'default', extname='extname', mandatory=False),
|
||||||
dict(default='default', extname='extname', export=True, mandatory=False)],
|
dict(default='default', extname='extname', export=True, mandatory=False)
|
||||||
[(IntRange(), '42', '_extname', False, True),
|
],
|
||||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True),
|
||||||
[(IntRange(), '42', '_extname', True, False),
|
dict(default=42, extname='_custom', export=True, mandatory=True),
|
||||||
dict(default=42, extname='_extname', export=True, mandatory=False)],
|
],
|
||||||
[(IntRange(), 42, '_extname', True, True),
|
[Prop(IntRange(), '42', export=True, name='name'),
|
||||||
dict(default=42, extname='_extname', export=True, mandatory=True)],
|
dict(default=42, extname='_name', export=True, mandatory=False)
|
||||||
[(IntRange(), 0, '', True, True),
|
],
|
||||||
dict(default=0, extname='', export=True, mandatory=True)],
|
[Prop(IntRange(), 42, '_extname', mandatory=True),
|
||||||
[(IntRange(), 0, '', True, False),
|
dict(default=42, extname='_extname', export=True, mandatory=True)
|
||||||
dict(default=0, extname='', export=True, mandatory=False)],
|
],
|
||||||
[(IntRange(), 0, '', False, True),
|
[Prop(IntRange(), 0, export=True, mandatory=True),
|
||||||
dict(default=0, extname='', export=False, mandatory=True)],
|
dict(default=0, extname='', export=True, mandatory=True)
|
||||||
[(IntRange(), 0, '', False, False),
|
],
|
||||||
dict(default=0, extname='', export=False, mandatory=False)],
|
[Prop(IntRange(), 0, export=True, mandatory=False),
|
||||||
[(IntRange(), None, '', None),
|
dict(default=0, extname='', export=True, mandatory=False)
|
||||||
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory
|
],
|
||||||
[(ValueType(), 1, '', False),
|
[Prop(IntRange(), 0, export=False, mandatory=True),
|
||||||
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory
|
dict(default=0, extname='', export=False, mandatory=True)
|
||||||
|
],
|
||||||
|
[Prop(IntRange(), 0, export=False, mandatory=False),
|
||||||
|
dict(default=0, extname='', export=False, mandatory=False)
|
||||||
|
],
|
||||||
|
[Prop(IntRange()),
|
||||||
|
dict(default=0, extname='', export=False, mandatory=True) # mandatory not given, no default -> mandatory
|
||||||
|
],
|
||||||
|
[Prop(ValueType(), 1),
|
||||||
|
dict(default=1, extname='', export=False, mandatory=False) # mandatory not given, default given -> NOT mandatory
|
||||||
|
],
|
||||||
]
|
]
|
||||||
@pytest.mark.parametrize('args, check', V_test_Property)
|
@pytest.mark.parametrize('propargs, check', V_test_Property)
|
||||||
def test_Property(args, check):
|
def test_Property(propargs, check):
|
||||||
p = Property('', *args)
|
name, args, kwds = propargs
|
||||||
|
p = Property('', *args, **kwds)
|
||||||
|
if name:
|
||||||
|
p.__set_name__(None, name)
|
||||||
result = {k: getattr(p, k) for k in check}
|
result = {k: getattr(p, k) for k in check}
|
||||||
assert result == check
|
assert result == check
|
||||||
|
|
||||||
|
|
||||||
def test_Property_basic():
|
def test_Property_basic():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
# pylint: disable=no-value-for-parameter
|
# pylint: disable=no-value-for-parameter
|
||||||
@ -67,47 +87,47 @@ def test_Property_basic():
|
|||||||
Property('', 1)
|
Property('', 1)
|
||||||
Property('', IntRange(), '42', 'extname', False, False)
|
Property('', IntRange(), '42', 'extname', False, False)
|
||||||
|
|
||||||
|
|
||||||
def test_Properties():
|
def test_Properties():
|
||||||
p = Properties()
|
class Cls(HasProperties):
|
||||||
with pytest.raises(ProgrammingError):
|
aa = Property('', IntRange(0, 99), '42', export=True)
|
||||||
p[1] = 2
|
bb = Property('', IntRange(), 0, export=False)
|
||||||
p['a'] = Property('', IntRange(), '42', export=True)
|
|
||||||
assert p['a'].default == 42
|
assert Cls.aa.default == 42
|
||||||
assert p['a'].export is True
|
assert Cls.aa.export is True
|
||||||
assert p['a'].extname == '_a'
|
assert Cls.aa.extname == '_aa'
|
||||||
with pytest.raises(ProgrammingError):
|
|
||||||
p['a'] = 137
|
cc = Cls()
|
||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(BadValueError):
|
||||||
del p[1]
|
cc.aa = 137
|
||||||
with pytest.raises(ProgrammingError):
|
|
||||||
del p['a']
|
assert Cls.bb.default == 0
|
||||||
p['a'] = Property('', IntRange(), 0, export=False)
|
assert Cls.bb.export is False
|
||||||
assert p['a'].default == 0
|
assert Cls.bb.extname == ''
|
||||||
assert p['a'].export is False
|
|
||||||
assert p['a'].extname == ''
|
|
||||||
|
|
||||||
|
|
||||||
class c(HasProperties):
|
class c(HasProperties):
|
||||||
properties = {
|
# properties
|
||||||
'a' : Property('', IntRange(), 1),
|
a = Property('', IntRange(), 1)
|
||||||
}
|
|
||||||
|
|
||||||
class cl(c):
|
class cl(c):
|
||||||
properties = {
|
# properties
|
||||||
'a' : Property('', IntRange(), 3),
|
a = Property('', IntRange(), 3)
|
||||||
'b' : Property('', FloatRange(), 3.14),
|
b = Property('', FloatRange(), 3.14)
|
||||||
'minabc': Property('', IntRange(), 8),
|
minabc = Property('', IntRange(), 8)
|
||||||
'maxabc': Property('', IntRange(), 9),
|
maxabc = Property('', IntRange(), 9)
|
||||||
'minx': Property('', IntRange(), 2),
|
minx = Property('', IntRange(), 2)
|
||||||
'maxy': Property('', IntRange(), 1),
|
maxy = Property('', IntRange(), 1)
|
||||||
}
|
|
||||||
|
|
||||||
def test_HasProperties():
|
def test_HasProperties():
|
||||||
o = c()
|
o = c()
|
||||||
assert o.properties['a'] == 1
|
assert o.a == 1
|
||||||
o = cl()
|
o = cl()
|
||||||
assert o.properties['a'] == 3
|
assert o.a == 3
|
||||||
assert o.properties['b'] == 3.14
|
assert o.b == 3.14
|
||||||
|
|
||||||
|
|
||||||
def test_Property_checks():
|
def test_Property_checks():
|
||||||
o = c()
|
o = c()
|
||||||
@ -119,6 +139,7 @@ def test_Property_checks():
|
|||||||
with pytest.raises(ConfigError):
|
with pytest.raises(ConfigError):
|
||||||
o.checkProperties()
|
o.checkProperties()
|
||||||
|
|
||||||
|
|
||||||
def test_Property_override():
|
def test_Property_override():
|
||||||
o1 = c()
|
o1 = c()
|
||||||
class co(c):
|
class co(c):
|
||||||
@ -131,10 +152,10 @@ def test_Property_override():
|
|||||||
class cx(c): # pylint: disable=unused-variable
|
class cx(c): # pylint: disable=unused-variable
|
||||||
def a(self):
|
def a(self):
|
||||||
pass
|
pass
|
||||||
assert 'collides with method' in str(e.value)
|
assert 'collides with' in str(e.value)
|
||||||
|
|
||||||
with pytest.raises(ProgrammingError) as e:
|
with pytest.raises(ProgrammingError) as e:
|
||||||
class cz(c): # pylint: disable=unused-variable
|
class cz(c): # pylint: disable=unused-variable
|
||||||
a = 's'
|
a = 's'
|
||||||
|
|
||||||
assert 'can not be set to' in str(e.value)
|
assert 'can not set' in str(e.value)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user