diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css index 5f508cc..1c0636c 100644 --- a/doc/source/_static/custom.css +++ b/doc/source/_static/custom.css @@ -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; +} + diff --git a/doc/source/conf.py b/doc/source/conf.py index 052c9bc..d319d15 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -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) \ No newline at end of file diff --git a/doc/source/reference.rst b/doc/source/reference.rst index 7f3d8fb..a8f67a4 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -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 ......... diff --git a/doc/source/secop_psi.rst b/doc/source/secop_psi.rst index 0112651..fd3663c 100644 --- a/doc/source/secop_psi.rst +++ b/doc/source/secop_psi.rst @@ -1,6 +1,14 @@ PSI (SINQ) ---------- +CCU4 tutorial example +..................... + +.. automodule:: secop_psi.ccu4 + :show-inheritance: + :members: + + PPMS .... diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst deleted file mode 100644 index c349f65..0000000 --- a/doc/source/tutorial.rst +++ /dev/null @@ -1,7 +0,0 @@ -Tutorial --------- - -.. toctree:: - :maxdepth: 2 - - tutorial_helevel diff --git a/doc/source/tutorial_helevel.rst b/doc/source/tutorial_helevel.rst index 2e1b467..d778c2d 100644 --- a/doc/source/tutorial_helevel.rst +++ b/doc/source/tutorial_helevel.rst @@ -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=\n' + reply = self._iodev.communicate('h') # send 'h\n' and get the reply 'h=\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 + ``.read_`` 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_* and *write_* 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_* and *write_* 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 '= -# -# ***************************************************************************** -"""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 diff --git a/secop/client/__init__.py b/secop/client/__init__.py index 27d53fd..c1d22b4 100644 --- a/secop/client/__init__.py +++ b/secop/client/__init__.py @@ -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 diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py deleted file mode 100644 index e46b81c..0000000 --- a/secop/client/baseclient.py +++ /dev/null @@ -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 -# -# ***************************************************************************** -"""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]) diff --git a/secop/client/console.py b/secop/client/console.py deleted file mode 100644 index df3d815..0000000 --- a/secop/client/console.py +++ /dev/null @@ -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 -# -# ***************************************************************************** -"""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) diff --git a/secop/core.py b/secop/core.py index 8ea1cd4..e6ca57f 100644 --- a/secop/core.py +++ b/secop/core.py @@ -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 diff --git a/secop/datatypes.py b/secop/datatypes.py index 54a2293..93c9b46 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -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 = - - exception: use members= 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 = + :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): diff --git a/secop/errors.py b/secop/errors.py index 6d652a1..53300e7 100644 --- a/secop/errors.py +++ b/secop/errors.py @@ -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, diff --git a/secop/features.py b/secop/features.py index 8eb9d11..522ac53 100644 --- a/secop/features.py +++ b/secop/features.py @@ -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=''), - # XXX: tbd. (how to upload/download/select a curve?) - } + + # parameters + curve = Parameter('Calibration curve', datatype=StringType(80), default='') diff --git a/secop/gui/cfg_editor/config_file.py b/secop/gui/cfg_editor/config_file.py index a707231..3418e12 100644 --- a/secop/gui/cfg_editor/config_file.py +++ b/secop/gui/cfg_editor/config_file.py @@ -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 diff --git a/secop/gui/cfg_editor/mainwindow.py b/secop/gui/cfg_editor/mainwindow.py index 66284b6..099686c 100644 --- a/secop/gui/cfg_editor/mainwindow.py +++ b/secop/gui/cfg_editor/mainwindow.py @@ -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' diff --git a/secop/gui/cfg_editor/node_display.py b/secop/gui/cfg_editor/node_display.py index 4b80833..300d55c 100644 --- a/secop/gui/cfg_editor/node_display.py +++ b/secop/gui/cfg_editor/node_display.py @@ -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): diff --git a/secop/gui/cfg_editor/tree_widget_item.py b/secop/gui/cfg_editor/tree_widget_item.py index 72bf728..948d891 100644 --- a/secop/gui/cfg_editor/tree_widget_item.py +++ b/secop/gui/cfg_editor/tree_widget_item.py @@ -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 diff --git a/secop/gui/cfg_editor/utils.py b/secop/gui/cfg_editor/utils.py index 1aa4b42..d85ee96 100644 --- a/secop/gui/cfg_editor/utils.py +++ b/secop/gui/cfg_editor/utils.py @@ -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__) diff --git a/secop/gui/cfg_editor/widgets.py b/secop/gui/cfg_editor/widgets.py index 46c325c..577ac29 100644 --- a/secop/gui/cfg_editor/widgets.py +++ b/secop/gui/cfg_editor/widgets.py @@ -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' diff --git a/secop/gui/mainwindow.py b/secop/gui/mainwindow.py index b8b69e7..3399eba 100644 --- a/secop/gui/mainwindow.py +++ b/secop/gui/mainwindow.py @@ -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) diff --git a/secop/gui/modulectrl.py b/secop/gui/modulectrl.py index 700e25b..4c54d86 100644 --- a/secop/gui/modulectrl.py +++ b/secop/gui/modulectrl.py @@ -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) diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index e6d0278..af6d3da 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -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)) diff --git a/secop/gui/params/__init__.py b/secop/gui/params/__init__.py index 18fc959..92e5584 100644 --- a/secop/gui/params/__init__.py +++ b/secop/gui/params/__init__.py @@ -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): diff --git a/secop/gui/util.py b/secop/gui/util.py index 4e24b62..e908733 100644 --- a/secop/gui/util.py +++ b/secop/gui/util.py @@ -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 diff --git a/secop/gui/valuewidgets.py b/secop/gui/valuewidgets.py index 43ff3d5..856e6bb 100644 --- a/secop/gui/valuewidgets.py +++ b/secop/gui/valuewidgets.py @@ -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 diff --git a/secop/iohandler.py b/secop/iohandler.py index 1c18532..594554b 100644 --- a/secop/iohandler.py +++ b/secop/iohandler.py @@ -54,8 +54,8 @@ method has to be called explicitly int the write_ 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_ and change_) + 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 diff --git a/secop/lib/__init__.py b/secop/lib/__init__.py index c712950..ad3df26 100644 --- a/secop/lib/__init__.py +++ b/secop/lib/__init__.py @@ -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.""" diff --git a/secop/lib/asynconn.py b/secop/lib/asynconn.py index 829b45f..348f971 100644 --- a/secop/lib/asynconn.py +++ b/secop/lib/asynconn.py @@ -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[?