migrated secop_psi drivers to new syntax

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,147 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""basic validators (for properties)"""
import re
from secop.errors import ProgrammingError
def FloatProperty(value):
return float(value)
def PositiveFloatProperty(value):
value = float(value)
if value > 0:
return value
raise ValueError('Value must be >0 !')
def NonNegativeFloatProperty(value):
value = float(value)
if value >= 0:
return value
raise ValueError('Value must be >=0 !')
def IntProperty(value):
if int(value) == float(value):
return int(value)
raise ValueError('Can\'t convert %r to int!' % value)
def PositiveIntProperty(value):
value = IntProperty(value)
if value > 0:
return value
raise ValueError('Value must be >0 !')
def NonNegativeIntProperty(value):
value = IntProperty(value)
if value >= 0:
return value
raise ValueError('Value must be >=0 !')
def BoolProperty(value):
try:
if value.lower() in ['0', 'false', 'no', 'off',]:
return False
if value.lower() in ['1', 'true', 'yes', 'on', ]:
return True
except AttributeError: # was no string
if bool(value) == value:
return value
raise ValueError('%r is no valid boolean: try one of True, False, "on", "off",...' % value)
def StringProperty(value):
return str(value)
def UnitProperty(value):
# probably too simple!
for s in str(value):
if s.lower() not in '°abcdefghijklmnopqrstuvwxyz':
raise ValueError('%r is not a valid unit!')
def FmtStrProperty(value, regexp=re.compile(r'^%\.?\d+[efg]$')):
value=str(value)
if regexp.match(value):
return value
raise ValueError('%r is not a valid fmtstr!' % value)
def OneOfProperty(*args):
# literally oneof!
if not args:
raise ProgrammingError('OneOfProperty needs some argumets to check against!')
def OneOfChecker(value):
if value not in args:
raise ValueError('Value must be one of %r' % list(args))
return value
return OneOfChecker
def NoneOr(checker):
if not callable(checker):
raise ProgrammingError('NoneOr needs a basic validator as Argument!')
def NoneOrChecker(value):
if value is None:
return None
return checker(value)
return NoneOrChecker
def EnumProperty(**kwds):
if not kwds:
raise ProgrammingError('EnumProperty needs a mapping!')
def EnumChecker(value):
if value in kwds:
return kwds[value]
if value in kwds.values():
return value
raise ValueError('Value must be one of %r' % list(kwds))
return EnumChecker
def TupleProperty(*checkers):
if not checkers:
checkers = [None]
for c in checkers:
if not callable(c):
raise ProgrammingError('TupleProperty needs basic validators as Arguments!')
def TupleChecker(values):
if len(values)==len(checkers):
return tuple(c(v) for c, v in zip(checkers, values))
raise ValueError('Value needs %d elements!' % len(checkers))
return TupleChecker
def ListOfProperty(checker):
if not callable(checker):
raise ProgrammingError('ListOfProperty needs a basic validator as Argument!')
def ListOfChecker(values):
return [checker(v) for v in values]
return ListOfChecker

View File

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

View File

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

View File

@ -1,193 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""console client"""
# this needs to be reworked or removed
import code
import socket
import threading
from collections import deque
from os import path
import configparser
import mlzlog
from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
from secop.protocol.messages import EVENTREPLY
class NameSpace(dict):
def __init__(self):
dict.__init__(self)
self.__const = set()
def setconst(self, name, value):
dict.__setitem__(self, name, value)
self.__const.add(name)
def __setitem__(self, name, value):
if name in self.__const:
raise RuntimeError('%s cannot be assigned' % name)
dict.__setitem__(self, name, value)
def __delitem__(self, name):
if name in self.__const:
raise RuntimeError('%s cannot be deleted' % name)
dict.__delitem__(self, name)
def getClientOpts(cfgfile):
parser = configparser.SafeConfigParser()
if not parser.read([cfgfile + '.cfg']):
print("Error reading cfg file %r" % cfgfile)
return {}
if not parser.has_section('client'):
print("No Server section found!")
return dict(item for item in parser.items('client'))
class ClientConsole:
def __init__(self, cfgname, basepath):
self.namespace = NameSpace()
self.namespace.setconst('help', self.helpCmd)
cfgfile = path.join(basepath, 'etc', cfgname)
cfg = getClientOpts(cfgfile)
self.client = Client(cfg)
self.client.populateNamespace(self.namespace)
def run(self):
console = code.InteractiveConsole(self.namespace)
console.interact("Welcome to the SECoP console")
def close(self):
pass
def helpCmd(self, arg=Ellipsis):
if arg is Ellipsis:
print("No help available yet")
else:
help(arg)
class TCPConnection:
def __init__(self, connect, port, **kwds):
self.log = mlzlog.log.getChild('connection', False)
port = int(port)
self.connection = socket.create_connection((connect, port), 3)
self.queue = deque()
self._rcvdata = ''
self.callbacks = set()
self._thread = threading.Thread(target=self.thread)
self._thread.daemonize = True
self._thread.start()
def send(self, msg):
self.log.debug("Sending msg %r" % msg)
data = encode_msg_frame(*msg.serialize())
self.log.debug("raw data: %r" % data)
self.connection.sendall(data)
def thread(self):
while True:
try:
self.thread_step()
except Exception as e:
self.log.exception("Exception in RCV thread: %r" % e)
def thread_step(self):
data = b''
while True:
newdata = self.connection.recv(1024)
self.log.debug("RCV: got raw data %r" % newdata)
data = data + newdata
while True:
origin, data = get_msg(data)
if origin is None:
break # no more messages to process
if not origin: # empty string
continue # ???
_ = decode_msg(origin)
# construct msgObj from msg
try:
#msgObj = Message(*msg)
#msgObj.origin = origin.decode('latin-1')
#self.handle(msgObj)
pass
except Exception:
# ??? what to do here?
pass
def handle(self, msg):
if msg.action == EVENTREPLY:
self.log.info("got Async: %r" % msg)
for cb in self.callbacks:
try:
cb(msg)
except Exception as e:
self.log.debug(
"handle_async: got exception %r" % e, exception=True)
else:
self.queue.append(msg)
def read(self):
while not self.queue:
pass # XXX: remove BUSY polling
return self.queue.popleft()
def register_callback(self, callback):
"""registers callback for async data"""
self.callbacks.add(callback)
def unregister_callback(self, callback):
"""unregisters callback for async data"""
self.callbacks.discard(callback)
class Client:
def __init__(self, opts):
self.log = mlzlog.log.getChild('client', True)
self._cache = dict()
self.connection = TCPConnection(**opts)
self.connection.register_callback(self.handle_async)
def handle_async(self, msg):
self.log.info("Got async update %r" % msg)
module = msg.module
param = msg.param
value = msg.value
self._cache.getdefault(module, {})[param] = value
# XXX: further notification-callbacks needed ???
def populateNamespace(self, namespace):
#self.connection.send(Message(DESCRIPTIONREQUEST))
# reply = self.connection.read()
# self.log.info("found modules %r" % reply)
# create proxies, populate cache....
namespace.setconst('connection', self.connection)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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