migrated secop_psi drivers to new syntax
- includes all changes up to 'fix inheritance order' from git_mlz
6a32ecf342
Change-Id: Ie3ceee3dbd0a9284b47b1d5b5dbe262eebe8f283
This commit is contained in:
@ -1,147 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""basic validators (for properties)"""
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
def FloatProperty(value):
|
||||
return float(value)
|
||||
|
||||
|
||||
def PositiveFloatProperty(value):
|
||||
value = float(value)
|
||||
if value > 0:
|
||||
return value
|
||||
raise ValueError('Value must be >0 !')
|
||||
|
||||
|
||||
def NonNegativeFloatProperty(value):
|
||||
value = float(value)
|
||||
if value >= 0:
|
||||
return value
|
||||
raise ValueError('Value must be >=0 !')
|
||||
|
||||
|
||||
def IntProperty(value):
|
||||
if int(value) == float(value):
|
||||
return int(value)
|
||||
raise ValueError('Can\'t convert %r to int!' % value)
|
||||
|
||||
|
||||
def PositiveIntProperty(value):
|
||||
value = IntProperty(value)
|
||||
if value > 0:
|
||||
return value
|
||||
raise ValueError('Value must be >0 !')
|
||||
|
||||
|
||||
def NonNegativeIntProperty(value):
|
||||
value = IntProperty(value)
|
||||
if value >= 0:
|
||||
return value
|
||||
raise ValueError('Value must be >=0 !')
|
||||
|
||||
|
||||
def BoolProperty(value):
|
||||
try:
|
||||
if value.lower() in ['0', 'false', 'no', 'off',]:
|
||||
return False
|
||||
if value.lower() in ['1', 'true', 'yes', 'on', ]:
|
||||
return True
|
||||
except AttributeError: # was no string
|
||||
if bool(value) == value:
|
||||
return value
|
||||
raise ValueError('%r is no valid boolean: try one of True, False, "on", "off",...' % value)
|
||||
|
||||
|
||||
def StringProperty(value):
|
||||
return str(value)
|
||||
|
||||
|
||||
def UnitProperty(value):
|
||||
# probably too simple!
|
||||
for s in str(value):
|
||||
if s.lower() not in '°abcdefghijklmnopqrstuvwxyz':
|
||||
raise ValueError('%r is not a valid unit!')
|
||||
|
||||
|
||||
def FmtStrProperty(value, regexp=re.compile(r'^%\.?\d+[efg]$')):
|
||||
value=str(value)
|
||||
if regexp.match(value):
|
||||
return value
|
||||
raise ValueError('%r is not a valid fmtstr!' % value)
|
||||
|
||||
|
||||
def OneOfProperty(*args):
|
||||
# literally oneof!
|
||||
if not args:
|
||||
raise ProgrammingError('OneOfProperty needs some argumets to check against!')
|
||||
def OneOfChecker(value):
|
||||
if value not in args:
|
||||
raise ValueError('Value must be one of %r' % list(args))
|
||||
return value
|
||||
return OneOfChecker
|
||||
|
||||
|
||||
def NoneOr(checker):
|
||||
if not callable(checker):
|
||||
raise ProgrammingError('NoneOr needs a basic validator as Argument!')
|
||||
def NoneOrChecker(value):
|
||||
if value is None:
|
||||
return None
|
||||
return checker(value)
|
||||
return NoneOrChecker
|
||||
|
||||
|
||||
def EnumProperty(**kwds):
|
||||
if not kwds:
|
||||
raise ProgrammingError('EnumProperty needs a mapping!')
|
||||
def EnumChecker(value):
|
||||
if value in kwds:
|
||||
return kwds[value]
|
||||
if value in kwds.values():
|
||||
return value
|
||||
raise ValueError('Value must be one of %r' % list(kwds))
|
||||
return EnumChecker
|
||||
|
||||
def TupleProperty(*checkers):
|
||||
if not checkers:
|
||||
checkers = [None]
|
||||
for c in checkers:
|
||||
if not callable(c):
|
||||
raise ProgrammingError('TupleProperty needs basic validators as Arguments!')
|
||||
def TupleChecker(values):
|
||||
if len(values)==len(checkers):
|
||||
return tuple(c(v) for c, v in zip(checkers, values))
|
||||
raise ValueError('Value needs %d elements!' % len(checkers))
|
||||
return TupleChecker
|
||||
|
||||
def ListOfProperty(checker):
|
||||
if not callable(checker):
|
||||
raise ProgrammingError('ListOfProperty needs a basic validator as Argument!')
|
||||
def ListOfChecker(values):
|
||||
return [checker(v) for v in values]
|
||||
return ListOfChecker
|
@ -22,21 +22,22 @@
|
||||
# *****************************************************************************
|
||||
"""general SECoP client"""
|
||||
|
||||
import time
|
||||
import queue
|
||||
import json
|
||||
from threading import Event, RLock, current_thread
|
||||
import queue
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from threading import Event, RLock, current_thread
|
||||
|
||||
from secop.lib import mkthread, formatExtendedTraceback, formatExtendedStack
|
||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from secop.datatypes import get_datatype
|
||||
from secop.protocol.interface import encode_msg_frame, decode_msg
|
||||
from secop.protocol.messages import REQUEST2REPLY, ERRORPREFIX, EVENTREPLY, WRITEREQUEST, WRITEREPLY, \
|
||||
READREQUEST, READREPLY, IDENTREQUEST, IDENTPREFIX, ENABLEEVENTSREQUEST, COMMANDREQUEST, \
|
||||
DESCRIPTIONREQUEST, HEARTBEATREQUEST
|
||||
import secop.errors
|
||||
import secop.params
|
||||
from secop.datatypes import get_datatype
|
||||
from secop.lib import mkthread
|
||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from secop.protocol.interface import decode_msg, encode_msg_frame
|
||||
from secop.protocol.messages import COMMANDREQUEST, \
|
||||
DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST, ERRORPREFIX, \
|
||||
EVENTREPLY, HEARTBEATREQUEST, IDENTPREFIX, IDENTREQUEST, \
|
||||
READREPLY, READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST
|
||||
|
||||
# replies to be handled for cache
|
||||
UPDATE_MESSAGES = {EVENTREPLY, READREPLY, WRITEREPLY, ERRORPREFIX + READREQUEST, ERRORPREFIX + EVENTREPLY}
|
||||
@ -160,7 +161,6 @@ class ProxyClient:
|
||||
if not cblist:
|
||||
self.callbacks[cbname].pop(key)
|
||||
|
||||
|
||||
def callback(self, key, cbname, *args):
|
||||
"""perform callbacks
|
||||
|
||||
|
@ -1,584 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""Define Client side proxies"""
|
||||
|
||||
|
||||
import json
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from select import select
|
||||
|
||||
try:
|
||||
import mlzlog
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import serial
|
||||
|
||||
from secop.datatypes import CommandType, EnumType, get_datatype
|
||||
from secop.errors import EXCEPTIONS
|
||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||
from secop.lib.parsing import format_time, parse_time
|
||||
from secop.protocol.messages import BUFFERREQUEST, COMMANDREQUEST, \
|
||||
DESCRIPTIONREPLY, DESCRIPTIONREQUEST, DISABLEEVENTSREQUEST, \
|
||||
ENABLEEVENTSREQUEST, ERRORPREFIX, EVENTREPLY, \
|
||||
HEARTBEATREQUEST, HELPREQUEST, IDENTREQUEST, READREPLY, \
|
||||
READREQUEST, REQUEST2REPLY, WRITEREPLY, WRITEREQUEST
|
||||
|
||||
class TCPConnection:
|
||||
# disguise a TCP connection as serial one
|
||||
|
||||
def __init__(self, host, port, getLogger=None):
|
||||
if getLogger:
|
||||
self.log = getLogger('TCPConnection')
|
||||
else:
|
||||
self.log = mlzlog.getLogger('TCPConnection')
|
||||
self._host = host
|
||||
self._port = int(port)
|
||||
self._thread = None
|
||||
self.callbacks = [] # called if SEC-node shuts down
|
||||
self._io = None
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
self._readbuffer = queue.Queue(100)
|
||||
time.sleep(1)
|
||||
io = socket.create_connection((self._host, self._port))
|
||||
io.setblocking(False)
|
||||
self.stopflag = False
|
||||
self._io = io
|
||||
if self._thread and self._thread.is_alive():
|
||||
return
|
||||
self._thread = mkthread(self._run)
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
data = b''
|
||||
while not self.stopflag:
|
||||
rlist, _, xlist = select([self._io], [], [self._io], 1)
|
||||
if xlist:
|
||||
# on some strange systems, a closed connection is indicated by
|
||||
# an exceptional condition instead of "read ready" + "empty recv"
|
||||
newdata = b''
|
||||
else:
|
||||
if not rlist:
|
||||
continue # check stopflag every second
|
||||
# self._io is now ready to read some bytes
|
||||
try:
|
||||
newdata = self._io.recv(1024)
|
||||
except socket.error as err:
|
||||
if err.args[0] == socket.EAGAIN:
|
||||
# if we receive an EAGAIN error, just continue
|
||||
continue
|
||||
newdata = b''
|
||||
except Exception:
|
||||
newdata = b''
|
||||
if not newdata: # no data on recv indicates a closed connection
|
||||
raise IOError('%s:%d disconnected' % (self._host, self._port))
|
||||
lines = (data + newdata).split(b'\n')
|
||||
for line in lines[:-1]: # last line is incomplete or empty
|
||||
try:
|
||||
self._readbuffer.put(line.strip(b'\r').decode('utf-8'),
|
||||
block=True, timeout=1)
|
||||
except queue.Full:
|
||||
self.log.debug('rcv queue full! dropping line: %r' % line)
|
||||
data = lines[-1]
|
||||
except Exception as err:
|
||||
self.log.error(err)
|
||||
try:
|
||||
self._io.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error:
|
||||
pass
|
||||
try:
|
||||
self._io.close()
|
||||
except socket.error:
|
||||
pass
|
||||
for cb, args in self.callbacks:
|
||||
cb(*args)
|
||||
|
||||
def readline(self, timeout=None):
|
||||
"""blocks until a full line was read and returns it
|
||||
|
||||
returns None when connection is stopped"""
|
||||
if self.stopflag:
|
||||
return None
|
||||
return self._readbuffer.get(block=True, timeout=timeout)
|
||||
|
||||
def stop(self):
|
||||
self.stopflag = True
|
||||
self._readbuffer.put(None) # terminate pending readline
|
||||
|
||||
def readable(self):
|
||||
return not self._readbuffer.empty()
|
||||
|
||||
def write(self, data):
|
||||
if self._io is None:
|
||||
self.connect()
|
||||
self._io.sendall(data.encode('latin-1'))
|
||||
|
||||
def writeline(self, line):
|
||||
self.write(line + '\n')
|
||||
|
||||
def writelines(self, *lines):
|
||||
for line in lines:
|
||||
self.writeline(line)
|
||||
|
||||
|
||||
class Value:
|
||||
t = None # pylint: disable = C0103
|
||||
u = None
|
||||
e = None
|
||||
fmtstr = '%s'
|
||||
|
||||
def __init__(self, value, qualifiers=None):
|
||||
self.value = value
|
||||
if qualifiers:
|
||||
self.__dict__.update(qualifiers)
|
||||
if 't' in qualifiers:
|
||||
try:
|
||||
self.t = float(qualifiers['t'])
|
||||
except Exception:
|
||||
self.t = parse_time(qualifiers['t'])
|
||||
|
||||
def __repr__(self):
|
||||
r = []
|
||||
if self.t is not None:
|
||||
r.append("timestamp=%r" % format_time(self.t))
|
||||
if self.u is not None:
|
||||
r.append('unit=%r' % self.u)
|
||||
if self.e is not None:
|
||||
r.append(('error=%s' % self.fmtstr) % self.e)
|
||||
if r:
|
||||
return (self.fmtstr + '(%s)') % (self.value, ', '.join(r))
|
||||
return self.fmtstr % self.value
|
||||
|
||||
|
||||
class Client:
|
||||
secop_id = 'unknown'
|
||||
describing_data = {}
|
||||
stopflag = False
|
||||
connection_established = False
|
||||
|
||||
def __init__(self, opts, autoconnect=True, getLogger=None):
|
||||
if 'testing' not in opts:
|
||||
if getLogger:
|
||||
self.log = getLogger('client')
|
||||
else:
|
||||
self.log = mlzlog.getLogger('client', True)
|
||||
else:
|
||||
class logStub:
|
||||
|
||||
def info(self, *args):
|
||||
pass
|
||||
debug = info
|
||||
error = info
|
||||
warning = info
|
||||
exception = info
|
||||
self.log = logStub()
|
||||
self._cache = dict()
|
||||
if 'module' in opts:
|
||||
# serial port
|
||||
devport = opts.pop('module')
|
||||
baudrate = int(opts.pop('baudrate', 115200))
|
||||
self.contactPoint = "serial://%s:%s" % (devport, baudrate)
|
||||
self.connection = serial.Serial(
|
||||
devport, baudrate=baudrate, timeout=1)
|
||||
self.connection.callbacks = []
|
||||
elif 'testing' not in opts:
|
||||
host = opts.pop('host', 'localhost')
|
||||
port = int(opts.pop('port', 10767))
|
||||
self.contactPoint = "tcp://%s:%d" % (host, port)
|
||||
self.connection = TCPConnection(host, port, getLogger=getLogger)
|
||||
else:
|
||||
self.contactPoint = 'testing'
|
||||
self.connection = opts.pop('testing')
|
||||
|
||||
# maps an expected reply to a list containing a single Event()
|
||||
# upon rcv of that reply, entry is appended with False and
|
||||
# the data of the reply.
|
||||
# if an error is received, the entry is appended with True and an
|
||||
# appropriate Exception.
|
||||
# Then the Event is set.
|
||||
self.expected_replies = {}
|
||||
|
||||
# maps spec to a set of callback functions (or single_shot callbacks)
|
||||
self.callbacks = dict()
|
||||
self.single_shots = dict()
|
||||
|
||||
# mapping the modulename to a dict mapping the parameter names to their values
|
||||
# note: the module value is stored as the value of the parameter value
|
||||
# of the module
|
||||
|
||||
self._syncLock = threading.RLock()
|
||||
self._thread = threading.Thread(target=self._run)
|
||||
self._thread.daemon = True
|
||||
self._thread.start()
|
||||
|
||||
if autoconnect:
|
||||
self.startup()
|
||||
|
||||
def _run(self):
|
||||
while not self.stopflag:
|
||||
try:
|
||||
self._inner_run()
|
||||
except Exception as err:
|
||||
print(formatExtendedStack())
|
||||
self.log.exception(err)
|
||||
raise
|
||||
|
||||
def _inner_run(self):
|
||||
data = ''
|
||||
self.connection.writeline('*IDN?')
|
||||
|
||||
while not self.stopflag:
|
||||
line = self.connection.readline()
|
||||
if line is None: # connection stopped
|
||||
break
|
||||
self.connection_established = True
|
||||
self.log.debug('got answer %r' % line)
|
||||
if line.startswith(('SECoP', 'SINE2020&ISSE,SECoP')):
|
||||
self.log.info('connected to: ' + line.strip())
|
||||
self.secop_id = line
|
||||
continue
|
||||
msgtype, spec, data = self.decode_message(line)
|
||||
if msgtype in (EVENTREPLY, READREPLY, WRITEREPLY):
|
||||
# handle async stuff
|
||||
self._handle_event(spec, data)
|
||||
# handle sync stuff
|
||||
self._handle_sync_reply(msgtype, spec, data)
|
||||
|
||||
def _handle_sync_reply(self, msgtype, spec, data):
|
||||
# handle sync stuff
|
||||
if msgtype.startswith(ERRORPREFIX):
|
||||
# find originating msgtype and map to expected_reply_type
|
||||
# errormessages carry to offending request as the first
|
||||
# result in the resultist
|
||||
request = msgtype[len(ERRORPREFIX):]
|
||||
reply = REQUEST2REPLY.get(request, request)
|
||||
|
||||
entry = self.expected_replies.get((reply, spec), None)
|
||||
if entry:
|
||||
self.log.error("request %r resulted in Error %r" %
|
||||
("%s %s" % (request, spec), (data[0], data[1])))
|
||||
entry.extend([True, EXCEPTIONS[data[0]](*data[1:])])
|
||||
entry[0].set()
|
||||
return
|
||||
self.log.error("got an unexpected %s %r" % (msgtype,data[0:1]))
|
||||
self.log.error(repr(data))
|
||||
return
|
||||
if msgtype == DESCRIPTIONREPLY:
|
||||
entry = self.expected_replies.get((msgtype, ''), None)
|
||||
else:
|
||||
entry = self.expected_replies.get((msgtype, spec), None)
|
||||
|
||||
if entry:
|
||||
self.log.debug("got expected reply '%s %s'" % (msgtype, spec)
|
||||
if spec else "got expected reply '%s'" % msgtype)
|
||||
entry.extend([False, msgtype, spec, data])
|
||||
entry[0].set()
|
||||
|
||||
def encode_message(self, requesttype, spec='', data=None):
|
||||
"""encodes the given message to a string
|
||||
"""
|
||||
req = [str(requesttype)]
|
||||
if spec:
|
||||
req.append(str(spec))
|
||||
if data is not None:
|
||||
req.append(json.dumps(data))
|
||||
req = ' '.join(req)
|
||||
return req
|
||||
|
||||
def decode_message(self, msg):
|
||||
"""return a decoded message triple"""
|
||||
msg = msg.strip()
|
||||
if ' ' not in msg:
|
||||
return msg, '', None
|
||||
msgtype, spec = msg.split(' ', 1)
|
||||
data = None
|
||||
if ' ' in spec:
|
||||
spec, json_data = spec.split(' ', 1)
|
||||
try:
|
||||
data = json.loads(json_data)
|
||||
except ValueError:
|
||||
# keep as string
|
||||
data = json_data
|
||||
# print formatException()
|
||||
return msgtype, spec, data
|
||||
|
||||
def _handle_event(self, spec, data):
|
||||
"""handles event"""
|
||||
# self.log.debug('handle_event %r %r' % (spec, data))
|
||||
if ':' not in spec:
|
||||
self.log.warning("deprecated specifier %r" % spec)
|
||||
spec = '%s:value' % spec
|
||||
modname, pname = spec.split(':', 1)
|
||||
|
||||
if data:
|
||||
self._cache.setdefault(modname, {})[pname] = Value(*data)
|
||||
else:
|
||||
self.log.warning(
|
||||
'got malformed answer! (%s,%s)' % (spec, data))
|
||||
|
||||
# self.log.info('cache: %s:%s=%r (was: %s)', modname, pname, data, previous)
|
||||
if spec in self.callbacks:
|
||||
for func in self.callbacks[spec]:
|
||||
try:
|
||||
mkthread(func, modname, pname, data)
|
||||
except Exception as err:
|
||||
self.log.exception('Exception in Callback!', err)
|
||||
run = set()
|
||||
if spec in self.single_shots:
|
||||
for func in self.single_shots[spec]:
|
||||
try:
|
||||
mkthread(func, data)
|
||||
except Exception as err:
|
||||
self.log.exception('Exception in Single-shot Callback!',
|
||||
err)
|
||||
run.add(func)
|
||||
self.single_shots[spec].difference_update(run)
|
||||
|
||||
def _getDescribingModuleData(self, module):
|
||||
return self.describingModulesData[module]
|
||||
|
||||
def _getDescribingParameterData(self, module, parameter):
|
||||
return self._getDescribingModuleData(module)['accessibles'][parameter]
|
||||
|
||||
def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102
|
||||
# take a dict and move all keys which are not in specialkeys
|
||||
# into a 'properties' subdict
|
||||
# specialkeys entries are converted from list to ordereddict
|
||||
try:
|
||||
result = {}
|
||||
for k in specialkeys:
|
||||
result[k] = OrderedDict(data.pop(k, []))
|
||||
result['properties'] = data
|
||||
return result
|
||||
except Exception as err:
|
||||
raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data))
|
||||
|
||||
def _issueDescribe(self):
|
||||
_, _, describing_data = self._communicate(DESCRIPTIONREQUEST)
|
||||
try:
|
||||
describing_data = self._decode_substruct(
|
||||
['modules'], describing_data)
|
||||
for modname, module in list(describing_data['modules'].items()):
|
||||
# convert old namings of interface_classes
|
||||
if 'interface_class' in module:
|
||||
module['interface_classes'] = module.pop('interface_class')
|
||||
elif 'interfaces' in module:
|
||||
module['interface_classes'] = module.pop('interfaces')
|
||||
describing_data['modules'][modname] = self._decode_substruct(
|
||||
['accessibles'], module)
|
||||
|
||||
self.describing_data = describing_data
|
||||
|
||||
for module, moduleData in self.describing_data['modules'].items():
|
||||
for aname, adata in moduleData['accessibles'].items():
|
||||
datatype = get_datatype(adata.pop('datainfo'))
|
||||
# *sigh* special handling for 'some' parameters....
|
||||
if isinstance(datatype, EnumType):
|
||||
datatype._enum.name = aname
|
||||
if aname == 'status':
|
||||
datatype.members[0]._enum.name = 'Status'
|
||||
self.describing_data['modules'][module]['accessibles'] \
|
||||
[aname]['datatype'] = datatype
|
||||
except Exception as _exc:
|
||||
print(formatException(verbose=True))
|
||||
raise
|
||||
|
||||
def register_callback(self, module, parameter, cb):
|
||||
self.log.debug('registering callback %r for %s:%s' %
|
||||
(cb, module, parameter))
|
||||
self.callbacks.setdefault('%s:%s' % (module, parameter), set()).add(cb)
|
||||
|
||||
def unregister_callback(self, module, parameter, cb):
|
||||
self.log.debug('unregistering callback %r for %s:%s' %
|
||||
(cb, module, parameter))
|
||||
self.callbacks.setdefault('%s:%s' % (module, parameter),
|
||||
set()).discard(cb)
|
||||
|
||||
def register_shutdown_callback(self, func, *args):
|
||||
self.connection.callbacks.append((func, args))
|
||||
|
||||
def communicate(self, msgtype, spec='', data=None):
|
||||
# only return the data portion....
|
||||
return self._communicate(msgtype, spec, data)[2]
|
||||
|
||||
def _communicate(self, msgtype, spec='', data=None):
|
||||
self.log.debug('communicate: %r %r %r' % (msgtype, spec, data))
|
||||
if self.stopflag:
|
||||
raise RuntimeError('alreading stopping!')
|
||||
if msgtype == IDENTREQUEST:
|
||||
return self.secop_id
|
||||
|
||||
# sanitize input
|
||||
msgtype = str(msgtype)
|
||||
spec = str(spec)
|
||||
|
||||
if msgtype not in (DESCRIPTIONREQUEST, ENABLEEVENTSREQUEST,
|
||||
DISABLEEVENTSREQUEST, COMMANDREQUEST,
|
||||
WRITEREQUEST, BUFFERREQUEST,
|
||||
READREQUEST, HEARTBEATREQUEST, HELPREQUEST):
|
||||
raise EXCEPTIONS['Protocol'](args=[
|
||||
self.encode_message(msgtype, spec, data),
|
||||
dict(
|
||||
errorclass='Protocol',
|
||||
errorinfo='%r: No Such Messagetype defined!' % msgtype, ),
|
||||
])
|
||||
|
||||
# handle syntactic sugar
|
||||
if msgtype == WRITEREQUEST and ':' not in spec:
|
||||
spec = spec + ':target'
|
||||
if msgtype == READREQUEST and ':' not in spec:
|
||||
spec = spec + ':value'
|
||||
|
||||
# check if such a request is already out
|
||||
rply = REQUEST2REPLY[msgtype]
|
||||
if (rply, spec) in self.expected_replies:
|
||||
raise RuntimeError(
|
||||
"can not have more than one requests of the same type at the same time!"
|
||||
)
|
||||
|
||||
# prepare sending request
|
||||
event = threading.Event()
|
||||
self.expected_replies[(rply, spec)] = [event]
|
||||
self.log.debug('prepared reception of %r msg' % rply)
|
||||
|
||||
# send request
|
||||
msg = self.encode_message(msgtype, spec, data)
|
||||
while not self.connection_established:
|
||||
self.log.debug('connection not established yet, waiting ...')
|
||||
time.sleep(0.1)
|
||||
self.connection.writeline(msg)
|
||||
self.log.debug('sent msg %r' % msg)
|
||||
|
||||
# wait for reply. timeout after 10s
|
||||
if event.wait(10):
|
||||
self.log.debug('checking reply')
|
||||
entry = self.expected_replies.pop((rply, spec))
|
||||
# entry is: event, is_error, exc_or_msgtype [,spec, date]<- if !err
|
||||
is_error = entry[1]
|
||||
if is_error:
|
||||
# if error, entry[2] contains the rigth Exception to raise
|
||||
raise entry[2]
|
||||
# valid reply: entry[2:5] contain msgtype, spec, data
|
||||
return tuple(entry[2:5])
|
||||
|
||||
# timed out
|
||||
del self.expected_replies[(rply, spec)]
|
||||
# XXX: raise a TimedOut ?
|
||||
raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype)
|
||||
|
||||
def quit(self):
|
||||
# after calling this the client is dysfunctional!
|
||||
# self.communicate(DISABLEEVENTSREQUEST)
|
||||
self.stopflag = True
|
||||
self.connection.stop()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(10)
|
||||
|
||||
def startup(self, _async=False):
|
||||
self._issueDescribe()
|
||||
# always fill our cache
|
||||
self.communicate(ENABLEEVENTSREQUEST)
|
||||
# deactivate updates if not wanted
|
||||
if not _async:
|
||||
self.communicate(DISABLEEVENTSREQUEST)
|
||||
|
||||
def queryCache(self, module, parameter=None):
|
||||
result = self._cache.get(module, {})
|
||||
|
||||
if parameter is not None:
|
||||
result = result[parameter]
|
||||
|
||||
return result
|
||||
|
||||
def getParameter(self, module, parameter):
|
||||
return self.communicate(READREQUEST, '%s:%s' % (module, parameter))
|
||||
|
||||
def setParameter(self, module, parameter, value):
|
||||
datatype = self._getDescribingParameterData(module,
|
||||
parameter)['datatype']
|
||||
|
||||
value = datatype.from_string(value)
|
||||
value = datatype.export_value(value)
|
||||
self.communicate(WRITEREQUEST, '%s:%s' % (module, parameter), value)
|
||||
|
||||
@property
|
||||
def describingData(self):
|
||||
return self.describing_data
|
||||
|
||||
@property
|
||||
def describingModulesData(self):
|
||||
return self.describingData['modules']
|
||||
|
||||
@property
|
||||
def equipmentId(self):
|
||||
if self.describingData:
|
||||
return self.describingData['properties']['equipment_id']
|
||||
return 'Undetermined'
|
||||
|
||||
@property
|
||||
def protocolVersion(self):
|
||||
return self.secop_id
|
||||
|
||||
@property
|
||||
def modules(self):
|
||||
return list(self.describing_data['modules'].keys())
|
||||
|
||||
def getParameters(self, module):
|
||||
params = filter(lambda item: not isinstance(item[1]['datatype'], CommandType),
|
||||
self.describing_data['modules'][module]['accessibles'].items())
|
||||
return list(param[0] for param in params)
|
||||
|
||||
def getModuleProperties(self, module):
|
||||
return self.describing_data['modules'][module]['properties']
|
||||
|
||||
def getModuleBaseClass(self, module):
|
||||
return self.getModuleProperties(module)['interface_classes']
|
||||
|
||||
def getCommands(self, module):
|
||||
cmds = filter(lambda item: isinstance(item[1]['datatype'], CommandType),
|
||||
self.describing_data['modules'][module]['accessibles'].items())
|
||||
return OrderedDict(cmds)
|
||||
|
||||
def execCommand(self, module, command, args):
|
||||
# ignore reply message + reply specifier, only return data
|
||||
return self._communicate(COMMANDREQUEST, '%s:%s' % (module, command), list(args) if args else None)[2]
|
||||
|
||||
def getProperties(self, module, parameter):
|
||||
return self.describing_data['modules'][module]['accessibles'][parameter]
|
||||
|
||||
def syncCommunicate(self, *msg):
|
||||
res = self._communicate(*msg) # pylint: disable=E1120
|
||||
try:
|
||||
res = self.encode_message(*res)
|
||||
except Exception:
|
||||
res = str(res)
|
||||
return res
|
||||
|
||||
def ping(self, pingctr=[0]): # pylint: disable=W0102
|
||||
pingctr[0] = pingctr[0] + 1
|
||||
self.communicate(HEARTBEATREQUEST, pingctr[0])
|
@ -1,193 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""console client"""
|
||||
|
||||
# this needs to be reworked or removed
|
||||
|
||||
|
||||
import code
|
||||
import socket
|
||||
import threading
|
||||
from collections import deque
|
||||
from os import path
|
||||
import configparser
|
||||
import mlzlog
|
||||
|
||||
from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
|
||||
from secop.protocol.messages import EVENTREPLY
|
||||
|
||||
|
||||
|
||||
class NameSpace(dict):
|
||||
|
||||
def __init__(self):
|
||||
dict.__init__(self)
|
||||
self.__const = set()
|
||||
|
||||
def setconst(self, name, value):
|
||||
dict.__setitem__(self, name, value)
|
||||
self.__const.add(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
if name in self.__const:
|
||||
raise RuntimeError('%s cannot be assigned' % name)
|
||||
dict.__setitem__(self, name, value)
|
||||
|
||||
def __delitem__(self, name):
|
||||
if name in self.__const:
|
||||
raise RuntimeError('%s cannot be deleted' % name)
|
||||
dict.__delitem__(self, name)
|
||||
|
||||
|
||||
|
||||
def getClientOpts(cfgfile):
|
||||
parser = configparser.SafeConfigParser()
|
||||
if not parser.read([cfgfile + '.cfg']):
|
||||
print("Error reading cfg file %r" % cfgfile)
|
||||
return {}
|
||||
if not parser.has_section('client'):
|
||||
print("No Server section found!")
|
||||
return dict(item for item in parser.items('client'))
|
||||
|
||||
|
||||
class ClientConsole:
|
||||
|
||||
def __init__(self, cfgname, basepath):
|
||||
self.namespace = NameSpace()
|
||||
self.namespace.setconst('help', self.helpCmd)
|
||||
|
||||
cfgfile = path.join(basepath, 'etc', cfgname)
|
||||
cfg = getClientOpts(cfgfile)
|
||||
self.client = Client(cfg)
|
||||
self.client.populateNamespace(self.namespace)
|
||||
|
||||
def run(self):
|
||||
console = code.InteractiveConsole(self.namespace)
|
||||
console.interact("Welcome to the SECoP console")
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def helpCmd(self, arg=Ellipsis):
|
||||
if arg is Ellipsis:
|
||||
print("No help available yet")
|
||||
else:
|
||||
help(arg)
|
||||
|
||||
|
||||
class TCPConnection:
|
||||
|
||||
def __init__(self, connect, port, **kwds):
|
||||
self.log = mlzlog.log.getChild('connection', False)
|
||||
port = int(port)
|
||||
self.connection = socket.create_connection((connect, port), 3)
|
||||
self.queue = deque()
|
||||
self._rcvdata = ''
|
||||
self.callbacks = set()
|
||||
self._thread = threading.Thread(target=self.thread)
|
||||
self._thread.daemonize = True
|
||||
self._thread.start()
|
||||
|
||||
def send(self, msg):
|
||||
self.log.debug("Sending msg %r" % msg)
|
||||
data = encode_msg_frame(*msg.serialize())
|
||||
self.log.debug("raw data: %r" % data)
|
||||
self.connection.sendall(data)
|
||||
|
||||
def thread(self):
|
||||
while True:
|
||||
try:
|
||||
self.thread_step()
|
||||
except Exception as e:
|
||||
self.log.exception("Exception in RCV thread: %r" % e)
|
||||
|
||||
def thread_step(self):
|
||||
data = b''
|
||||
while True:
|
||||
newdata = self.connection.recv(1024)
|
||||
self.log.debug("RCV: got raw data %r" % newdata)
|
||||
data = data + newdata
|
||||
while True:
|
||||
origin, data = get_msg(data)
|
||||
if origin is None:
|
||||
break # no more messages to process
|
||||
if not origin: # empty string
|
||||
continue # ???
|
||||
_ = decode_msg(origin)
|
||||
# construct msgObj from msg
|
||||
try:
|
||||
#msgObj = Message(*msg)
|
||||
#msgObj.origin = origin.decode('latin-1')
|
||||
#self.handle(msgObj)
|
||||
pass
|
||||
except Exception:
|
||||
# ??? what to do here?
|
||||
pass
|
||||
|
||||
def handle(self, msg):
|
||||
if msg.action == EVENTREPLY:
|
||||
self.log.info("got Async: %r" % msg)
|
||||
for cb in self.callbacks:
|
||||
try:
|
||||
cb(msg)
|
||||
except Exception as e:
|
||||
self.log.debug(
|
||||
"handle_async: got exception %r" % e, exception=True)
|
||||
else:
|
||||
self.queue.append(msg)
|
||||
|
||||
def read(self):
|
||||
while not self.queue:
|
||||
pass # XXX: remove BUSY polling
|
||||
return self.queue.popleft()
|
||||
|
||||
def register_callback(self, callback):
|
||||
"""registers callback for async data"""
|
||||
self.callbacks.add(callback)
|
||||
|
||||
def unregister_callback(self, callback):
|
||||
"""unregisters callback for async data"""
|
||||
self.callbacks.discard(callback)
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
def __init__(self, opts):
|
||||
self.log = mlzlog.log.getChild('client', True)
|
||||
self._cache = dict()
|
||||
self.connection = TCPConnection(**opts)
|
||||
self.connection.register_callback(self.handle_async)
|
||||
|
||||
def handle_async(self, msg):
|
||||
self.log.info("Got async update %r" % msg)
|
||||
module = msg.module
|
||||
param = msg.param
|
||||
value = msg.value
|
||||
self._cache.getdefault(module, {})[param] = value
|
||||
# XXX: further notification-callbacks needed ???
|
||||
|
||||
def populateNamespace(self, namespace):
|
||||
#self.connection.send(Message(DESCRIPTIONREQUEST))
|
||||
# reply = self.connection.read()
|
||||
# self.log.info("found modules %r" % reply)
|
||||
# create proxies, populate cache....
|
||||
namespace.setconst('connection', self.connection)
|
@ -26,14 +26,14 @@
|
||||
# allow to import the most important classes from 'secop'
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from secop.datatypes import FloatRange, IntRange, ScaledInteger, \
|
||||
BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf
|
||||
from secop.lib.enum import Enum
|
||||
from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached
|
||||
from secop.properties import Property
|
||||
from secop.params import Parameter, Command, Override
|
||||
from secop.metaclass import Done
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||
FloatRange, IntRange, ScaledInteger, StringType, StructOf, TupleOf
|
||||
from secop.iohandler import IOHandler, IOHandlerBase
|
||||
from secop.stringio import StringIO, HasIodev
|
||||
from secop.proxy import SecNode, Proxy, proxy_class
|
||||
from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC
|
||||
from secop.lib.enum import Enum
|
||||
from secop.modules import Attached, Communicator, \
|
||||
Done, Drivable, Module, Readable, Writable
|
||||
from secop.params import Command, Parameter
|
||||
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
|
||||
from secop.properties import Property
|
||||
from secop.proxy import Proxy, SecNode, proxy_class
|
||||
from secop.stringio import HasIodev, StringIO
|
||||
|
@ -28,13 +28,13 @@
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from secop.errors import ProgrammingError, ProtocolError, BadValueError, ConfigError
|
||||
from secop.errors import BadValueError, \
|
||||
ConfigError, ProgrammingError, ProtocolError
|
||||
from secop.lib import clamp
|
||||
from secop.lib.enum import Enum
|
||||
from secop.parse import Parser
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
|
||||
# Only export these classes for 'from secop.datatypes import *'
|
||||
__all__ = [
|
||||
'DataType', 'get_datatype',
|
||||
@ -53,6 +53,7 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
|
||||
Parser = Parser()
|
||||
|
||||
|
||||
# base class for all DataTypes
|
||||
class DataType(HasProperties):
|
||||
"""base class for all data types"""
|
||||
IS_COMMAND = False
|
||||
@ -97,7 +98,7 @@ class DataType(HasProperties):
|
||||
def set_properties(self, **kwds):
|
||||
"""init datatype properties"""
|
||||
try:
|
||||
for k,v in kwds.items():
|
||||
for k, v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
self.checkProperties()
|
||||
except Exception as e:
|
||||
@ -126,10 +127,6 @@ class DataType(HasProperties):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def short_doc(self):
|
||||
"""short description for automatic extension of doc strings"""
|
||||
return None
|
||||
|
||||
|
||||
class Stub(DataType):
|
||||
"""incomplete datatype, to be replaced with a proper one later during module load
|
||||
@ -154,42 +151,35 @@ class Stub(DataType):
|
||||
"""
|
||||
for dtcls in globals().values():
|
||||
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
||||
for prop in dtcls.properties.values():
|
||||
for prop in dtcls.propertyDict.values():
|
||||
stub = prop.datatype
|
||||
if isinstance(stub, cls):
|
||||
prop.datatype = globals()[stub.name](*stub.args)
|
||||
|
||||
def short_doc(self):
|
||||
return self.name.replace('Type', '').replace('Range', '').lower()
|
||||
|
||||
|
||||
# SECoP types:
|
||||
|
||||
|
||||
class FloatRange(DataType):
|
||||
"""(restricted) float type
|
||||
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
:param properties: any of the properties below
|
||||
:param kwds: any of the properties below
|
||||
"""
|
||||
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
|
||||
max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
|
||||
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
|
||||
extname='absolute_resolution', default=0.0)
|
||||
relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
|
||||
extname='relative_resolution', default=1.2e-7)
|
||||
|
||||
properties = {
|
||||
'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max),
|
||||
'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max),
|
||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
||||
'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0),
|
||||
extname='absolute_resolution', default=0.0),
|
||||
'relative_resolution': Property('relative resolution', Stub('FloatRange', 0),
|
||||
extname='relative_resolution', default=1.2e-7),
|
||||
}
|
||||
|
||||
def __init__(self, minval=None, maxval=None, **properties):
|
||||
def __init__(self, minval=None, maxval=None, **kwds):
|
||||
super().__init__()
|
||||
properties['min'] = minval if minval is not None else -sys.float_info.max
|
||||
properties['max'] = maxval if maxval is not None else sys.float_info.max
|
||||
self.set_properties(**properties)
|
||||
kwds['min'] = minval if minval is not None else -sys.float_info.max
|
||||
kwds['max'] = maxval if maxval is not None else sys.float_info.max
|
||||
self.set_properties(**kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||
@ -213,7 +203,7 @@ class FloatRange(DataType):
|
||||
if self.min - prec <= value <= self.max + prec:
|
||||
return min(max(value, self.min), self.max)
|
||||
raise BadValueError('%.14g should be a float between %.14g and %.14g' %
|
||||
(value, self.min, self.max))
|
||||
(value, self.min, self.max))
|
||||
|
||||
def __repr__(self):
|
||||
hints = self.get_info()
|
||||
@ -221,7 +211,7 @@ class FloatRange(DataType):
|
||||
hints['minval'] = hints.pop('min')
|
||||
if 'max' in hints:
|
||||
hints['maxval'] = hints.pop('max')
|
||||
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k,v) for k,v in hints.items()))
|
||||
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k, v) for k, v in hints.items()))
|
||||
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -249,9 +239,6 @@ class FloatRange(DataType):
|
||||
other(max(sys.float_info.min, self.min))
|
||||
other(min(sys.float_info.max, self.max))
|
||||
|
||||
def short_doc(self):
|
||||
return 'float'
|
||||
|
||||
|
||||
class IntRange(DataType):
|
||||
"""restricted int type
|
||||
@ -259,12 +246,10 @@ class IntRange(DataType):
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
"""
|
||||
properties = {
|
||||
'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True),
|
||||
'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True),
|
||||
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
||||
# 'unit': Property('physical unit', StringType(), extname='unit', default=''),
|
||||
}
|
||||
min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True)
|
||||
max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True)
|
||||
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
||||
# unit = Property('physical unit', StringType(), extname='unit', default='')
|
||||
|
||||
def __init__(self, minval=None, maxval=None):
|
||||
super().__init__()
|
||||
@ -290,7 +275,12 @@ class IntRange(DataType):
|
||||
raise BadValueError('Can not convert %r to int' % value)
|
||||
|
||||
def __repr__(self):
|
||||
return 'IntRange(%d, %d)' % (self.min, self.max)
|
||||
args = (self.min, self.max)
|
||||
if args[1] == DEFAULT_MAX_INT:
|
||||
args = args[:1]
|
||||
if args[0] == DEFAULT_MIN_INT:
|
||||
args = ()
|
||||
return 'IntRange%s' % repr(args)
|
||||
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -316,48 +306,38 @@ class IntRange(DataType):
|
||||
for i in range(self.min, self.max + 1):
|
||||
other(i)
|
||||
|
||||
def short_doc(self):
|
||||
return 'int'
|
||||
|
||||
|
||||
class ScaledInteger(DataType):
|
||||
"""scaled integer (= fixed resolution float) type
|
||||
|
||||
| In general *ScaledInteger* is needed only in special cases,
|
||||
e.g. when the a SEC node is running on very limited hardware
|
||||
without floating point support.
|
||||
| Please use *FloatRange* instead.
|
||||
|
||||
:param minval: (property **min**)
|
||||
:param maxval: (property **max**)
|
||||
:param properties: any of the properties below
|
||||
:param kwds: any of the properties below
|
||||
|
||||
{properties}
|
||||
:note: - limits are for the scaled float value
|
||||
- the scale is only used for calculating to/from transport serialisation
|
||||
note: limits are for the scaled float value
|
||||
the scale is only used for calculating to/from transport serialisation
|
||||
"""
|
||||
properties = {
|
||||
'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True),
|
||||
'min': Property('low limit', FloatRange(), extname='min', mandatory=True),
|
||||
'max': Property('high limit', FloatRange(), extname='max', mandatory=True),
|
||||
'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''),
|
||||
'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'),
|
||||
'absolute_resolution': Property('absolute resolution', FloatRange(0),
|
||||
extname='absolute_resolution', default=0.0),
|
||||
'relative_resolution': Property('relative resolution', FloatRange(0),
|
||||
extname='relative_resolution', default=1.2e-7),
|
||||
}
|
||||
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
|
||||
min = Property('low limit', FloatRange(), extname='min', mandatory=True)
|
||||
max = Property('high limit', FloatRange(), extname='max', mandatory=True)
|
||||
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
|
||||
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
|
||||
absolute_resolution = Property('absolute resolution', FloatRange(0),
|
||||
extname='absolute_resolution', default=0.0)
|
||||
relative_resolution = Property('relative resolution', FloatRange(0),
|
||||
extname='relative_resolution', default=1.2e-7)
|
||||
|
||||
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **properties):
|
||||
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
|
||||
super().__init__()
|
||||
scale = float(scale)
|
||||
if absolute_resolution is None:
|
||||
absolute_resolution = scale
|
||||
self.set_properties(scale=scale,
|
||||
self.set_properties(
|
||||
scale=scale,
|
||||
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
|
||||
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
||||
absolute_resolution=absolute_resolution,
|
||||
**properties)
|
||||
**kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||
@ -384,8 +364,8 @@ class ScaledInteger(DataType):
|
||||
|
||||
def export_datatype(self):
|
||||
return self.get_info(type='scaled',
|
||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
||||
min=int((self.min + self.scale * 0.5) // self.scale),
|
||||
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
@ -398,15 +378,15 @@ class ScaledInteger(DataType):
|
||||
value = min(max(value, self.min), self.max)
|
||||
else:
|
||||
raise BadValueError('%g should be a float between %g and %g' %
|
||||
(value, self.min, self.max))
|
||||
(value, self.min, self.max))
|
||||
intval = int((value + self.scale * 0.5) // self.scale)
|
||||
value = float(intval * self.scale)
|
||||
return value # return 'actual' value (which is more discrete than a float)
|
||||
|
||||
def __repr__(self):
|
||||
hints = self.get_info(scale=float('%g' % self.scale),
|
||||
min = int((self.min + self.scale * 0.5) // self.scale),
|
||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
||||
min=int((self.min + self.scale * 0.5) // self.scale),
|
||||
max=int((self.max + self.scale * 0.5) // self.scale))
|
||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||
|
||||
def export_value(self, value):
|
||||
@ -435,25 +415,19 @@ class ScaledInteger(DataType):
|
||||
other(self.min)
|
||||
other(self.max)
|
||||
|
||||
def short_doc(self):
|
||||
return 'float'
|
||||
|
||||
|
||||
class EnumType(DataType):
|
||||
"""enumeration
|
||||
|
||||
:param enum_or_name: the name of the Enum or an Enum to inherit from
|
||||
:param members: each argument denotes <member name>=<member int value>
|
||||
|
||||
exception: use members=<member dict> to add members from a dict
|
||||
:param members: members dict or None when using kwds only
|
||||
:param kwds: (additional) members
|
||||
"""
|
||||
def __init__(self, enum_or_name='', **members):
|
||||
def __init__(self, enum_or_name='', *, members=None, **kwds):
|
||||
super().__init__()
|
||||
if 'members' in members:
|
||||
members = dict(members)
|
||||
members.update(members['members'])
|
||||
members.pop('members')
|
||||
self._enum = Enum(enum_or_name, **members)
|
||||
if members is not None:
|
||||
kwds.update(members)
|
||||
self._enum = Enum(enum_or_name, **kwds)
|
||||
self.default = self._enum[self._enum.members[0]]
|
||||
|
||||
def copy(self):
|
||||
@ -461,10 +435,11 @@ class EnumType(DataType):
|
||||
return EnumType(self._enum)
|
||||
|
||||
def export_datatype(self):
|
||||
return {'type': 'enum', 'members':dict((m.name, m.value) for m in self._enum.members)}
|
||||
return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)}
|
||||
|
||||
def __repr__(self):
|
||||
return "EnumType(%r, %s)" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members))
|
||||
return "EnumType(%r, %s)" % (self._enum.name,
|
||||
', '.join('%s=%d' % (m.name, m.value) for m in self._enum.members))
|
||||
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
@ -478,7 +453,7 @@ class EnumType(DataType):
|
||||
"""return the validated (internal) value or raise"""
|
||||
try:
|
||||
return self._enum[value]
|
||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||
raise BadValueError('%r is not a member of enum %r' % (value, self._enum))
|
||||
|
||||
def from_string(self, text):
|
||||
@ -487,25 +462,24 @@ class EnumType(DataType):
|
||||
def format_value(self, value, unit=None):
|
||||
return '%s<%s>' % (self._enum[value].name, self._enum[value].value)
|
||||
|
||||
def set_name(self, name):
|
||||
self._enum.name = name
|
||||
|
||||
def compatible(self, other):
|
||||
for m in self._enum.members:
|
||||
other(m)
|
||||
|
||||
def short_doc(self):
|
||||
return 'one of %s' % str(tuple(self._enum.keys()))
|
||||
|
||||
|
||||
class BLOBType(DataType):
|
||||
"""binary large object
|
||||
|
||||
internally treated as bytes
|
||||
"""
|
||||
properties = {
|
||||
'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0),
|
||||
'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||
mandatory=True),
|
||||
}
|
||||
|
||||
minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes',
|
||||
default=0)
|
||||
maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes',
|
||||
mandatory=True)
|
||||
|
||||
def __init__(self, minbytes=0, maxbytes=None):
|
||||
super().__init__()
|
||||
@ -565,21 +539,20 @@ class BLOBType(DataType):
|
||||
class StringType(DataType):
|
||||
"""string
|
||||
|
||||
for parameters see properties below
|
||||
"""
|
||||
properties = {
|
||||
'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0),
|
||||
'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='maxchars', default=UNLIMITED),
|
||||
'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||
Stub('BoolType'), extname='isUTF8', default=False),
|
||||
}
|
||||
minchars = Property('minimum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='minchars', default=0)
|
||||
maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED),
|
||||
extname='maxchars', default=UNLIMITED)
|
||||
isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII',
|
||||
Stub('BoolType'), extname='isUTF8', default=False)
|
||||
|
||||
def __init__(self, minchars=0, maxchars=None, isUTF8=False):
|
||||
def __init__(self, minchars=0, maxchars=None, **kwds):
|
||||
super().__init__()
|
||||
if maxchars is None:
|
||||
maxchars = minchars or UNLIMITED
|
||||
self.set_properties(minchars=minchars, maxchars=maxchars, isUTF8=isUTF8)
|
||||
self.set_properties(minchars=minchars, maxchars=maxchars, **kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = ' ' * self.minchars
|
||||
@ -635,24 +608,13 @@ class StringType(DataType):
|
||||
except AttributeError:
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
return 'str'
|
||||
|
||||
|
||||
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
|
||||
# whereas StringType is supposed to not contain '\n'
|
||||
# unfortunately, SECoP makes no distinction here....
|
||||
# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation
|
||||
# note: content is supposed to follow the format of a git commit message,
|
||||
# i.e. a line of text, 2 '\n' + a longer explanation
|
||||
class TextType(StringType):
|
||||
"""special string type, intended for longer texts
|
||||
|
||||
:param maxchars: maximum number of characters
|
||||
|
||||
whereas StringType is supposed to not contain '\n'
|
||||
unfortunately, SECoP makes no distinction here....
|
||||
note: content is supposed to follow the format of a git commit message,
|
||||
i.e. a line of text, 2 '\n' + a longer explanation
|
||||
"""
|
||||
def __init__(self, maxchars=None):
|
||||
if maxchars is None:
|
||||
maxchars = UNLIMITED
|
||||
@ -661,7 +623,7 @@ class TextType(StringType):
|
||||
def __repr__(self):
|
||||
if self.maxchars == UNLIMITED:
|
||||
return 'TextType()'
|
||||
return 'TextType(%d)' % (self.maxchars)
|
||||
return 'TextType(%d)' % self.maxchars
|
||||
|
||||
def copy(self):
|
||||
# DataType.copy will not work, because it is exported as 'string'
|
||||
@ -669,9 +631,7 @@ class TextType(StringType):
|
||||
|
||||
|
||||
class BoolType(DataType):
|
||||
"""boolean
|
||||
|
||||
"""
|
||||
"""boolean"""
|
||||
default = False
|
||||
|
||||
def export_datatype(self):
|
||||
@ -707,9 +667,6 @@ class BoolType(DataType):
|
||||
other(False)
|
||||
other(True)
|
||||
|
||||
def short_doc(self):
|
||||
return 'bool'
|
||||
|
||||
|
||||
Stub.fix_datatypes()
|
||||
|
||||
@ -721,14 +678,12 @@ Stub.fix_datatypes()
|
||||
class ArrayOf(DataType):
|
||||
"""data structure with fields of homogeneous type
|
||||
|
||||
:param members: the datatype for all elements
|
||||
:param members: the datatype of the elements
|
||||
"""
|
||||
properties = {
|
||||
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0),
|
||||
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||
mandatory=True),
|
||||
}
|
||||
minlen = Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||
default=0)
|
||||
maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||
mandatory=True)
|
||||
|
||||
def __init__(self, members, minlen=0, maxlen=None):
|
||||
super().__init__()
|
||||
@ -759,14 +714,14 @@ class ArrayOf(DataType):
|
||||
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of members"""
|
||||
if key in self.__class__.properties:
|
||||
if key in self.propertyDict:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.members.setProperty(key, value)
|
||||
|
||||
def export_datatype(self):
|
||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||
members=self.members.export_datatype())
|
||||
members=self.members.export_datatype())
|
||||
|
||||
def __repr__(self):
|
||||
return 'ArrayOf(%s, %s, %s)' % (
|
||||
@ -818,16 +773,12 @@ class ArrayOf(DataType):
|
||||
except AttributeError:
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
return 'array of %s' % self.members.short_doc()
|
||||
|
||||
|
||||
class TupleOf(DataType):
|
||||
"""data structure with fields of inhomogeneous type
|
||||
|
||||
:param members: each argument is a datatype of an element
|
||||
types are given as positional arguments
|
||||
"""
|
||||
|
||||
def __init__(self, *members):
|
||||
super().__init__()
|
||||
if not members:
|
||||
@ -855,11 +806,10 @@ class TupleOf(DataType):
|
||||
try:
|
||||
if len(value) != len(self.members):
|
||||
raise BadValueError(
|
||||
'Illegal number of Arguments! Need %d arguments.' %
|
||||
(len(self.members)))
|
||||
'Illegal number of Arguments! Need %d arguments.' % len(self.members))
|
||||
# validate elements and return as list
|
||||
return tuple(sub(elem)
|
||||
for sub, elem in zip(self.members, value))
|
||||
for sub, elem in zip(self.members, value))
|
||||
except Exception as exc:
|
||||
raise BadValueError('Can not validate:', str(exc))
|
||||
|
||||
@ -879,19 +829,16 @@ class TupleOf(DataType):
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
return '(%s)' % (', '.join([sub.format_value(elem)
|
||||
for sub, elem in zip(self.members, value)]))
|
||||
for sub, elem in zip(self.members, value)]))
|
||||
|
||||
def compatible(self, other):
|
||||
if not isinstance(other, TupleOf):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
if len(self.members) != len(other.members) :
|
||||
if len(self.members) != len(other.members):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
for a, b in zip(self.members, other.members):
|
||||
a.compatible(b)
|
||||
|
||||
def short_doc(self):
|
||||
return 'tuple of (%s)' % ', '.join(m.short_doc() for m in self.members)
|
||||
|
||||
|
||||
class ImmutableDict(dict):
|
||||
def _no(self, *args, **kwds):
|
||||
@ -902,8 +849,8 @@ class ImmutableDict(dict):
|
||||
class StructOf(DataType):
|
||||
"""data structure with named fields
|
||||
|
||||
:param optional: (*sequence*) optional members
|
||||
:param members: each argument denotes <member name>=<member data type>
|
||||
:param optional: a list of optional members
|
||||
:param members: names as keys and types as values for all members
|
||||
"""
|
||||
def __init__(self, optional=None, **members):
|
||||
super().__init__()
|
||||
@ -919,15 +866,15 @@ class StructOf(DataType):
|
||||
if name not in members:
|
||||
raise ProgrammingError(
|
||||
'Only members of StructOf may be declared as optional!')
|
||||
self.default = dict((k,el.default) for k, el in members.items())
|
||||
self.default = dict((k, el.default) for k, el in members.items())
|
||||
|
||||
def copy(self):
|
||||
"""DataType.copy does not work when members contain enums"""
|
||||
return StructOf(self.optional, **{k: v.copy() for k,v in self.members.items()})
|
||||
return StructOf(self.optional, **{k: v.copy() for k, v in self.members.items()})
|
||||
|
||||
def export_datatype(self):
|
||||
res = dict(type='struct', members=dict((n, s.export_datatype())
|
||||
for n, s in list(self.members.items())))
|
||||
for n, s in list(self.members.items())))
|
||||
if self.optional:
|
||||
res['optional'] = self.optional
|
||||
return res
|
||||
@ -979,18 +926,11 @@ class StructOf(DataType):
|
||||
except (AttributeError, TypeError, KeyError):
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
return 'dict'
|
||||
|
||||
|
||||
class CommandType(DataType):
|
||||
"""command
|
||||
|
||||
a pseudo datatype for commands with arguments and return values
|
||||
|
||||
:param argument: None or the data type of the argument. multiple arguments may be simulated
|
||||
by TupleOf or StructOf
|
||||
:param result: None or the data type of the result
|
||||
"""
|
||||
IS_COMMAND = True
|
||||
|
||||
@ -1049,16 +989,10 @@ class CommandType(DataType):
|
||||
except AttributeError:
|
||||
raise BadValueError('incompatible datatypes')
|
||||
|
||||
def short_doc(self):
|
||||
argument = self.argument.short_doc() if self.argument else ''
|
||||
result = ' -> %s' % self.argument.short_doc() if self.result else ''
|
||||
return '(%s)%s' % (argument, result) # return argument list only
|
||||
|
||||
|
||||
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||
|
||||
class DataTypeType(DataType):
|
||||
"""DataType type"""
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is a valid datatype
|
||||
|
||||
@ -1102,9 +1036,7 @@ class ValueType(DataType):
|
||||
|
||||
|
||||
class NoneOr(DataType):
|
||||
"""validates a None or other
|
||||
|
||||
:param other: the other datatype"""
|
||||
"""validates a None or smth. else"""
|
||||
default = None
|
||||
|
||||
def __init__(self, other):
|
||||
@ -1119,16 +1051,8 @@ class NoneOr(DataType):
|
||||
return None
|
||||
return self.other.export_value(value)
|
||||
|
||||
def short_doc(self):
|
||||
other = self.other.short_doc()
|
||||
return '%s or None' % other if other else None
|
||||
|
||||
|
||||
class OrType(DataType):
|
||||
"""validates one of the
|
||||
|
||||
:param types: each argument denotes one allowed type
|
||||
"""
|
||||
def __init__(self, *types):
|
||||
super().__init__()
|
||||
self.types = types
|
||||
@ -1142,12 +1066,6 @@ class OrType(DataType):
|
||||
pass
|
||||
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
|
||||
|
||||
def short_doc(self):
|
||||
types = [t.short_doc() for t in self.types]
|
||||
if None in types:
|
||||
return None
|
||||
return ' or '.join(types)
|
||||
|
||||
|
||||
Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
|
||||
Int16 = IntRange(-(1 << 15), (1 << 15) - 1)
|
||||
@ -1161,12 +1079,6 @@ UInt64 = IntRange(0, (1 << 64) - 1)
|
||||
|
||||
# Goodie: Convenience Datatypes for Programming
|
||||
class LimitsType(TupleOf):
|
||||
"""limit (min, max) tuple
|
||||
|
||||
:param members: the type of both members
|
||||
|
||||
checks for min <= max
|
||||
"""
|
||||
def __init__(self, members):
|
||||
TupleOf.__init__(self, members, members)
|
||||
|
||||
@ -1178,22 +1090,13 @@ class LimitsType(TupleOf):
|
||||
|
||||
|
||||
class StatusType(TupleOf):
|
||||
"""SECoP status type
|
||||
|
||||
:param enum: the status code enum type
|
||||
|
||||
allows to access enum members directly
|
||||
"""
|
||||
|
||||
# shorten initialisation and allow access to status enumMembers from status values
|
||||
def __init__(self, enum):
|
||||
TupleOf.__init__(self, EnumType(enum), StringType())
|
||||
self.enum = enum
|
||||
self._enum = enum
|
||||
|
||||
def __getattr__(self, key):
|
||||
enum = TupleOf.__getattr__(self, 'enum')
|
||||
if hasattr(enum, key):
|
||||
return getattr(enum, key)
|
||||
return TupleOf.__getattr__(self, key)
|
||||
return getattr(self._enum, key)
|
||||
|
||||
|
||||
def floatargs(kwds):
|
||||
|
@ -22,7 +22,6 @@
|
||||
"""Define (internal) SECoP Errors"""
|
||||
|
||||
|
||||
|
||||
class SECoPError(RuntimeError):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
@ -138,12 +137,6 @@ def secop_error(exception):
|
||||
return InternalError(repr(exception))
|
||||
|
||||
|
||||
def fmt_error(exception):
|
||||
if isinstance(exception, SECoPError):
|
||||
return str(exception)
|
||||
return repr(exception)
|
||||
|
||||
|
||||
EXCEPTIONS = dict(
|
||||
NoSuchModule=NoSuchModuleError,
|
||||
NoSuchParameter=NoSuchParameterError,
|
||||
|
@ -24,11 +24,10 @@
|
||||
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, \
|
||||
FloatRange, StringType, StructOf, TupleOf
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.modules import Command, Parameter
|
||||
from secop.modules import Command, HasAccessibles, Parameter
|
||||
|
||||
|
||||
class Feature(metaclass=ModuleMeta):
|
||||
class Feature(HasAccessibles):
|
||||
"""all things belonging to a small, predefined functionality influencing the working of a module"""
|
||||
|
||||
|
||||
@ -39,33 +38,37 @@ class HAS_PID(Feature):
|
||||
# note: (i would still but them in the same group, though)
|
||||
# note: if extra elements are implemented in the pid struct they MUST BE
|
||||
# properly described in the description of the pid Parameter
|
||||
parameters = {
|
||||
'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ),
|
||||
'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ),
|
||||
'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True),
|
||||
'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True),
|
||||
'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True),
|
||||
'pid': Parameter('(optional) Struct of p,i,d, minimum output value',
|
||||
datatype=StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
base_output=FloatRange(0),
|
||||
), optional=True,
|
||||
), # note: struct may be extended with custom elements (names should be prefixed with '_')
|
||||
'output' : Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False),
|
||||
}
|
||||
|
||||
# parameters
|
||||
use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), )
|
||||
# pylint: disable=invalid-name
|
||||
p = Parameter('proportional part of the regulation', datatype=FloatRange(0), )
|
||||
i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True)
|
||||
d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True)
|
||||
base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True)
|
||||
pid = Parameter('(optional) Struct of p,i,d, minimum output value',
|
||||
datatype=StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
base_output=FloatRange(0),
|
||||
), optional=True,
|
||||
) # note: struct may be extended with custom elements (names should be prefixed with '_')
|
||||
output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False)
|
||||
|
||||
|
||||
|
||||
class Has_PIDTable(HAS_PID):
|
||||
parameters = {
|
||||
'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)),
|
||||
'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
||||
StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
_heater_range=FloatRange(0),
|
||||
_base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange'
|
||||
}
|
||||
|
||||
# parameters
|
||||
use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1))
|
||||
pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0),
|
||||
StructOf(p=FloatRange(0),
|
||||
i=FloatRange(0),
|
||||
d=FloatRange(0),
|
||||
_heater_range=FloatRange(0),
|
||||
_base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange'
|
||||
|
||||
|
||||
|
||||
|
||||
class HAS_Persistent(Feature):
|
||||
@ -75,89 +78,98 @@ class HAS_Persistent(Feature):
|
||||
# 'coupled' : Status.BUSY+2, # to be discussed.
|
||||
# 'decoupling' : Status.BUSY+3, # to be discussed.
|
||||
#}
|
||||
parameters = {
|
||||
'persistent_mode': Parameter('Use persistent mode',
|
||||
datatype=EnumType(off=0,on=1),
|
||||
default=0, readonly=False),
|
||||
'is_persistent': Parameter('current state of persistence',
|
||||
datatype=BoolType(), optional=True),
|
||||
'stored_value': Parameter('current persistence value, often used as the modules value',
|
||||
datatype='main', unit='$', optional=True),
|
||||
'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||
datatype='main', unit='$' ),
|
||||
}
|
||||
|
||||
# parameters
|
||||
persistent_mode = Parameter('Use persistent mode',
|
||||
datatype=EnumType(off=0,on=1),
|
||||
default=0, readonly=False)
|
||||
is_persistent = Parameter('current state of persistence',
|
||||
datatype=BoolType(), optional=True)
|
||||
stored_value = Parameter('current persistence value, often used as the modules value',
|
||||
datatype='main', unit='$', optional=True)
|
||||
driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)',
|
||||
datatype='main', unit='$' )
|
||||
|
||||
|
||||
|
||||
class HAS_Tolerance(Feature):
|
||||
# detects IDLE status by checking if the value lies in a given window:
|
||||
# tolerance is the maximum allowed deviation from target, value must lie in this interval
|
||||
# for at least ´timewindow´ seconds.
|
||||
parameters = {
|
||||
'tolerance': Parameter('Half height of the Window',
|
||||
datatype=FloatRange(0), default=1, unit='$'),
|
||||
'timewindow': Parameter('Length of the timewindow to check',
|
||||
datatype=FloatRange(0), default=30, unit='s',
|
||||
optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
tolerance = Parameter('Half height of the Window',
|
||||
datatype=FloatRange(0), default=1, unit='$')
|
||||
timewindow = Parameter('Length of the timewindow to check',
|
||||
datatype=FloatRange(0), default=30, unit='s',
|
||||
optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_Timeout(Feature):
|
||||
parameters = {
|
||||
'timeout': Parameter('timeout for movement',
|
||||
datatype=FloatRange(0), default=0, unit='s'),
|
||||
}
|
||||
|
||||
# parameters
|
||||
timeout = Parameter('timeout for movement',
|
||||
datatype=FloatRange(0), default=0, unit='s')
|
||||
|
||||
|
||||
|
||||
class HAS_Pause(Feature):
|
||||
# just a proposal, can't agree on it....
|
||||
parameters = {
|
||||
'pause': Command('pauses movement', argument=None, result=None),
|
||||
'go': Command('continues movement or start a new one if target was change since the last pause',
|
||||
argument=None, result=None),
|
||||
}
|
||||
|
||||
@Command(argument=None, result=None)
|
||||
def pause(self):
|
||||
"""pauses movement"""
|
||||
|
||||
@Command(argument=None, result=None)
|
||||
def go(self):
|
||||
"""continues movement or start a new one if target was change since the last pause"""
|
||||
|
||||
|
||||
class HAS_Ramp(Feature):
|
||||
parameters = {
|
||||
'ramp': Parameter('speed of movement', unit='$/min',
|
||||
datatype=FloatRange(0)),
|
||||
'use_ramp': Parameter('use the ramping of the setpoint, or jump',
|
||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||
optional=True),
|
||||
'setpoint': Parameter('currently active setpoint',
|
||||
datatype=FloatRange(0), unit='$',
|
||||
readonly=True, ),
|
||||
}
|
||||
|
||||
# parameters
|
||||
ramp =Parameter('speed of movement', unit='$/min',
|
||||
datatype=FloatRange(0))
|
||||
use_ramp = Parameter('use the ramping of the setpoint, or jump',
|
||||
datatype=EnumType(disable_ramp=0, use_ramp=1),
|
||||
optional=True)
|
||||
setpoint = Parameter('currently active setpoint',
|
||||
datatype=FloatRange(0), unit='$',
|
||||
readonly=True, )
|
||||
|
||||
|
||||
|
||||
class HAS_Speed(Feature):
|
||||
parameters = {
|
||||
'speed' : Parameter('(maximum) speed of movement (of the main value)',
|
||||
unit='$/s', datatype=FloatRange(0)),
|
||||
}
|
||||
|
||||
# parameters
|
||||
speed = Parameter('(maximum) speed of movement (of the main value)',
|
||||
unit='$/s', datatype=FloatRange(0))
|
||||
|
||||
|
||||
|
||||
class HAS_Accel(HAS_Speed):
|
||||
parameters = {
|
||||
'accel' : Parameter('acceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0)),
|
||||
'decel' : Parameter('deceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0), optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
accel = Parameter('acceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0))
|
||||
decel = Parameter('deceleration of movement', unit='$/s^2',
|
||||
datatype=FloatRange(0), optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_MotorCurrents(Feature):
|
||||
parameters = {
|
||||
'movecurrent' : Parameter('Current while moving',
|
||||
datatype=FloatRange(0)),
|
||||
'idlecurrent' : Parameter('Current while idle',
|
||||
datatype=FloatRange(0), optional=True),
|
||||
}
|
||||
|
||||
# parameters
|
||||
movecurrent = Parameter('Current while moving',
|
||||
datatype=FloatRange(0))
|
||||
idlecurrent = Parameter('Current while idle',
|
||||
datatype=FloatRange(0), optional=True)
|
||||
|
||||
|
||||
|
||||
class HAS_Curve(Feature):
|
||||
# proposed, not yet agreed upon!
|
||||
parameters = {
|
||||
'curve' : Parameter('Calibration curve', datatype=StringType(80), default='<unset>'),
|
||||
# XXX: tbd. (how to upload/download/select a curve?)
|
||||
}
|
||||
|
||||
# parameters
|
||||
curve = Parameter('Calibration curve', datatype=StringType(80), default='<unset>')
|
||||
|
@ -21,13 +21,13 @@
|
||||
# *****************************************************************************
|
||||
|
||||
import configparser
|
||||
from configparser import NoOptionError
|
||||
from collections import OrderedDict
|
||||
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
||||
from secop.gui.cfg_editor.utils import get_all_items, get_params, get_props,\
|
||||
get_all_children_with_names, get_module_class_from_name, \
|
||||
get_interface_class_from_name
|
||||
from configparser import NoOptionError
|
||||
|
||||
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
||||
from secop.gui.cfg_editor.utils import get_all_children_with_names, \
|
||||
get_all_items, get_interface_class_from_name, \
|
||||
get_module_class_from_name, get_params, get_props
|
||||
|
||||
NODE = 'node'
|
||||
INTERFACE = 'interface'
|
||||
@ -58,7 +58,7 @@ def write_config(file_name, tree_widget):
|
||||
value = value.replace('\n\n', '\n.\n')
|
||||
value = value.replace('\n', '\n ')
|
||||
itm_lines[id(itm)] = '[%s %s]\n' % (itm.kind, itm.name) +\
|
||||
value_str % (SECTIONS[itm.kind], value)
|
||||
value_str % (SECTIONS[itm.kind], value)
|
||||
# TODO params and props
|
||||
elif itm.kind == PARAMETER and value:
|
||||
itm_lines[id(itm)] = value_str % (itm.name, value)
|
||||
@ -142,7 +142,7 @@ def read_config(file_path):
|
||||
else:
|
||||
param.addChild(TreeWidgetItem(PROPERTY,
|
||||
separated[1], get_value(config, section,
|
||||
option)))
|
||||
option)))
|
||||
node = get_comments(node, ifs, mods, file_path)
|
||||
return node, ifs, mods
|
||||
|
||||
|
@ -21,11 +21,11 @@
|
||||
# *****************************************************************************
|
||||
|
||||
import os
|
||||
from secop.gui.qt import QMainWindow, QMessageBox
|
||||
from secop.gui.cfg_editor.node_display import NodeDisplay
|
||||
from secop.gui.cfg_editor.utils import loadUi, get_file_paths
|
||||
from secop.gui.cfg_editor.widgets import TabBar
|
||||
|
||||
from secop.gui.cfg_editor.node_display import NodeDisplay
|
||||
from secop.gui.cfg_editor.utils import get_file_paths, loadUi
|
||||
from secop.gui.cfg_editor.widgets import TabBar
|
||||
from secop.gui.qt import QMainWindow, QMessageBox
|
||||
|
||||
# TODO move secop mainwinodw to gui/client and all specific stuff
|
||||
NODE = 'node'
|
||||
|
@ -20,8 +20,8 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from secop.gui.qt import QWidget, Qt, QHBoxLayout, QSpacerItem, QSizePolicy
|
||||
from secop.gui.cfg_editor.utils import loadUi
|
||||
from secop.gui.qt import QHBoxLayout, QSizePolicy, QSpacerItem, Qt, QWidget
|
||||
|
||||
|
||||
class NodeDisplay(QWidget):
|
||||
|
@ -20,10 +20,11 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from secop.gui.qt import QTreeWidgetItem, QFont, QWidget, QVBoxLayout, QLabel, \
|
||||
QHBoxLayout, QPushButton, QSize, QSizePolicy, QDialog, QTextEdit, pyqtSignal
|
||||
from secop.gui.cfg_editor.utils import setTreeIcon, setIcon, loadUi, \
|
||||
set_name_edit_style
|
||||
from secop.gui.cfg_editor.utils import loadUi, \
|
||||
set_name_edit_style, setIcon, setTreeIcon
|
||||
from secop.gui.qt import QDialog, QFont, QHBoxLayout, \
|
||||
QLabel, QPushButton, QSize, QSizePolicy, QTextEdit, \
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
|
||||
from secop.gui.valuewidgets import get_widget
|
||||
from secop.properties import Property
|
||||
|
||||
|
@ -20,15 +20,16 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from os import path, listdir
|
||||
import sys
|
||||
import inspect
|
||||
from secop.gui.qt import uic, QIcon, QSize, QFileDialog, QDialogButtonBox
|
||||
from secop.server import getGeneralConfig
|
||||
import sys
|
||||
from os import listdir, path
|
||||
|
||||
from secop.gui.qt import QDialogButtonBox, QFileDialog, QIcon, QSize, uic
|
||||
from secop.modules import Module
|
||||
from secop.params import Parameter
|
||||
from secop.properties import Property
|
||||
from secop.protocol.interface.tcp import TCPServer
|
||||
from secop.server import getGeneralConfig
|
||||
|
||||
uipath = path.dirname(__file__)
|
||||
|
||||
|
@ -23,15 +23,15 @@
|
||||
|
||||
import os
|
||||
|
||||
from secop.gui.cfg_editor.config_file import write_config, read_config
|
||||
from secop.gui.cfg_editor.config_file import read_config, write_config
|
||||
from secop.gui.cfg_editor.tree_widget_item import TreeWidgetItem
|
||||
from secop.gui.cfg_editor.utils import get_file_paths, get_modules, \
|
||||
get_interfaces, loadUi, set_name_edit_style, get_module_class_from_name, \
|
||||
get_all_items, get_interface_class_from_name, get_params, get_props, \
|
||||
setActionIcon
|
||||
from secop.gui.qt import QWidget, QDialog, QLabel, QTabBar, Qt, QPoint, QMenu, \
|
||||
QTreeWidget, QSize, pyqtSignal, QLineEdit, QComboBox, QDialogButtonBox, \
|
||||
QTextEdit, QTreeView, QStandardItemModel, QStandardItem
|
||||
from secop.gui.cfg_editor.utils import get_all_items, \
|
||||
get_file_paths, get_interface_class_from_name, get_interfaces, \
|
||||
get_module_class_from_name, get_modules, get_params, \
|
||||
get_props, loadUi, set_name_edit_style, setActionIcon
|
||||
from secop.gui.qt import QComboBox, QDialog, QDialogButtonBox, QLabel, \
|
||||
QLineEdit, QMenu, QPoint, QSize, QStandardItem, QStandardItemModel, \
|
||||
Qt, QTabBar, QTextEdit, QTreeView, QTreeWidget, QWidget, pyqtSignal
|
||||
|
||||
NODE = 'node'
|
||||
MODULE = 'module'
|
||||
|
@ -26,9 +26,9 @@ import secop.client
|
||||
from secop.gui.modulectrl import ModuleCtrl
|
||||
from secop.gui.nodectrl import NodeCtrl
|
||||
from secop.gui.paramview import ParameterView
|
||||
from secop.gui.qt import QInputDialog, QMainWindow, QMessageBox, \
|
||||
QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot, QBrush, QColor
|
||||
from secop.gui.util import loadUi, Value
|
||||
from secop.gui.qt import QBrush, QColor, QInputDialog, QMainWindow, \
|
||||
QMessageBox, QObject, QTreeWidgetItem, pyqtSignal, pyqtSlot
|
||||
from secop.gui.util import Value, loadUi
|
||||
from secop.lib import formatExtendedTraceback
|
||||
|
||||
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
|
||||
@ -90,7 +90,7 @@ class QSECNode(QObject):
|
||||
|
||||
def queryCache(self, module):
|
||||
return {k: Value(*self.conn.cache[(module, k)])
|
||||
for k in self.modules[module]['parameters']}
|
||||
for k in self.modules[module]['parameters']}
|
||||
|
||||
def syncCommunicate(self, action, ident='', data=None):
|
||||
reply = self.conn.request(action, ident, data)
|
||||
|
@ -36,19 +36,19 @@ class CommandDialog(QDialog):
|
||||
loadUi(self, 'cmddialog.ui')
|
||||
|
||||
self.setWindowTitle('Arguments for %s' % cmdname)
|
||||
#row = 0
|
||||
# row = 0
|
||||
|
||||
self._labels = []
|
||||
self.widgets = []
|
||||
# improve! recursive?
|
||||
dtype = argument
|
||||
l = QLabel(repr(dtype))
|
||||
l.setWordWrap(True)
|
||||
w = get_widget(dtype, readonly=False)
|
||||
self.gridLayout.addWidget(l, 0, 0)
|
||||
self.gridLayout.addWidget(w, 0, 1)
|
||||
self._labels.append(l)
|
||||
self.widgets.append(w)
|
||||
label = QLabel(repr(dtype))
|
||||
label.setWordWrap(True)
|
||||
widget = get_widget(dtype, readonly=False)
|
||||
self.gridLayout.addWidget(label, 0, 0)
|
||||
self.gridLayout.addWidget(widget, 0, 1)
|
||||
self._labels.append(label)
|
||||
self.widgets.append(widget)
|
||||
|
||||
self.gridLayout.setRowStretch(1, 1)
|
||||
self.setModal(True)
|
||||
|
@ -25,14 +25,15 @@
|
||||
import json
|
||||
import pprint
|
||||
from time import sleep
|
||||
|
||||
import mlzlog
|
||||
|
||||
import secop.lib
|
||||
from secop.datatypes import EnumType, StringType
|
||||
from secop.errors import SECoPError
|
||||
from secop.gui.qt import QFont, QFontMetrics, QLabel, \
|
||||
QMessageBox, QTextCursor, QWidget, pyqtSlot, toHtmlEscaped
|
||||
from secop.gui.util import loadUi, Value
|
||||
import secop.lib
|
||||
from secop.gui.util import Value, loadUi
|
||||
|
||||
|
||||
class NodeCtrl(QWidget):
|
||||
@ -167,7 +168,6 @@ class NodeCtrl(QWidget):
|
||||
print(secop.lib.formatExtendedTraceback())
|
||||
widget = QLabel('Bad configured Module %s! (%s)' % (modname, e))
|
||||
|
||||
|
||||
if unit:
|
||||
labelstr = '%s (%s):' % (modname, unit)
|
||||
else:
|
||||
@ -289,7 +289,7 @@ class DrivableWidget(ReadableWidget):
|
||||
|
||||
def update_current(self, value):
|
||||
self.currentLineEdit.setText(str(value))
|
||||
#elif self._is_enum:
|
||||
# elif self._is_enum:
|
||||
# member = self._map[self._revmap[value.value]]
|
||||
# self.currentLineEdit.setText('%s.%s (%d)' % (member.enum.name, member.name, member.value))
|
||||
|
||||
|
@ -22,12 +22,9 @@
|
||||
# *****************************************************************************
|
||||
|
||||
|
||||
from secop.datatypes import EnumType, FloatRange, IntRange
|
||||
from secop.gui.qt import QPushButton as QButton
|
||||
from secop.gui.qt import QCheckBox, QLabel, QLineEdit, \
|
||||
QMessageBox, QSizePolicy, Qt, QWidget, pyqtSignal, pyqtSlot
|
||||
from secop.datatypes import EnumType
|
||||
from secop.gui.qt import QWidget, pyqtSignal, pyqtSlot
|
||||
from secop.gui.util import loadUi
|
||||
from secop.lib import formatExtendedStack
|
||||
|
||||
|
||||
class ParameterWidget(QWidget):
|
||||
|
@ -32,6 +32,7 @@ uipath = path.dirname(__file__)
|
||||
def loadUi(widget, uiname, subdir='ui'):
|
||||
uic.loadUi(path.join(uipath, subdir, uiname), widget)
|
||||
|
||||
|
||||
class Value:
|
||||
def __init__(self, value, timestamp=None, readerror=None):
|
||||
self.value = value
|
||||
|
@ -23,12 +23,13 @@
|
||||
|
||||
|
||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||
FloatRange, IntRange, StringType, StructOf, TupleOf, TextType
|
||||
from secop.gui.qt import QCheckBox, QComboBox, QDialog, QDoubleSpinBox, \
|
||||
QFrame, QGridLayout, QGroupBox, QLabel, QLineEdit, QSpinBox, QVBoxLayout, \
|
||||
QTextEdit
|
||||
FloatRange, IntRange, StringType, StructOf, TextType, TupleOf
|
||||
from secop.gui.qt import QCheckBox, QComboBox, QDialog, \
|
||||
QDoubleSpinBox, QFrame, QGridLayout, QGroupBox, \
|
||||
QLabel, QLineEdit, QSpinBox, QTextEdit, QVBoxLayout
|
||||
from secop.gui.util import loadUi
|
||||
|
||||
|
||||
# XXX: implement live validators !!!!
|
||||
# XXX: signals upon change of value
|
||||
# XXX: honor readonly in all cases!
|
||||
@ -171,12 +172,12 @@ class StructWidget(QGroupBox):
|
||||
self._labels = []
|
||||
for idx, name in enumerate(sorted(datatype.members)):
|
||||
dt = datatype.members[name]
|
||||
w = get_widget(dt, readonly=readonly, parent=self)
|
||||
l = QLabel(name)
|
||||
self.layout.addWidget(l, idx, 0)
|
||||
self.layout.addWidget(w, idx, 1)
|
||||
self._labels.append(l)
|
||||
self.subwidgets[name] = (w, dt)
|
||||
widget = get_widget(dt, readonly=readonly, parent=self)
|
||||
label = QLabel(name)
|
||||
self.layout.addWidget(label, idx, 0)
|
||||
self.layout.addWidget(widget, idx, 1)
|
||||
self._labels.append(label)
|
||||
self.subwidgets[name] = (widget, dt)
|
||||
self.datatypes.append(dt)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
@ -215,21 +216,22 @@ class ArrayWidget(QGroupBox):
|
||||
w.set_value(v)
|
||||
|
||||
|
||||
|
||||
def get_widget(datatype, readonly=False, parent=None):
|
||||
return {FloatRange: FloatWidget,
|
||||
IntRange: IntWidget,
|
||||
StringType: StringWidget,
|
||||
TextType: TextWidget,
|
||||
BLOBType: BlobWidget,
|
||||
EnumType: EnumWidget,
|
||||
BoolType: BoolWidget,
|
||||
TupleOf: TupleWidget,
|
||||
StructOf: StructWidget,
|
||||
ArrayOf: ArrayWidget,
|
||||
return {
|
||||
FloatRange: FloatWidget,
|
||||
IntRange: IntWidget,
|
||||
StringType: StringWidget,
|
||||
TextType: TextWidget,
|
||||
BLOBType: BlobWidget,
|
||||
EnumType: EnumWidget,
|
||||
BoolType: BoolWidget,
|
||||
TupleOf: TupleWidget,
|
||||
StructOf: StructWidget,
|
||||
ArrayOf: ArrayWidget,
|
||||
}.get(datatype.__class__)(datatype, readonly, parent)
|
||||
# TODO: handle NoneOr
|
||||
|
||||
|
||||
class msg(QDialog):
|
||||
def __init__(self, stuff, parent=None):
|
||||
super(msg, self).__init__(parent)
|
||||
@ -242,7 +244,7 @@ class msg(QDialog):
|
||||
dt = StructOf(i=IntRange(0, 10), f=FloatRange(), b=BoolType())
|
||||
w = StructWidget(dt)
|
||||
self.gridLayout.addWidget(w, row, 1)
|
||||
row+=1
|
||||
row += 1
|
||||
|
||||
self.gridLayout.addWidget(QLabel('stuff'), row, 0, 1, 0)
|
||||
row += 1 # at pos (0,0) span 2 cols, 1 row
|
||||
|
@ -54,8 +54,8 @@ method has to be called explicitly int the write_<parameter> method, if needed.
|
||||
"""
|
||||
import re
|
||||
|
||||
from secop.metaclass import Done
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.modules import Done
|
||||
|
||||
|
||||
class CmdParser:
|
||||
@ -202,13 +202,19 @@ class IOHandler(IOHandlerBase):
|
||||
:param replyfmt: the format for reading the reply with some scanf like behaviour
|
||||
:param changecmd: the first part of the change command (without values), may be
|
||||
omitted if no write happens
|
||||
|
||||
"""
|
||||
"""
|
||||
CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands
|
||||
CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator
|
||||
|
||||
def __init__(self, group, querycmd, replyfmt, changecmd=None):
|
||||
"""initialize the IO handler"""
|
||||
"""initialize the IO handler
|
||||
|
||||
group: the handler group (used for analyze_<group> and change_<group>)
|
||||
querycmd: the command for a query, may contain named formats for cmdargs
|
||||
replyfmt: the format for reading the reply with some scanf like behaviour
|
||||
changecmd: the first part of the change command (without values), may be
|
||||
omitted if no write happens
|
||||
"""
|
||||
self.group = group
|
||||
self.parameters = set()
|
||||
self._module_class = None
|
||||
|
@ -21,13 +21,13 @@
|
||||
# *****************************************************************************
|
||||
"""Define helpers"""
|
||||
|
||||
import importlib
|
||||
import linecache
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
import importlib
|
||||
from os import path, environ
|
||||
from os import environ, path
|
||||
|
||||
repodir = path.abspath(path.join(path.dirname(__file__), '..', '..'))
|
||||
|
||||
@ -58,6 +58,7 @@ CONFIG['basedir'] = repodir
|
||||
|
||||
unset_value = object()
|
||||
|
||||
|
||||
class lazy_property:
|
||||
"""A property that calculates its value only once."""
|
||||
|
||||
|
@ -28,16 +28,18 @@ support for asynchronous communication, but may be used also for
|
||||
synchronous IO (see secop.stringio.StringIO)
|
||||
"""
|
||||
|
||||
import socket
|
||||
import select
|
||||
import time
|
||||
import ast
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
|
||||
from secop.errors import CommunicationFailedError, ConfigError
|
||||
from secop.lib import closeSocket, parseHostPort, tcpSocket
|
||||
|
||||
try:
|
||||
from serial import Serial
|
||||
except ImportError:
|
||||
Serial = None
|
||||
from secop.lib import parseHostPort, tcpSocket, closeSocket
|
||||
from secop.errors import ConfigError, CommunicationFailedError
|
||||
|
||||
|
||||
class ConnectionClosed(ConnectionError):
|
||||
@ -60,10 +62,10 @@ class AsynConn:
|
||||
except (ValueError, TypeError, AssertionError):
|
||||
if 'COM' in uri:
|
||||
raise ValueError("the correct uri for a COM port is: "
|
||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'" )
|
||||
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'")
|
||||
if '/dev' in uri:
|
||||
raise ValueError("the correct uri for a serial port is: "
|
||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'" )
|
||||
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'")
|
||||
raise ValueError('invalid uri: %s' % uri)
|
||||
iocls = cls.SCHEME_MAP['tcp']
|
||||
uri = 'tcp://%s:%d' % host_port
|
||||
|
@ -20,57 +20,168 @@
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from inspect import cleandoc
|
||||
from textwrap import indent
|
||||
|
||||
from secop.modules import Command, HasProperties, Module, Parameter, Property
|
||||
|
||||
|
||||
def indent_description(p):
|
||||
"""indent lines except first one"""
|
||||
return indent(p.description, ' ').replace(' ', '', 1)
|
||||
return indent(p.description, ' ').replace(' ', '', 1)
|
||||
|
||||
|
||||
def append_to_doc(cls, name, title, attrname, newitems, fmtfunc):
|
||||
def fmt_param(name, param):
|
||||
desc = indent_description(param)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [short_doc(param.datatype), 'rd' if param.readonly else 'wr',
|
||||
None if param.export else 'hidden']
|
||||
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
def fmt_command(name, command):
|
||||
desc = indent_description(command)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = '' # note: we expect that desc contains argument list
|
||||
else:
|
||||
dtinfo = '*%s*' % short_doc(command.datatype) + ' -%s ' % ('' if command.export else ' *(hidden)*')
|
||||
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
def fmt_property(name, prop):
|
||||
desc = indent_description(prop)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [short_doc(prop.datatype), None if prop.export else 'hidden']
|
||||
dtinfo = ', '.join(filter(None, dtinfo))
|
||||
if dtinfo:
|
||||
dtinfo = '*(%s)* ' % dtinfo
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
|
||||
SIMPLETYPES = {
|
||||
'FloatRange': 'float',
|
||||
'ScaledInteger': 'float',
|
||||
'IntRange': 'int',
|
||||
'BlobType': 'bytes',
|
||||
'StringType': 'str',
|
||||
'TextType': 'str',
|
||||
'BoolType': 'bool',
|
||||
'StructOf': 'dict',
|
||||
}
|
||||
|
||||
|
||||
def short_doc(datatype):
|
||||
# pylint: disable=possibly-unused-variable
|
||||
|
||||
def doc_EnumType(dt):
|
||||
return 'one of %s' % str(tuple(dt._enum.keys()))
|
||||
|
||||
def doc_ArrayOf(dt):
|
||||
return 'array of %s' % short_doc(dt.members)
|
||||
|
||||
def doc_TupleOf(dt):
|
||||
return 'tuple of (%s)' % ', '.join(short_doc(m) for m in dt.members)
|
||||
|
||||
def doc_CommandType(dt):
|
||||
argument = short_doc(dt.argument) if dt.argument else ''
|
||||
result = ' -> %s' % short_doc(dt.result) if dt.result else ''
|
||||
return '(%s)%s' % (argument, result) # return argument list only
|
||||
|
||||
def doc_NoneOr(dt):
|
||||
other = short_doc(dt.other)
|
||||
return '%s or None' % other if other else None
|
||||
|
||||
def doc_OrType(dt):
|
||||
types = [short_doc(t) for t in dt.types]
|
||||
if None in types: # type is anyway broad: no doc
|
||||
return None
|
||||
return ' or '.join(types)
|
||||
|
||||
def doc_Stub(dt):
|
||||
return dt.name.replace('Type', '').replace('Range', '').lower()
|
||||
|
||||
clsname = datatype.__class__.__name__
|
||||
result = SIMPLETYPES.get(clsname)
|
||||
if result:
|
||||
return result
|
||||
fun = locals().get('doc_' + clsname)
|
||||
if fun:
|
||||
return fun(datatype)
|
||||
return None # broad type like ValueType: no doc
|
||||
|
||||
|
||||
def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc):
|
||||
"""add information about some items to the doc
|
||||
|
||||
:param cls: the class with the doc string to be extended
|
||||
:param name: the name of the attribute dict to be used
|
||||
:param title: the title to be used
|
||||
:param newitems: the set of new items defined for this class
|
||||
:param lines: content of the docstring, as lines
|
||||
:param itemcls: the class of the attribute to be collected, a tuple of classes is also allowed.
|
||||
:param attrname: the name of the attribute dict to look for
|
||||
:param name: the name of the items to be collected (used for the title and for the tags)
|
||||
:param fmtfunc: a function returning a formatted item to be displayed, including line feed at end
|
||||
or an empty string to suppress output for this item
|
||||
:type fmtfunc: function(key, value)
|
||||
|
||||
rules, assuming name='properties':
|
||||
|
||||
- if the docstring contains ``{properties}``, new properties are inserted here
|
||||
- if the docstring contains ``{all properties}``, all properties are inserted here
|
||||
- if the docstring contains ``{no properties}``, no properties are inserted
|
||||
|
||||
only the first appearance of a tag above is considered
|
||||
"""
|
||||
doc = cleandoc(cls.__doc__ or '')
|
||||
doc = '\n'.join(lines)
|
||||
title = 'SECoP %s' % name.title()
|
||||
allitems = getattr(cls, attrname, {})
|
||||
fmtdict = {n: fmtfunc(n, p) or ' - **%s** *removed*\n' % n for n, p in allitems.items()}
|
||||
fmtdict = {n: fmtfunc(n, p) for n, p in allitems.items() if isinstance(p, itemcls)}
|
||||
head, _, tail = doc.partition('{all %s}' % name)
|
||||
clsset = set()
|
||||
if tail: # take all
|
||||
inherited = set()
|
||||
fmted = ''.join(fmtdict.values())
|
||||
fmted = fmtdict.values()
|
||||
else:
|
||||
inherited = {n: p for n, p in allitems.items() if fmtdict.get(n) and n not in newitems}
|
||||
fmted = ''.join(' ' + v for k, v in fmtdict.items() if k in newitems)
|
||||
head, _, tail = doc.partition('{%s}' % name)
|
||||
if not tail:
|
||||
head, _, tail = doc.partition('{no %s}' % name)
|
||||
if tail: # add no information
|
||||
return
|
||||
# no tag found: append to the end
|
||||
if fmted:
|
||||
clsset = set()
|
||||
for name in inherited:
|
||||
p = allitems[name]
|
||||
refcls = cls
|
||||
|
||||
fmted = []
|
||||
for key, formatted_item in fmtdict.items():
|
||||
if not formatted_item:
|
||||
continue
|
||||
# find where item is defined or modified
|
||||
refcls = None
|
||||
for base in cls.__mro__:
|
||||
dp = getattr(base, attrname, {}).get(name)
|
||||
if dp:
|
||||
if dp == p:
|
||||
p = getattr(base, attrname, {}).get(key)
|
||||
if isinstance(p, itemcls):
|
||||
if fmtfunc(key, p) == formatted_item:
|
||||
refcls = base
|
||||
else:
|
||||
break
|
||||
clsset.add(refcls)
|
||||
clsset.discard(cls)
|
||||
if refcls == cls:
|
||||
# definition in cls is new or modified
|
||||
fmted.append(formatted_item)
|
||||
else:
|
||||
# definition of last modification in refcls
|
||||
clsset.add(refcls)
|
||||
if fmted:
|
||||
if clsset:
|
||||
fmted += ' - see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||
for c in cls.__mro__ if c in clsset))
|
||||
cls.__doc__ = '%s\n\n:%s: %s\n%s' % (head, title, fmted, tail)
|
||||
fmted.append('- see also %s\n' % (', '.join(':class:`%s.%s`' % (c.__module__, c.__name__)
|
||||
for c in cls.__mro__ if c in clsset)))
|
||||
|
||||
doc = '%s\n\n:%s: %s\n\n%s' % (head, title, ' '.join(fmted), tail)
|
||||
lines[:] = doc.split('\n')
|
||||
|
||||
|
||||
def class_doc_handler(app, what, name, cls, options, lines):
|
||||
if what == 'class':
|
||||
if issubclass(cls, HasProperties):
|
||||
append_to_doc(cls, lines, Property, 'properties', 'propertyDict', fmt_property)
|
||||
if issubclass(cls, Module):
|
||||
append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param)
|
||||
append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command)
|
||||
|
@ -32,6 +32,7 @@ class EnumMember:
|
||||
has an int-type value and attributes 'name' and 'value'
|
||||
"""
|
||||
__slots__ = ['name', 'value', 'enum']
|
||||
|
||||
def __init__(self, enum, name, value):
|
||||
if not isinstance(enum, Enum):
|
||||
raise TypeError('1st Argument must be an instance of class Enum()')
|
||||
@ -49,7 +50,7 @@ class EnumMember:
|
||||
try:
|
||||
other = int(other)
|
||||
except Exception:
|
||||
#raise TypeError('%r can not be compared to %r!' %(other, self))
|
||||
# raise TypeError('%r can not be compared to %r!' %(other, self))
|
||||
return -1 # XXX:!
|
||||
if self.value < other:
|
||||
return -1
|
||||
@ -59,10 +60,12 @@ class EnumMember:
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == -1
|
||||
|
||||
def __le__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) < 1
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, (EnumMember)):
|
||||
if isinstance(other, EnumMember):
|
||||
return other.value == self.value
|
||||
if isinstance(other, int):
|
||||
return other == self.value
|
||||
@ -72,10 +75,13 @@ class EnumMember:
|
||||
return self.name == other
|
||||
return False
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 0
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) > -1
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.__cmp__(other.value if isinstance(other, EnumMember) else other) == 1
|
||||
|
||||
@ -100,77 +106,105 @@ class EnumMember:
|
||||
def __repr__(self):
|
||||
return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value)
|
||||
|
||||
|
||||
# numeric operations: delegate to int. Do we really need any of those?
|
||||
def __add__(self, other):
|
||||
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __sub__(self, other):
|
||||
return self.value.__sub__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self.value.__mul__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self.value.__truediv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __floordiv__(self, other):
|
||||
return self.value.__floordiv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __mod__(self, other):
|
||||
return self.value.__mod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __divmod__(self, other):
|
||||
return self.value.__divmod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __pow__(self, other, *args):
|
||||
return self.value.__pow__(other, *args)
|
||||
|
||||
def __lshift__(self, other):
|
||||
return self.value.__lshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rshift__(self, other):
|
||||
return self.value.__rshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.value.__radd__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.value.__rsub__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self.value.__rmul__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self.value.__rtruediv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rfloordiv__(self, other):
|
||||
return self.value.__rfloordiv__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rmod__(self, other):
|
||||
return self.value.__rmod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rdivmod__(self, other):
|
||||
return self.value.__rdivmod__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rpow__(self, other, *args):
|
||||
return self.value.__rpow__(other, *args)
|
||||
|
||||
def __rlshift__(self, other):
|
||||
return self.value.__rlshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rrshift__(self, other):
|
||||
return self.value.__rrshift__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
# logical operations
|
||||
def __and__(self, other):
|
||||
return self.value.__and__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __xor__(self, other):
|
||||
return self.value.__xor__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __or__(self, other):
|
||||
return self.value.__or__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rand__(self, other):
|
||||
return self.value.__rand__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __rxor__(self, other):
|
||||
return self.value.__rxor__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self.value.__ror__(other.value if isinstance(other, EnumMember) else other)
|
||||
|
||||
# other stuff
|
||||
def __neg__(self):
|
||||
return self.value.__neg__()
|
||||
|
||||
def __pos__(self):
|
||||
return self.value.__pos__()
|
||||
|
||||
def __abs__(self):
|
||||
return self.value.__abs__()
|
||||
|
||||
def __invert__(self):
|
||||
return self.value.__invert__()
|
||||
|
||||
def __int__(self):
|
||||
return self.value.__int__()
|
||||
|
||||
def __float__(self):
|
||||
return self.value.__float__()
|
||||
#return NotImplemented # makes no sense
|
||||
|
||||
def __index__(self):
|
||||
return self.value.__index__()
|
||||
|
||||
@ -206,6 +240,7 @@ class Enum(dict):
|
||||
You only can create an extended Enum.
|
||||
"""
|
||||
name = ''
|
||||
|
||||
def __init__(self, name='', parent=None, **kwds):
|
||||
super(Enum, self).__init__()
|
||||
if isinstance(name, (dict, Enum)) and parent is None:
|
||||
@ -217,7 +252,7 @@ class Enum(dict):
|
||||
# if name was not given, use that of the parent
|
||||
# this means, an extended Enum behaves like the parent
|
||||
# THIS MAY BE CONFUSING SOMETIMES!
|
||||
name=parent.name
|
||||
name = parent.name
|
||||
# else:
|
||||
# raise TypeError('Enum instances need a name or an Enum parent!')
|
||||
if not isinstance(name, str):
|
||||
@ -225,8 +260,9 @@ class Enum(dict):
|
||||
|
||||
names = set()
|
||||
values = set()
|
||||
|
||||
# pylint: disable=dangerous-default-value
|
||||
def add(self, k, v, names = names, value = values):
|
||||
def add(self, k, v, names=names, value=values):
|
||||
"""helper for creating the enum members"""
|
||||
if v is None:
|
||||
# sugar: take the next free number if value was None
|
||||
@ -237,7 +273,7 @@ class Enum(dict):
|
||||
if v in names:
|
||||
v = self[v].value
|
||||
while v in values:
|
||||
v +=1
|
||||
v += 1
|
||||
|
||||
# check that the value is an int
|
||||
_v = int(v)
|
||||
@ -290,7 +326,6 @@ class Enum(dict):
|
||||
|
||||
def __repr__(self):
|
||||
return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members))
|
||||
# return '<Enum %r (%d values)>' % (self.name, len(self)//2)
|
||||
|
||||
def __call__(self, key):
|
||||
return self[key]
|
||||
|
@ -21,6 +21,7 @@
|
||||
# *****************************************************************************
|
||||
"""Define parsing helpers"""
|
||||
|
||||
# TODO: remove, as currently not used
|
||||
|
||||
import re
|
||||
import time
|
||||
|
@ -141,7 +141,7 @@ class SequencerMixin:
|
||||
return self.read_hw_status()
|
||||
return self.Status.IDLE, ''
|
||||
|
||||
def do_stop(self):
|
||||
def stop(self):
|
||||
if self.seq_is_alive():
|
||||
self._seq_stopflag = True
|
||||
|
||||
|
@ -1,259 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""Define Metaclass for Modules/Features"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.errors import ProgrammingError, BadValueError
|
||||
from secop.params import Command, Override, Parameter
|
||||
from secop.datatypes import EnumType
|
||||
from secop.properties import PropertyMeta
|
||||
from secop.lib.classdoc import append_to_doc, indent_description
|
||||
|
||||
|
||||
class Done:
|
||||
"""a special return value for a read/write function
|
||||
|
||||
indicating that the setter is triggered already"""
|
||||
|
||||
|
||||
# warning: MAGIC!
|
||||
|
||||
class ModuleMeta(PropertyMeta):
|
||||
"""Metaclass
|
||||
|
||||
joining the class's properties, parameters and commands dicts with
|
||||
those of base classes.
|
||||
also creates getters/setter for parameter access
|
||||
and wraps read_*/write_* methods
|
||||
(so the dispatcher will get notfied of changed values)
|
||||
"""
|
||||
def __new__(cls, name, bases, attrs):
|
||||
commands = attrs.pop('commands', {})
|
||||
parameters = attrs.pop('parameters', {})
|
||||
overrides = attrs.pop('overrides', {})
|
||||
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if '__constructed__' in attrs:
|
||||
return newtype
|
||||
|
||||
newtype = PropertyMeta.__join_properties__(newtype, name, bases, attrs)
|
||||
|
||||
# merge accessibles from all sub-classes, treat overrides
|
||||
# for now, allow to use also the old syntax (parameters/commands dict)
|
||||
accessibles_list = []
|
||||
for base in reversed(bases):
|
||||
if hasattr(base, "accessibles"):
|
||||
accessibles_list.append(base.accessibles)
|
||||
for accessibles in [parameters, commands, overrides]:
|
||||
accessibles_list.append(accessibles)
|
||||
accessibles = {} # unordered dict of accessibles, will be sorted later
|
||||
for accessibles_dict in accessibles_list:
|
||||
for key, obj in accessibles_dict.items():
|
||||
if isinstance(obj, Override):
|
||||
if key not in accessibles:
|
||||
raise ProgrammingError("module %s: can not apply Override on %s: no such accessible!"
|
||||
% (name, key))
|
||||
obj = obj.apply(accessibles[key])
|
||||
accessibles[key] = obj
|
||||
else:
|
||||
if obj is None: # allow removal of accessibles
|
||||
accessibles.pop(key, None)
|
||||
continue
|
||||
if key in accessibles:
|
||||
# for now, accept redefinitions:
|
||||
print("WARNING: module %s: %s should not be redefined"
|
||||
% (name, key))
|
||||
# raise ProgrammingError("module %s: %s must not be redefined"
|
||||
# % (name, key))
|
||||
if isinstance(obj, Parameter):
|
||||
accessibles[key] = obj
|
||||
elif isinstance(obj, Command):
|
||||
# XXX: convert to param with datatype=CommandType???
|
||||
accessibles[key] = obj
|
||||
else:
|
||||
raise ProgrammingError('%r: accessibles entry %r should be a '
|
||||
'Parameter or Command object!' % (name, key))
|
||||
|
||||
# Correct naming of EnumTypes
|
||||
for k, v in accessibles.items():
|
||||
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
|
||||
v.datatype._enum.name = k
|
||||
|
||||
# newtype.accessibles will be used in 2 places only:
|
||||
# 1) for inheritance (see above)
|
||||
# 2) for the describing message
|
||||
newtype.accessibles = OrderedDict(sorted(accessibles.items(), key=lambda item: item[1].ctr))
|
||||
|
||||
# check for attributes overriding parameter values
|
||||
for pname, pobj in newtype.accessibles.items():
|
||||
if pname in attrs:
|
||||
try:
|
||||
value = pobj.datatype(attrs[pname])
|
||||
except BadValueError:
|
||||
raise ProgrammingError('parameter %s can not be set to %r'
|
||||
% (pname, attrs[pname]))
|
||||
newtype.accessibles[pname] = Override(default=value).apply(pobj)
|
||||
|
||||
# check validity of Parameter entries
|
||||
for pname, pobj in newtype.accessibles.items():
|
||||
# XXX: create getters for the units of params ??
|
||||
|
||||
# wrap of reading/writing funcs
|
||||
if isinstance(pobj, Command):
|
||||
# skip commands for now
|
||||
continue
|
||||
rfunc = attrs.get('read_' + pname, None)
|
||||
rfunc_handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None
|
||||
if rfunc_handler:
|
||||
if rfunc:
|
||||
raise ProgrammingError("parameter '%s' can not have a handler "
|
||||
"and read_%s" % (pname, pname))
|
||||
rfunc = rfunc_handler
|
||||
else:
|
||||
for base in bases:
|
||||
if rfunc is not None:
|
||||
break
|
||||
rfunc = getattr(base, 'read_' + pname, None)
|
||||
|
||||
# create wrapper except when read function is already wrapped
|
||||
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
|
||||
|
||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||
if rfunc:
|
||||
self.log.debug("calling %r" % rfunc)
|
||||
try:
|
||||
value = rfunc(self)
|
||||
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
||||
if value is Done: # the setter is already triggered
|
||||
return getattr(self, pname)
|
||||
except Exception as e:
|
||||
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
||||
self.announceUpdate(pname, None, e)
|
||||
raise
|
||||
else:
|
||||
# return cached value
|
||||
self.log.debug("rfunc(%s): return cached value" % pname)
|
||||
value = self.accessibles[pname].value
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
|
||||
if rfunc:
|
||||
wrapped_rfunc.__doc__ = rfunc.__doc__
|
||||
setattr(newtype, 'read_' + pname, wrapped_rfunc)
|
||||
wrapped_rfunc.__wrapped__ = True
|
||||
|
||||
if not pobj.readonly:
|
||||
wfunc = attrs.get('write_' + pname, None)
|
||||
if wfunc is None: # ignore the handler, if a write function is present
|
||||
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
|
||||
for base in bases:
|
||||
if wfunc is not None:
|
||||
break
|
||||
wfunc = getattr(base, 'write_' + pname, None)
|
||||
|
||||
# create wrapper except when write function is already wrapped
|
||||
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
|
||||
|
||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
||||
pobj = self.accessibles[pname]
|
||||
value = pobj.datatype(value)
|
||||
if wfunc:
|
||||
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
||||
returned_value = wfunc(self, value)
|
||||
if returned_value is Done: # the setter is already triggered
|
||||
return getattr(self, pname)
|
||||
if returned_value is not None: # goodie: accept missing return value
|
||||
value = returned_value
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
if wfunc:
|
||||
wrapped_wfunc.__doc__ = wfunc.__doc__
|
||||
setattr(newtype, 'write_' + pname, wrapped_wfunc)
|
||||
wrapped_wfunc.__wrapped__ = True
|
||||
|
||||
def getter(self, pname=pname):
|
||||
return self.accessibles[pname].value
|
||||
|
||||
def setter(self, value, pname=pname):
|
||||
self.announceUpdate(pname, value)
|
||||
|
||||
setattr(newtype, pname, property(getter, setter))
|
||||
|
||||
# check information about Command's
|
||||
for attrname in attrs:
|
||||
if attrname.startswith('do_'):
|
||||
if attrname[3:] not in newtype.accessibles:
|
||||
raise ProgrammingError('%r: command %r has to be specified '
|
||||
'explicitly!' % (name, attrname[3:]))
|
||||
|
||||
def fmt_param(name, param):
|
||||
if not isinstance(param, Parameter):
|
||||
return ''
|
||||
desc = indent_description(param)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [param.datatype.short_doc(), 'rd' if param.readonly else 'wr',
|
||||
None if param.export else 'hidden']
|
||||
dtinfo = '*(%s)* ' % ', '.join(filter(None, dtinfo))
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
def fmt_command(name, command):
|
||||
if not isinstance(command, Command):
|
||||
return ''
|
||||
desc = indent_description(command)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = '' # note: we expect that desc contains argument list
|
||||
else:
|
||||
dtinfo = '*%s*' % command.datatype.short_doc() + ' -%s ' % ('' if command.export else ' *(hidden)*')
|
||||
return '- **%s**\\ %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
append_to_doc(newtype, 'parameters', 'SECOP Parameters',
|
||||
'accessibles', set(parameters) | set(overrides), fmt_param)
|
||||
append_to_doc(newtype, 'commands', 'SECOP Commands',
|
||||
'accessibles', set(commands) | set(overrides), fmt_command)
|
||||
|
||||
attrs['__constructed__'] = True
|
||||
return newtype
|
||||
|
||||
@property
|
||||
def configurables(cls):
|
||||
# note: this ends up as an property of the Module class (not on the instance)!
|
||||
|
||||
# dict of properties with Property and Parameter with dict of properties
|
||||
res = {}
|
||||
# collect info about properties
|
||||
for pn, pv in cls.properties.items():
|
||||
if pv.settable:
|
||||
res[pn] = pv
|
||||
# collect info about parameters and their properties
|
||||
for param, pobj in cls.accessibles.items():
|
||||
res[param] = {}
|
||||
for pn, pv in pobj.getProperties().items():
|
||||
if pv.settable:
|
||||
res[param][pn] = pv
|
||||
return res
|
353
secop/modules.py
353
secop/modules.py
@ -20,32 +20,158 @@
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
"""Define Baseclasses for real Modules implemented in the server"""
|
||||
"""Define base classes for real Modules implemented in the server"""
|
||||
|
||||
|
||||
import sys
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
|
||||
StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType
|
||||
from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueError,\
|
||||
SilentError, InternalError, secop_error
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
||||
IntRange, StatusType, StringType, TextType, TupleOf, get_datatype
|
||||
from secop.errors import BadValueError, ConfigError, InternalError, \
|
||||
ProgrammingError, SECoPError, SilentError, secop_error
|
||||
from secop.lib import formatException, formatExtendedStack, mkthread
|
||||
from secop.lib.enum import Enum
|
||||
from secop.metaclass import ModuleMeta
|
||||
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
|
||||
from secop.params import PREDEFINED_ACCESSIBLES, Accessible, Command, Parameter
|
||||
from secop.poller import BasicPoller, Poller
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.poller import Poller, BasicPoller
|
||||
|
||||
Done = object() #: a special return value for a read/write function indicating that the setter is triggered already
|
||||
|
||||
|
||||
# XXX: connect with 'protocol'-Modules.
|
||||
# Idea: every Module defined herein is also a 'protocol'-Module,
|
||||
# all others MUST derive from those, the 'interface'-class is still derived
|
||||
# from these base classes (how to do this?)
|
||||
class HasAccessibles(HasProperties):
|
||||
"""base class of module
|
||||
|
||||
joining the class's properties, parameters and commands dicts with
|
||||
those of base classes.
|
||||
wrap read_*/write_* methods
|
||||
(so the dispatcher will get notified of changed values)
|
||||
"""
|
||||
@classmethod
|
||||
def __init_subclass__(cls): # pylint: disable=too-many-branches
|
||||
super().__init_subclass__()
|
||||
# merge accessibles from all sub-classes, treat overrides
|
||||
# for now, allow to use also the old syntax (parameters/commands dict)
|
||||
accessibles = {}
|
||||
for base in reversed(cls.__bases__):
|
||||
accessibles.update(getattr(base, 'accessibles', {}))
|
||||
newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)}
|
||||
for aname, aobj in accessibles.items():
|
||||
value = getattr(cls, aname, None)
|
||||
if not isinstance(value, Accessible): # else override is already done in __set_name__
|
||||
anew = aobj.override(value)
|
||||
newaccessibles[aname] = anew
|
||||
setattr(cls, aname, anew)
|
||||
anew.__set_name__(cls, aname)
|
||||
ordered = {}
|
||||
for aname in cls.__dict__.get('paramOrder', ()):
|
||||
if aname in accessibles:
|
||||
ordered[aname] = accessibles.pop(aname)
|
||||
elif aname in newaccessibles:
|
||||
ordered[aname] = newaccessibles.pop(aname)
|
||||
# ignore unknown names
|
||||
# starting from old accessibles not mentioned, append items from 'order'
|
||||
accessibles.update(ordered)
|
||||
# then new accessibles not mentioned
|
||||
accessibles.update(newaccessibles)
|
||||
cls.accessibles = accessibles
|
||||
|
||||
# Correct naming of EnumTypes
|
||||
for k, v in accessibles.items():
|
||||
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
|
||||
v.datatype.set_name(k)
|
||||
|
||||
# check validity of Parameter entries
|
||||
for pname, pobj in accessibles.items():
|
||||
# XXX: create getters for the units of params ??
|
||||
|
||||
# wrap of reading/writing funcs
|
||||
if isinstance(pobj, Command):
|
||||
# nothing to do for now
|
||||
continue
|
||||
rfunc = cls.__dict__.get('read_' + pname, None)
|
||||
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
||||
if rfunc_handler:
|
||||
if rfunc:
|
||||
raise ProgrammingError("parameter '%s' can not have a handler "
|
||||
"and read_%s" % (pname, pname))
|
||||
rfunc = rfunc_handler
|
||||
|
||||
# create wrapper except when read function is already wrapped
|
||||
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False:
|
||||
|
||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||
if rfunc:
|
||||
self.log.debug("calling %r" % rfunc)
|
||||
try:
|
||||
value = rfunc(self)
|
||||
self.log.debug("rfunc(%s) returned %r" % (pname, value))
|
||||
if value is Done: # the setter is already triggered
|
||||
return getattr(self, pname)
|
||||
except Exception as e:
|
||||
self.log.debug("rfunc(%s) failed %r" % (pname, e))
|
||||
self.announceUpdate(pname, None, e)
|
||||
raise
|
||||
else:
|
||||
# return cached value
|
||||
self.log.debug("rfunc(%s): return cached value" % pname)
|
||||
value = self.accessibles[pname].value
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
|
||||
if rfunc:
|
||||
wrapped_rfunc.__doc__ = rfunc.__doc__
|
||||
setattr(cls, 'read_' + pname, wrapped_rfunc)
|
||||
wrapped_rfunc.__wrapped__ = True
|
||||
|
||||
if not pobj.readonly:
|
||||
wfunc = getattr(cls, 'write_' + pname, None)
|
||||
if wfunc is None: # ignore the handler, if a write function is present
|
||||
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None
|
||||
|
||||
# create wrapper except when write function is already wrapped
|
||||
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
|
||||
|
||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
||||
pobj = self.accessibles[pname]
|
||||
value = pobj.datatype(value)
|
||||
if wfunc:
|
||||
self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value))
|
||||
returned_value = wfunc(self, value)
|
||||
if returned_value is Done: # the setter is already triggered
|
||||
return getattr(self, pname)
|
||||
if returned_value is not None: # goodie: accept missing return value
|
||||
value = returned_value
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
if wfunc:
|
||||
wrapped_wfunc.__doc__ = wfunc.__doc__
|
||||
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
||||
wrapped_wfunc.__wrapped__ = True
|
||||
|
||||
# check information about Command's
|
||||
for attrname in cls.__dict__:
|
||||
if attrname.startswith('do_'):
|
||||
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||
% (cls.__name__, attrname))
|
||||
|
||||
res = {}
|
||||
# collect info about properties
|
||||
for pn, pv in cls.propertyDict.items():
|
||||
if pv.settable:
|
||||
res[pn] = pv
|
||||
# collect info about parameters and their properties
|
||||
for param, pobj in cls.accessibles.items():
|
||||
res[param] = {}
|
||||
for pn, pv in pobj.getProperties().items():
|
||||
if pv.settable:
|
||||
res[param][pn] = pv
|
||||
cls.configurables = res
|
||||
|
||||
|
||||
class Module(HasProperties, metaclass=ModuleMeta):
|
||||
class Module(HasAccessibles):
|
||||
"""basic module
|
||||
|
||||
all SECoP modules derive from this.
|
||||
@ -58,7 +184,8 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
Notes:
|
||||
|
||||
- the programmer normally should not need to reimplement :meth:`__init__`
|
||||
- within modules, parameters should only be addressed as ``self.<pname>``, i.e. ``self.value``, ``self.target`` etc...
|
||||
- within modules, parameters should only be addressed as ``self.<pname>``,
|
||||
i.e. ``self.value``, ``self.target`` etc...
|
||||
|
||||
- these are accessing the cached version.
|
||||
- they can also be written to, generating an async update
|
||||
@ -77,25 +204,21 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# note: properties don't change after startup and are usually filled
|
||||
# with data from a cfg file...
|
||||
# note: only the properties predefined here are allowed to be set in the cfg file
|
||||
# note: the names map to a [datatype, value] list, value comes from the cfg file,
|
||||
# datatype is fixed!
|
||||
properties = {
|
||||
'export': Property('flag if this Module is to be exported', BoolType(), default=True, export=False),
|
||||
'group': Property('optional group the Module belongs to', StringType(), default='', extname='group'),
|
||||
'description': Property('description of the module', TextType(), extname='description', mandatory=True),
|
||||
'meaning': Property('dptional Meaning indicator', TupleOf(StringType(),IntRange(0,50)),
|
||||
default=('',0), extname='meaning'),
|
||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
default='user', extname='visibility'),
|
||||
'implementation': Property('internal name of the implementation class of the module', StringType(),
|
||||
extname='implementation'),
|
||||
'interface_classes': Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes'),
|
||||
}
|
||||
export = Property('flag if this module is to be exported', BoolType(), default=True, export=False)
|
||||
group = Property('optional group the module belongs to', StringType(), default='', extname='group')
|
||||
description = Property('description of the module', TextType(), extname='description', mandatory=True)
|
||||
meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)),
|
||||
default=('', 0), extname='meaning')
|
||||
visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
default='user', extname='visibility')
|
||||
implementation = Property('internal name of the implementation class of the module', StringType(),
|
||||
extname='implementation')
|
||||
interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()),
|
||||
extname='interface_classes')
|
||||
|
||||
# properties, parameters and commands are auto-merged upon subclassing
|
||||
parameters = {} #: definition of parameters
|
||||
commands = {} #: definition of commands
|
||||
parameters = {}
|
||||
commands = {}
|
||||
|
||||
# reference to the dispatcher (used for sending async updates)
|
||||
DISPATCHER = None
|
||||
@ -112,14 +235,14 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
# handle module properties
|
||||
# 1) make local copies of properties
|
||||
super(Module, self).__init__()
|
||||
super().__init__()
|
||||
|
||||
# 2) check and apply properties specified in cfgdict
|
||||
# specified as '.<propertyname> = <propertyvalue>'
|
||||
# (this is for legacy config files only)
|
||||
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
|
||||
if k[0] == '.':
|
||||
if k[1:] in self.__class__.properties:
|
||||
if k[1:] in self.propertyDict:
|
||||
self.setProperty(k[1:], cfgdict.pop(k))
|
||||
else:
|
||||
raise ConfigError('Module %r has no property %r' %
|
||||
@ -127,20 +250,20 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
|
||||
# 3) check and apply properties specified in cfgdict as
|
||||
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
||||
for k in self.__class__.properties:
|
||||
for k in self.propertyDict:
|
||||
if k in cfgdict:
|
||||
self.setProperty(k, cfgdict.pop(k))
|
||||
|
||||
# 4) set automatic properties
|
||||
mycls = self.__class__
|
||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||
self.properties['implementation'] = myclassname
|
||||
self.implementation = myclassname
|
||||
# list of all 'secop' modules
|
||||
self.properties['interface_classes'] = [
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||
# self.interface_classes = [
|
||||
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
|
||||
# list of only the 'highest' secop module class
|
||||
self.properties['interface_classes'] = [[
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
|
||||
self.interface_classes = [
|
||||
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1]
|
||||
|
||||
# handle Features
|
||||
# XXX: todo
|
||||
@ -149,7 +272,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
# 1) make local copies of parameter objects
|
||||
# they need to be individual per instance since we use them also
|
||||
# to cache the current value + qualifiers...
|
||||
accessibles = OrderedDict()
|
||||
accessibles = {}
|
||||
# conversion from exported names to internal attribute names
|
||||
accessiblename2attr = {}
|
||||
for aname, aobj in self.accessibles.items():
|
||||
@ -158,31 +281,31 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if isinstance(aobj, Parameter):
|
||||
# fix default properties poll and needscfg
|
||||
if aobj.poll is None:
|
||||
aobj.properties['poll'] = bool(aobj.handler)
|
||||
aobj.poll = bool(aobj.handler)
|
||||
if aobj.needscfg is None:
|
||||
aobj.properties['needscfg'] = not aobj.poll
|
||||
aobj.needscfg = not aobj.poll
|
||||
|
||||
if not self.export: # do not export parameters of a module not exported
|
||||
aobj.properties['export'] = False
|
||||
aobj.export = False
|
||||
if aobj.export:
|
||||
if aobj.export is True:
|
||||
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
|
||||
if predefined_obj:
|
||||
if isinstance(aobj, predefined_obj):
|
||||
aobj.setProperty('export', aname)
|
||||
aobj.export = aname
|
||||
else:
|
||||
raise ProgrammingError("can not use '%s' as name of a %s" %
|
||||
(aname, aobj.__class__.__name__))
|
||||
else: # create custom parameter
|
||||
aobj.setProperty('export', '_' + aname)
|
||||
(aname, aobj.__class__.__name__))
|
||||
else: # create custom parameter
|
||||
aobj.export = '_' + aname
|
||||
accessiblename2attr[aobj.export] = aname
|
||||
accessibles[aname] = aobj
|
||||
# do not re-use self.accessibles as this is the same for all instances
|
||||
self.accessibles = accessibles
|
||||
self.accessiblename2attr = accessiblename2attr
|
||||
# provide properties to 'filter' out the parameters/commands
|
||||
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter))
|
||||
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command))
|
||||
self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
|
||||
self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
|
||||
|
||||
# 2) check and apply parameter_properties
|
||||
# specified as '<paramname>.<propertyname> = <propertyvalue>'
|
||||
@ -199,6 +322,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
else:
|
||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||
(self.name, paramname, propname))
|
||||
else:
|
||||
raise ConfigError('Module %s has no Parameter %r!' %
|
||||
(self.name, paramname))
|
||||
|
||||
# 3) check config for problems:
|
||||
# only accept remaining config items specified in parameters
|
||||
@ -208,7 +334,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
'Module %s:config Parameter %r '
|
||||
'not understood! (use one of %s)' %
|
||||
(self.name, k, ', '.join(list(self.parameters) +
|
||||
list(self.__class__.properties))))
|
||||
list(self.propertyDict))))
|
||||
|
||||
# 4) complain if a Parameter entry has no default value and
|
||||
# is not specified in cfgdict and deal with parameters to be written.
|
||||
@ -220,6 +346,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if pname in cfgdict:
|
||||
if not pobj.readonly and pobj.initwrite is not False:
|
||||
# parameters given in cfgdict have to call write_<pname>
|
||||
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
||||
try:
|
||||
pobj.value = pobj.datatype(cfgdict[pname])
|
||||
except BadValueError as e:
|
||||
@ -228,7 +355,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
else:
|
||||
if pobj.default is None:
|
||||
if pobj.needscfg:
|
||||
raise ConfigError('Module %s: Parameter %r has no default '
|
||||
raise ConfigError('Parameter %s.%s has no default '
|
||||
'value and was not given in config!' %
|
||||
(self.name, pname))
|
||||
# we do not want to call the setter for this parameter for now,
|
||||
@ -243,9 +370,10 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
except BadValueError as e:
|
||||
raise ProgrammingError('bad default for %s.%s: %s'
|
||||
% (name, pname, e))
|
||||
if pobj.initwrite:
|
||||
if pobj.initwrite and not pobj.readonly:
|
||||
# we will need to call write_<pname>
|
||||
# if this is not desired, the default must not be given
|
||||
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
|
||||
pobj.value = value
|
||||
self.writeDict[pname] = value
|
||||
else:
|
||||
@ -312,7 +440,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
for cb in cblist:
|
||||
try:
|
||||
cb(arg)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# print(formatExtendedTraceback())
|
||||
pass
|
||||
|
||||
@ -358,38 +486,18 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
modobj.announceUpdate(p, value)
|
||||
self.valueCallbacks[pname].append(cb)
|
||||
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""helper function for treating substates of BUSY correctly"""
|
||||
# defined even for non drivable (used for dynamic polling)
|
||||
return False
|
||||
|
||||
def earlyInit(self):
|
||||
"""may be overriden in derived classes to init stuff
|
||||
|
||||
after creating the module (no super call needed)
|
||||
"""
|
||||
# may be overriden in derived classes to init stuff
|
||||
self.log.debug('empty %s.earlyInit()' % self.__class__.__name__)
|
||||
|
||||
def initModule(self):
|
||||
"""may be overriden to do stuff after all modules are intiialized
|
||||
|
||||
no super call needed
|
||||
"""
|
||||
self.log.debug('empty %s.initModule()' % self.__class__.__name__)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""runs after init of all modules
|
||||
|
||||
:param started_callback: argument less function to be called when the thread
|
||||
spawned by startModule has finished its initial work
|
||||
:return: None or a timeout value, if different from default (30 sec)
|
||||
|
||||
override this method for doing stuff during startup, after all modules are
|
||||
initialized. do not forget the super call
|
||||
"""
|
||||
mkthread(self.writeInitParams, started_callback)
|
||||
|
||||
def pollOneParam(self, pname):
|
||||
"""poll parameter <pname> with proper error handling"""
|
||||
try:
|
||||
@ -421,33 +529,33 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if started_callback:
|
||||
started_callback()
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""runs after init of all modules
|
||||
|
||||
started_callback to be called when the thread spawned by startModule
|
||||
has finished its initial work
|
||||
might return a timeout value, if different from default
|
||||
"""
|
||||
mkthread(self.writeInitParams, started_callback)
|
||||
|
||||
|
||||
class Readable(Module):
|
||||
"""basic readable module"""
|
||||
# pylint: disable=invalid-name
|
||||
Status = Enum('Status',
|
||||
IDLE = 100,
|
||||
WARN = 200,
|
||||
UNSTABLE = 270,
|
||||
ERROR = 400,
|
||||
DISABLED = 0,
|
||||
UNKNOWN = 401,
|
||||
) #: status codes
|
||||
parameters = {
|
||||
'value': Parameter('current value of the Module', readonly=True,
|
||||
datatype=FloatRange(),
|
||||
poll=True,
|
||||
),
|
||||
'pollinterval': Parameter('sleeptime between polls', default=5,
|
||||
readonly=False,
|
||||
datatype=FloatRange(0.1, 120),
|
||||
),
|
||||
'status': Parameter('*(rd, tuple of (Readable.Status, str))* current status of the Module',
|
||||
default=(Status.IDLE, ''),
|
||||
datatype=TupleOf(EnumType(Status), StringType()),
|
||||
readonly=True, poll=True,
|
||||
),
|
||||
}
|
||||
IDLE=100,
|
||||
WARN=200,
|
||||
UNSTABLE=270,
|
||||
ERROR=400,
|
||||
DISABLED=0,
|
||||
UNKNOWN=401,
|
||||
) #: status codes
|
||||
|
||||
value = Parameter('current value of the module', FloatRange(), poll=True)
|
||||
status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
|
||||
default=(Status.IDLE, ''), poll=True)
|
||||
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120),
|
||||
default=5, readonly=False)
|
||||
|
||||
def startModule(self, started_callback):
|
||||
"""start basic polling thread"""
|
||||
@ -496,30 +604,17 @@ class Readable(Module):
|
||||
|
||||
class Writable(Readable):
|
||||
"""basic writable module"""
|
||||
parameters = {
|
||||
'target': Parameter('target value of the Module',
|
||||
default=0, readonly=False, datatype=FloatRange(),
|
||||
),
|
||||
}
|
||||
|
||||
target = Parameter('target value of the module',
|
||||
default=0, readonly=False, datatype=FloatRange())
|
||||
|
||||
|
||||
class Drivable(Writable):
|
||||
"""basic drivable module"""
|
||||
|
||||
Status = Enum(Readable.Status, BUSY=300) #: Status codes
|
||||
Status = Enum(Readable.Status, BUSY=300) #: status codes
|
||||
|
||||
commands = {
|
||||
'stop': Command(
|
||||
'cease driving, go to IDLE state',
|
||||
argument=None,
|
||||
result=None
|
||||
),
|
||||
}
|
||||
|
||||
overrides = {
|
||||
'status': Override('*(rd, tuple of (Drivable.Status, str))* current status of the Module',
|
||||
datatype=StatusType(Status)),
|
||||
}
|
||||
status = Parameter(datatype=StatusType(Status)) # override Readable.status
|
||||
|
||||
def isBusy(self, status=None):
|
||||
"""check for busy, treating substates correctly
|
||||
@ -533,7 +628,6 @@ class Drivable(Writable):
|
||||
|
||||
returns True when busy, but not finalizing
|
||||
"""
|
||||
""""""
|
||||
return 300 <= (status or self.status)[0] < 390
|
||||
|
||||
# improved polling: may poll faster if module is BUSY
|
||||
@ -554,26 +648,16 @@ class Drivable(Writable):
|
||||
self.pollOneParam(pname)
|
||||
return fastpoll
|
||||
|
||||
def do_stop(self):
|
||||
# default implementation of the stop command
|
||||
# by default does nothing
|
||||
pass
|
||||
@Command(None, result=None)
|
||||
def stop(self):
|
||||
"""cease driving, go to IDLE state"""
|
||||
|
||||
|
||||
class Communicator(Module):
|
||||
"""basic communication module
|
||||
"""basic abstract communication module"""
|
||||
|
||||
providing no parameters, but a 'communicate' command.
|
||||
"""
|
||||
|
||||
commands = {
|
||||
"communicate": Command("provides the simplest mean to communication",
|
||||
argument=StringType(),
|
||||
result=StringType()
|
||||
),
|
||||
}
|
||||
|
||||
def do_communicate(self, command):
|
||||
@Command(StringType(), result=StringType())
|
||||
def communicate(self, command):
|
||||
"""communicate command
|
||||
|
||||
:param command: the command to be sent
|
||||
@ -583,7 +667,7 @@ class Communicator(Module):
|
||||
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached module
|
||||
"""a special property, defining an attached modle
|
||||
|
||||
assign a module name to this property in the cfg file,
|
||||
and the server will create an attribute with this module
|
||||
@ -594,7 +678,8 @@ class Attached(Property):
|
||||
# we can not put this to properties.py, as it needs datatypes
|
||||
def __init__(self, attrname=None):
|
||||
self.attrname = attrname
|
||||
super().__init__('attached module', StringType())
|
||||
# we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev
|
||||
super().__init__('attached module', StringType(), mandatory=False)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '')
|
||||
|
607
secop/params.py
607
secop/params.py
@ -23,154 +23,243 @@
|
||||
"""Define classes for Parameters/Commands and Overriding them"""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from inspect import cleandoc
|
||||
import inspect
|
||||
|
||||
from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \
|
||||
NoneOr, TextType, IntRange
|
||||
from secop.errors import ProgrammingError, BadValueError
|
||||
from secop.datatypes import BoolType, CommandType, DataType, \
|
||||
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
|
||||
StringType, StructOf, TextType, TupleOf, ValueType
|
||||
from secop.errors import BadValueError, ProgrammingError
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
|
||||
class CountedObj:
|
||||
ctr = [0]
|
||||
def __init__(self):
|
||||
cl = self.__class__.ctr
|
||||
cl[0] += 1
|
||||
self.ctr = cl[0]
|
||||
UNSET = object() # an argument not given, not even None
|
||||
|
||||
|
||||
class Accessible(HasProperties, CountedObj):
|
||||
'''base class for Parameter and Command'''
|
||||
class Accessible(HasProperties):
|
||||
"""base class for Parameter and Command"""
|
||||
|
||||
properties = {}
|
||||
kwds = None # is a dict if it might be used as Override
|
||||
|
||||
def __init__(self, **kwds):
|
||||
super(Accessible, self).__init__()
|
||||
# do not use self.properties.update here, as no invalid values should be
|
||||
super().__init__()
|
||||
self.init(kwds)
|
||||
|
||||
def init(self, kwds):
|
||||
# do not use self.propertyValues.update here, as no invalid values should be
|
||||
# assigned to properties, even not before checkProperties
|
||||
for k,v in kwds.items():
|
||||
for k, v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join(
|
||||
['%s=%r' % (k, self.properties.get(k, v.default)) for k, v in sorted(self.__class__.properties.items())]))
|
||||
def inherit(self, cls, owner):
|
||||
for base in owner.__bases__:
|
||||
if hasattr(base, self.name):
|
||||
aobj = getattr(base, 'accessibles', {}).get(self.name)
|
||||
if aobj:
|
||||
if not isinstance(aobj, cls):
|
||||
raise ProgrammingError('%s %s.%s can not inherit from a %s' %
|
||||
(cls.__name__, owner.__name__, self.name, aobj.__class__.__name__))
|
||||
# inherit from aobj
|
||||
for pname, value in aobj.propertyValues.items():
|
||||
if pname not in self.propertyValues:
|
||||
self.propertyValues[pname] = value
|
||||
break
|
||||
|
||||
def as_dict(self):
|
||||
return self.propertyValues
|
||||
|
||||
def override(self, value=UNSET, **kwds):
|
||||
"""return a copy, overridden by a bare attribute
|
||||
|
||||
and/or some properties"""
|
||||
raise NotImplementedError
|
||||
|
||||
def copy(self):
|
||||
# return a copy of ourselfs
|
||||
props = dict(self.properties, ctr=self.ctr)
|
||||
# deep copy, as datatype might be altered from config
|
||||
props['datatype'] = props['datatype'].copy()
|
||||
return type(self)(**props)
|
||||
"""return a (deep) copy of ourselfs"""
|
||||
raise NotImplementedError
|
||||
|
||||
def for_export(self):
|
||||
"""prepare for serialisation"""
|
||||
return self.exportProperties()
|
||||
raise NotImplementedError
|
||||
|
||||
def __repr__(self):
|
||||
props = []
|
||||
for k, prop in sorted(self.propertyDict.items()):
|
||||
v = self.propertyValues.get(k, prop.default)
|
||||
if v != prop.default:
|
||||
props.append('%s=%r' % (k, v))
|
||||
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
|
||||
|
||||
|
||||
class Parameter(Accessible):
|
||||
"""storage for parameter settings + value + qualifiers"""
|
||||
# poll: meaning for the basicPoller:
|
||||
# - True or 1 (poll this every pollinterval)
|
||||
# - positive int (poll every N(th) pollinterval)
|
||||
# - negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval)
|
||||
# note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
|
||||
"""defines a parameter
|
||||
|
||||
properties = {
|
||||
'description': Property('mandatory description of the parameter', TextType(),
|
||||
extname='description', mandatory=True),
|
||||
'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||
extname='datainfo', mandatory=True),
|
||||
'readonly': Property('not changeable via SECoP (default True)', BoolType(),
|
||||
extname='readonly', mandatory=True),
|
||||
'group': Property('optional parameter group this parameter belongs to', StringType(),
|
||||
extname='group', default=''),
|
||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
extname='visibility', default=1),
|
||||
'constant': Property('optional constant value for constant parameters', ValueType(),
|
||||
extname='constant', default=None, mandatory=False),
|
||||
'default': Property('[internal] default (startup) value of this parameter '
|
||||
'if it can not be read from the hardware.',
|
||||
ValueType(), export=False, default=None, mandatory=False),
|
||||
'export': Property('''
|
||||
[internal] export settings
|
||||
|
||||
* False: not accessible via SECoP.
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''',
|
||||
OrType(BoolType(), StringType()), export=False, default=True),
|
||||
'poll': Property('''
|
||||
[internal] polling indicator
|
||||
|
||||
may be:
|
||||
|
||||
* None (omitted): will be converted to True/False if handler is/is not None
|
||||
* False or 0 (never poll this parameter)
|
||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||
* 3 (REGULAR), polled with pollperiod
|
||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||
else polled with pollperiod
|
||||
''',
|
||||
NoneOr(IntRange()), export=False, default=None),
|
||||
'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None),
|
||||
'optional': Property('[internal] is this parameter optional?', BoolType(), export=False,
|
||||
settable=False, default=False),
|
||||
'handler': Property('[internal] overload the standard read and write functions',
|
||||
ValueType(), export=False, default=None, mandatory=False, settable=False),
|
||||
'initwrite': Property('[internal] write this parameter on initialization'
|
||||
' (default None: write if given in config)',
|
||||
NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False),
|
||||
}
|
||||
:param description: description
|
||||
:param datatype: the datatype
|
||||
:param inherit: whether properties not given should be inherited
|
||||
:param kwds: optional properties
|
||||
"""
|
||||
# storage for Parameter settings + value + qualifiers
|
||||
|
||||
def __init__(self, description, datatype, *, ctr=None, unit=None, **kwds):
|
||||
description = Property(
|
||||
'mandatory description of the parameter', TextType(),
|
||||
extname='description', mandatory=True)
|
||||
datatype = Property(
|
||||
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||
extname='datainfo', mandatory=True)
|
||||
readonly = Property(
|
||||
'not changeable via SECoP (default True)', BoolType(),
|
||||
extname='readonly', default=True, export='always')
|
||||
group = Property(
|
||||
'optional parameter group this parameter belongs to', StringType(),
|
||||
extname='group', default='')
|
||||
visibility = Property(
|
||||
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
extname='visibility', default=1)
|
||||
constant = Property(
|
||||
'optional constant value for constant parameters', ValueType(),
|
||||
extname='constant', default=None)
|
||||
default = Property(
|
||||
'''[internal] default (startup) value of this parameter
|
||||
|
||||
if ctr is not None:
|
||||
self.ctr = ctr
|
||||
if it can not be read from the hardware''', ValueType(),
|
||||
export=False, default=None)
|
||||
export = Property(
|
||||
'''[internal] export settings
|
||||
|
||||
if not isinstance(datatype, DataType):
|
||||
if issubclass(datatype, DataType):
|
||||
# goodie: make an instance from a class (forgotten ()???)
|
||||
datatype = datatype()
|
||||
else:
|
||||
raise ProgrammingError(
|
||||
'datatype MUST be derived from class DataType!')
|
||||
* False: not accessible via SECoP.
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
poll = Property(
|
||||
'''[internal] polling indicator
|
||||
|
||||
kwds['description'] = cleandoc(description)
|
||||
kwds['datatype'] = datatype
|
||||
kwds['readonly'] = kwds.get('readonly', True) # for frappy optional, for SECoP mandatory
|
||||
if unit is not None: # for legacy code only
|
||||
datatype.setProperty('unit', unit)
|
||||
super(Parameter, self).__init__(**kwds)
|
||||
may be:
|
||||
|
||||
if self.initwrite and self.readonly:
|
||||
raise ProgrammingError('can not have both readonly and initwrite!')
|
||||
* None (omitted): will be converted to True/False if handler is/is not None
|
||||
* False or 0 (never poll this parameter)
|
||||
* True or 1 (AUTO), converted to SLOW (readonly=False)
|
||||
DYNAMIC (*status* and *value*) or REGULAR (else)
|
||||
* 2 (SLOW), polled with lower priority and a multiple of pollinterval
|
||||
* 3 (REGULAR), polled with pollperiod
|
||||
* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval,
|
||||
else polled with pollperiod
|
||||
''', NoneOr(IntRange()),
|
||||
export=False, default=None)
|
||||
needscfg = Property(
|
||||
'[internal] needs value in config', NoneOr(BoolType()),
|
||||
export=False, default=None)
|
||||
optional = Property(
|
||||
'[internal] is this parameter optional?', BoolType(),
|
||||
export=False, settable=False, default=False)
|
||||
handler = Property(
|
||||
'[internal] overload the standard read and write functions', ValueType(),
|
||||
export=False, default=None, settable=False)
|
||||
initwrite = Property(
|
||||
'''[internal] write this parameter on initialization
|
||||
|
||||
if self.constant is not None:
|
||||
self.properties['readonly'] = True
|
||||
default None: write if given in config''', NoneOr(BoolType()),
|
||||
export=False, default=None, settable=False)
|
||||
|
||||
# used on the instance copy only
|
||||
value = None
|
||||
timestamp = 0
|
||||
readerror = None
|
||||
|
||||
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
|
||||
super().__init__(**kwds)
|
||||
if datatype is not None:
|
||||
if not isinstance(datatype, DataType):
|
||||
if isinstance(datatype, type) and issubclass(datatype, DataType):
|
||||
# goodie: make an instance from a class (forgotten ()???)
|
||||
datatype = datatype()
|
||||
else:
|
||||
raise ProgrammingError(
|
||||
'datatype MUST be derived from class DataType!')
|
||||
self.datatype = datatype
|
||||
if 'default' in kwds:
|
||||
self.default = datatype(kwds['default'])
|
||||
|
||||
if description is not None:
|
||||
self.description = inspect.cleandoc(description)
|
||||
|
||||
# save for __set_name__
|
||||
self._inherit = inherit
|
||||
self._unit = unit # for legacy code only
|
||||
self._constant = constant
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
# not used yet
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.parameters[self.name].value
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj.announceUpdate(self.name, value)
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
|
||||
if self._inherit:
|
||||
self.inherit(Parameter, owner)
|
||||
|
||||
# check for completeness
|
||||
missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues]
|
||||
if missing_properties:
|
||||
raise ProgrammingError('Parameter %s.%s needs a %s' %
|
||||
(owner.__name__, name, ' and a '.join(missing_properties)))
|
||||
if self._unit is not None:
|
||||
self.datatype.setProperty('unit', self._unit)
|
||||
|
||||
if self._constant is not None:
|
||||
constant = self.datatype(self._constant)
|
||||
# The value of the `constant` property should be the
|
||||
# serialised version of the constant, or unset
|
||||
constant = self.datatype(kwds['constant'])
|
||||
self.properties['constant'] = self.datatype.export_value(constant)
|
||||
self.constant = self.datatype.export_value(constant)
|
||||
self.readonly = True
|
||||
|
||||
# internal caching: value and timestamp of last change...
|
||||
self.value = self.default
|
||||
self.timestamp = 0
|
||||
self.readerror = None # if not None, indicates that last read was not successful
|
||||
if 'default' in self.propertyValues:
|
||||
# fixes in case datatype has changed
|
||||
try:
|
||||
self.datatype(self.default)
|
||||
except BadValueError:
|
||||
# clear default, if it does not match datatype
|
||||
self.propertyValues.pop('default')
|
||||
|
||||
if self.export is True:
|
||||
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||
self.export = name
|
||||
else:
|
||||
self.export = '_' + name
|
||||
|
||||
def copy(self):
|
||||
# deep copy, as datatype might be altered from config
|
||||
res = Parameter()
|
||||
res.name = self.name
|
||||
res.init(self.propertyValues)
|
||||
res.datatype = res.datatype.copy()
|
||||
return res
|
||||
|
||||
def override(self, value=UNSET, **kwds):
|
||||
res = self.copy()
|
||||
res.init(kwds)
|
||||
if value is not UNSET:
|
||||
res.value = res.datatype(value)
|
||||
return res
|
||||
|
||||
def export_value(self):
|
||||
return self.datatype.export_value(self.value)
|
||||
|
||||
def for_export(self):
|
||||
return dict(self.exportProperties(), readonly=self.readonly)
|
||||
|
||||
def getProperties(self):
|
||||
"""get also properties of datatype"""
|
||||
superProp = super().getProperties().copy()
|
||||
superProp.update(self.datatype.getProperties())
|
||||
return superProp
|
||||
super_prop = super().getProperties().copy()
|
||||
super_prop.update(self.datatype.getProperties())
|
||||
return super_prop
|
||||
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of datatype"""
|
||||
if key in self.__class__.properties:
|
||||
if key in self.propertyDict:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.datatype.setProperty(key, value)
|
||||
@ -179,158 +268,168 @@ class Parameter(Accessible):
|
||||
super().checkProperties()
|
||||
self.datatype.checkProperties()
|
||||
|
||||
def for_export(self):
|
||||
"""prepare for serialisation
|
||||
|
||||
readonly is mandatory for serialisation, but not for declaration in classes
|
||||
"""
|
||||
r = super().for_export()
|
||||
if 'readonly' not in r:
|
||||
r['readonly'] = self.__class__.properties['readonly'].default
|
||||
return r
|
||||
|
||||
|
||||
class UnusedClass:
|
||||
# do not derive anything from this!
|
||||
pass
|
||||
|
||||
|
||||
class Parameters(OrderedDict):
|
||||
"""class storage for Parameters"""
|
||||
def __init__(self, *args, **kwds):
|
||||
self.exported = {} # only for lookups!
|
||||
super(Parameters, self).__init__(*args, **kwds)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if value.export:
|
||||
if isinstance(value, PREDEFINED_ACCESSIBLES.get(key, UnusedClass)):
|
||||
value.properties['export'] = key
|
||||
else:
|
||||
value.properties['export'] = '_' + key
|
||||
self.exported[value.export] = key
|
||||
super(Parameters, self).__setitem__(key, value)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return super(Parameters, self).__getitem__(self.exported.get(item, item))
|
||||
|
||||
|
||||
class ParamValue:
|
||||
__slots__ = ['value', 'timestamp']
|
||||
def __init__(self, value, timestamp=0):
|
||||
self.value = value
|
||||
self.timestamp = timestamp
|
||||
|
||||
|
||||
class Commands(Parameters):
|
||||
"""class storage for Commands"""
|
||||
|
||||
|
||||
class Override(CountedObj):
|
||||
"""Stores the overrides to be applied to a Parameter or Command
|
||||
|
||||
note: overrides are applied by the metaclass during class creating
|
||||
reorder=True: use position of Override instead of inherited for the order
|
||||
"""
|
||||
def __init__(self, description="", datatype=None, *, reorder=False, **kwds):
|
||||
super(Override, self).__init__()
|
||||
self.kwds = kwds
|
||||
self.reorder = reorder
|
||||
# allow to override description and datatype without keyword
|
||||
if description:
|
||||
self.kwds['description'] = cleandoc(description)
|
||||
if datatype is not None:
|
||||
self.kwds['datatype'] = datatype
|
||||
# for now, do not use the Override ctr
|
||||
# self.kwds['ctr'] = self.ctr
|
||||
|
||||
def __repr__(self):
|
||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
|
||||
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
|
||||
|
||||
def apply(self, obj):
|
||||
if isinstance(obj, Accessible):
|
||||
props = obj.properties.copy()
|
||||
props['datatype'] = props['datatype'].copy()
|
||||
if isinstance(obj, Parameter):
|
||||
if 'constant' in self.kwds:
|
||||
constant = obj.datatype(self.kwds.pop('constant'))
|
||||
self.kwds['constant'] = obj.datatype.export_value(constant)
|
||||
self.kwds['readonly'] = True
|
||||
if 'datatype' in self.kwds and 'default' not in self.kwds:
|
||||
try:
|
||||
self.kwds['datatype'](obj.default)
|
||||
except BadValueError:
|
||||
# clear default, if it does not match datatype
|
||||
props['default'] = None
|
||||
props.update(self.kwds)
|
||||
|
||||
if self.reorder:
|
||||
return type(obj)(**props)
|
||||
return type(obj)(ctr=self.ctr, **props)
|
||||
raise ProgrammingError(
|
||||
"Overrides can only be applied to Accessibles, %r is none!" % obj)
|
||||
|
||||
|
||||
class Command(Accessible):
|
||||
"""storage for Commands settings (description + call signature...)
|
||||
"""decorator to turn a method into a command
|
||||
|
||||
:param argument: the datatype of the argument or None
|
||||
:param result: the datatype of the result or None
|
||||
:param inherit: whether properties not given should be inherited
|
||||
:param kwds: optional properties
|
||||
"""
|
||||
properties = {
|
||||
'description': Property('description of the command', TextType(),
|
||||
extname='description', export=True, mandatory=True),
|
||||
'group': Property('optional command group of the command.', StringType(),
|
||||
extname='group', export=True, default=''),
|
||||
'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
extname='visibility', export=True, default=1),
|
||||
'export': Property('''
|
||||
[internal] export settings
|
||||
|
||||
- False: not accessible via SECoP.
|
||||
- True: exported, name automatic.
|
||||
- a string: exported with custom name''',
|
||||
OrType(BoolType(), StringType()), export=False, default=True),
|
||||
'optional': Property('[internal] is the command optional to implement? (vs. mandatory)',
|
||||
BoolType(), export=False, default=False, settable=False),
|
||||
'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'',
|
||||
DataTypeType(), extname='datainfo', mandatory=True),
|
||||
'argument': Property('datatype of the argument to the command, or None.',
|
||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
||||
'result': Property('datatype of the result from the command, or None.',
|
||||
NoneOr(DataTypeType()), export=False, mandatory=True),
|
||||
}
|
||||
|
||||
def __init__(self, description, ctr=None, **kwds):
|
||||
kwds['description'] = cleandoc(description)
|
||||
kwds['datatype'] = CommandType(kwds.get('argument', None), kwds.get('result', None))
|
||||
super(Command, self).__init__(**kwds)
|
||||
if ctr is not None:
|
||||
self.ctr = ctr
|
||||
description = Property(
|
||||
'description of the Command', TextType(),
|
||||
extname='description', export=True, mandatory=True)
|
||||
group = Property(
|
||||
'optional command group of the command.', StringType(),
|
||||
extname='group', export=True, default='')
|
||||
visibility = Property(
|
||||
'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||
extname='visibility', export=True, default=1)
|
||||
export = Property(
|
||||
'''[internal] export settings
|
||||
|
||||
@property
|
||||
def argument(self):
|
||||
return self.datatype.argument
|
||||
* False: not accessible via SECoP.
|
||||
* True: exported, name automatic.
|
||||
* a string: exported with custom name''', OrType(BoolType(), StringType()),
|
||||
export=False, default=True)
|
||||
optional = Property(
|
||||
'[internal] is the command optional to implement? (vs. mandatory)', BoolType(),
|
||||
export=False, default=False, settable=False)
|
||||
datatype = Property(
|
||||
"datatype of the command, auto generated from 'argument' and 'result'",
|
||||
DataTypeType(), extname='datainfo', export='always')
|
||||
argument = Property(
|
||||
'datatype of the argument to the command, or None', NoneOr(DataTypeType()),
|
||||
export=False, mandatory=True)
|
||||
result = Property(
|
||||
'datatype of the result from the command, or None', NoneOr(DataTypeType()),
|
||||
export=False, mandatory=True)
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
return self.datatype.result
|
||||
func = None
|
||||
|
||||
def __init__(self, argument=False, *, result=None, inherit=True, **kwds):
|
||||
super().__init__(**kwds)
|
||||
if result or kwds or isinstance(argument, DataType) or not callable(argument):
|
||||
# normal case
|
||||
if argument is False and result:
|
||||
argument = None
|
||||
if argument is not False:
|
||||
if isinstance(argument, (tuple, list)):
|
||||
# goodie: treat as TupleOf
|
||||
argument = TupleOf(*argument)
|
||||
self.argument = argument
|
||||
self.result = result
|
||||
else:
|
||||
# goodie: allow @Command instead of @Command()
|
||||
self.func = argument # this is the wrapped method!
|
||||
if argument.__doc__:
|
||||
self.description = inspect.cleandoc(argument.__doc__)
|
||||
self.name = self.func.__name__
|
||||
self._inherit = inherit # save for __set_name__
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
if self.func is None:
|
||||
raise ProgrammingError('Command %s.%s must be used as a method decorator' %
|
||||
(owner.__name__, name))
|
||||
if self._inherit:
|
||||
self.inherit(Command, owner)
|
||||
|
||||
self.datatype = CommandType(self.argument, self.result)
|
||||
if self.export is True:
|
||||
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))):
|
||||
self.export = name
|
||||
else:
|
||||
self.export = '_' + name
|
||||
|
||||
def __get__(self, obj, owner=None):
|
||||
if obj is None:
|
||||
return self
|
||||
if not self.func:
|
||||
raise ProgrammingError('Command %s not properly configured' % self.name)
|
||||
return self.func.__get__(obj, owner)
|
||||
|
||||
def __call__(self, func):
|
||||
if 'description' not in self.propertyValues and func.__doc__:
|
||||
self.description = inspect.cleandoc(func.__doc__)
|
||||
self.func = func
|
||||
return self
|
||||
|
||||
def copy(self):
|
||||
res = Command()
|
||||
res.name = self.name
|
||||
res.func = self.func
|
||||
res.init(self.propertyValues)
|
||||
if res.argument:
|
||||
res.argument = res.argument.copy()
|
||||
if res.result:
|
||||
res.result = res.result.copy()
|
||||
res.datatype = CommandType(res.argument, res.result)
|
||||
return res
|
||||
|
||||
def override(self, value=UNSET, **kwds):
|
||||
res = self.copy()
|
||||
res.init(kwds)
|
||||
if value is not UNSET:
|
||||
res.func = value
|
||||
return res
|
||||
|
||||
def do(self, module_obj, argument):
|
||||
"""perform function call
|
||||
|
||||
:param module_obj: the module on which the command is to be executed
|
||||
:param argument: the argument from the do command
|
||||
:returns: the return value converted to the result type
|
||||
|
||||
- when the argument type is TupleOf, the function is called with multiple arguments
|
||||
- when the argument type is StructOf, the function is called with keyworded arguments
|
||||
- the validity of the argument/s is/are checked
|
||||
"""
|
||||
func = self.__get__(module_obj)
|
||||
if self.argument:
|
||||
# validate
|
||||
argument = self.argument(argument)
|
||||
if isinstance(self.argument, TupleOf):
|
||||
res = func(*argument)
|
||||
elif isinstance(self.argument, StructOf):
|
||||
res = func(**argument)
|
||||
else:
|
||||
res = func(argument)
|
||||
else:
|
||||
if argument is not None:
|
||||
raise BadValueError('%s.%s takes no arguments' % (module_obj.__class__.__name__, self.name))
|
||||
res = func()
|
||||
if self.result:
|
||||
return self.result(res)
|
||||
return None # silently ignore the result from the method
|
||||
|
||||
def for_export(self):
|
||||
return self.exportProperties()
|
||||
|
||||
def __repr__(self):
|
||||
result = super().__repr__()
|
||||
return result[:-1] + ', %r)' % self.func if self.func else result
|
||||
|
||||
|
||||
# list of predefined accessibles with their type
|
||||
PREDEFINED_ACCESSIBLES = dict(
|
||||
value = Parameter,
|
||||
status = Parameter,
|
||||
target = Parameter,
|
||||
pollinterval = Parameter,
|
||||
ramp = Parameter,
|
||||
user_ramp = Parameter,
|
||||
setpoint = Parameter,
|
||||
time_to_target = Parameter,
|
||||
unit = Parameter, # reserved name
|
||||
loglevel = Parameter, # reserved name
|
||||
mode = Parameter, # reserved name
|
||||
stop = Command,
|
||||
reset = Command,
|
||||
go = Command,
|
||||
abort = Command,
|
||||
shutdown = Command,
|
||||
communicate = Command,
|
||||
value=Parameter,
|
||||
status=Parameter,
|
||||
target=Parameter,
|
||||
pollinterval=Parameter,
|
||||
ramp=Parameter,
|
||||
user_ramp=Parameter,
|
||||
setpoint=Parameter,
|
||||
time_to_target=Parameter,
|
||||
unit=Parameter, # reserved name
|
||||
loglevel=Parameter, # reserved name
|
||||
mode=Parameter, # reserved name
|
||||
stop=Command,
|
||||
reset=Command,
|
||||
go=Command,
|
||||
abort=Command,
|
||||
shutdown=Command,
|
||||
communicate=Command,
|
||||
)
|
||||
|
@ -34,10 +34,11 @@ Usage examples:
|
||||
"""
|
||||
|
||||
import time
|
||||
from threading import Event
|
||||
from heapq import heapify, heapreplace
|
||||
from secop.lib import mkthread
|
||||
from threading import Event
|
||||
|
||||
from secop.errors import ProgrammingError
|
||||
from secop.lib import mkthread
|
||||
|
||||
# poll types:
|
||||
AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC
|
||||
@ -166,8 +167,8 @@ class Poller(PollerBase):
|
||||
continue # only one poller per handler
|
||||
handlers.add(pobj.handler)
|
||||
# placeholders 0 are used for due, lastdue and idx
|
||||
self.queues[polltype].append((0, 0,
|
||||
(0, module, pobj, pname, factors[polltype])))
|
||||
self.queues[polltype].append(
|
||||
(0, 0, (0, module, pobj, pname, factors[polltype])))
|
||||
|
||||
def poll_next(self, polltype):
|
||||
"""try to poll next item
|
||||
|
@ -23,11 +23,44 @@
|
||||
"""Define validated data types."""
|
||||
|
||||
|
||||
from collections import OrderedDict
|
||||
from inspect import cleandoc
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
||||
from secop.lib.classdoc import append_to_doc, indent_description
|
||||
from secop.errors import BadValueError, ConfigError, ProgrammingError
|
||||
|
||||
|
||||
class HasDescriptorMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if sys.version_info < (3, 6):
|
||||
# support older python versions
|
||||
for key, attr in attrs.items():
|
||||
if hasattr(attr, '__set_name__'):
|
||||
attr.__set_name__(newtype, key)
|
||||
newtype.__init_subclass__()
|
||||
return newtype
|
||||
|
||||
|
||||
class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||
bad = [k for k, v in cls.__dict__.items()
|
||||
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||
if bad:
|
||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||
|
||||
@classmethod
|
||||
def filterDescriptors(cls, filter_type):
|
||||
res = {}
|
||||
for name in dir(cls):
|
||||
desc = getattr(cls, name, None)
|
||||
if isinstance(desc, filter_type):
|
||||
res[name] = desc
|
||||
return res
|
||||
|
||||
|
||||
UNSET = object() # an unset value, not even None
|
||||
|
||||
|
||||
# storage for 'properties of a property'
|
||||
@ -35,175 +68,148 @@ class Property:
|
||||
"""base class holding info about a property
|
||||
|
||||
:param description: mandatory
|
||||
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed,
|
||||
:param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed!
|
||||
also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc.
|
||||
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
||||
when they match the default
|
||||
:param extname: external name
|
||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given
|
||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given.
|
||||
special value 'always': export also when matching the default
|
||||
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
||||
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
||||
:param settable: settable from the cfg file
|
||||
"""
|
||||
|
||||
# note: this is intended to be used on base classes.
|
||||
# the VALUES of the properties are on the instances!
|
||||
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
|
||||
def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None,
|
||||
settable=True, value=UNSET, name=''):
|
||||
if not callable(datatype):
|
||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||
self.description = cleandoc(description)
|
||||
self.default = datatype.default if default is None else datatype(default)
|
||||
self.description = inspect.cleandoc(description)
|
||||
self.default = datatype.default if default is UNSET else datatype(default)
|
||||
self.datatype = datatype
|
||||
self.extname = extname
|
||||
self.export = export or bool(extname)
|
||||
if mandatory is None:
|
||||
mandatory = default is None
|
||||
mandatory = default is UNSET
|
||||
self.mandatory = mandatory
|
||||
self.settable = settable or mandatory # settable means settable from the cfg file
|
||||
self.value = UNSET if value is UNSET else datatype(value)
|
||||
self.name = name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.propertyValues.get(self.name, self.default)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance.propertyValues[self.name] = self.datatype(value)
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
if self.export and not self.extname:
|
||||
self.extname = '_' + name
|
||||
if self.description == '_':
|
||||
# the programmer indicates, that the name is already speaking for itself
|
||||
self.description = name.replace('_', ' ')
|
||||
|
||||
def __repr__(self):
|
||||
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % (
|
||||
self.description, self.datatype, self.default, self.extname, self.export,
|
||||
self.mandatory, self.settable)
|
||||
extras = ['default=%s' % repr(self.default)]
|
||||
if self.export:
|
||||
extras.append('extname=%r' % self.extname)
|
||||
extras.append('export=%r' % self.export)
|
||||
if self.mandatory:
|
||||
extras.append('mandatory=True')
|
||||
if not self.settable:
|
||||
extras.append('settable=False')
|
||||
if self.value is not UNSET:
|
||||
extras.append('value=%s' % repr(self.value))
|
||||
if not self.name:
|
||||
extras.append('name=%r' % self.name)
|
||||
return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras))
|
||||
|
||||
|
||||
class Properties(OrderedDict):
|
||||
"""a collection of `Property` objects
|
||||
|
||||
checks values upon assignment.
|
||||
You can either assign a Property object, or a value
|
||||
(which must pass the validator of the already existing Property)
|
||||
"""
|
||||
def __setitem__(self, key, value):
|
||||
if not isinstance(value, Property):
|
||||
raise ProgrammingError('setting property %r on classes is not supported!' % key)
|
||||
# make sure, extname is valid if export is True
|
||||
if not value.extname and value.export:
|
||||
value.extname = '_%s' % key # generate custom key
|
||||
elif value.extname and not value.export:
|
||||
value.export = True
|
||||
OrderedDict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ProgrammingError('deleting Properties is not supported!')
|
||||
|
||||
|
||||
class PropertyMeta(type):
|
||||
"""Metaclass for HasProperties
|
||||
|
||||
joining the class's properties with those of base classes.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if '__constructed__' in attrs:
|
||||
return newtype
|
||||
|
||||
newtype = cls.__join_properties__(newtype, name, bases, attrs)
|
||||
|
||||
attrs['__constructed__'] = True
|
||||
return newtype
|
||||
|
||||
@classmethod
|
||||
def __join_properties__(cls, newtype, name, bases, attrs):
|
||||
# merge properties from all sub-classes
|
||||
properties = Properties()
|
||||
for base in reversed(bases):
|
||||
properties.update(getattr(base, "properties", {}))
|
||||
# update with properties from new class
|
||||
properties.update(attrs.get('properties', {}))
|
||||
newtype.properties = properties
|
||||
|
||||
# generate getters
|
||||
for k, po in properties.items():
|
||||
|
||||
def getter(self, pname=k):
|
||||
val = self.__class__.properties[pname].default
|
||||
return self.properties.get(pname, val)
|
||||
|
||||
if k in attrs and not isinstance(attrs[k], property):
|
||||
if callable(attrs[k]):
|
||||
raise ProgrammingError('%r: property %r collides with method'
|
||||
% (newtype, k))
|
||||
# store the attribute value for putting on the instance later
|
||||
try:
|
||||
# for inheritance reasons, it seems best to store it as a renamed attribute
|
||||
setattr(newtype, '_initProp_' + k, po.datatype(attrs[k]))
|
||||
except BadValueError:
|
||||
raise ProgrammingError('%r: property %r can not be set to %r'
|
||||
% (newtype, k, attrs[k]))
|
||||
setattr(newtype, k, property(getter))
|
||||
|
||||
# add property information to the doc string
|
||||
def fmt_property(name, prop):
|
||||
desc = indent_description(prop)
|
||||
if '(' in desc[0:2]:
|
||||
dtinfo = ''
|
||||
else:
|
||||
dtinfo = [prop.datatype.short_doc(), None if prop.export else 'hidden']
|
||||
dtinfo = ', '.join(filter(None, dtinfo))
|
||||
if dtinfo:
|
||||
dtinfo = '*(%s)* ' % dtinfo
|
||||
return '- **%s** - %s%s\n' % (name, dtinfo, desc)
|
||||
|
||||
append_to_doc(newtype, 'properties', 'SECOP Properties',
|
||||
'properties', attrs.get("properties", {}), fmt_property)
|
||||
|
||||
return newtype
|
||||
|
||||
|
||||
class HasProperties(metaclass=PropertyMeta):
|
||||
properties = {}
|
||||
class HasProperties(HasDescriptors):
|
||||
propertyValues = None
|
||||
|
||||
def __init__(self):
|
||||
super(HasProperties, self).__init__()
|
||||
self.initProperties()
|
||||
|
||||
def initProperties(self):
|
||||
# store property values in the instance, keep descriptors on the class
|
||||
self.properties = {}
|
||||
# pre-init with properties default value (if any)
|
||||
for pn, po in self.__class__.properties.items():
|
||||
value = getattr(self, '_initProp_' + pn, self)
|
||||
if value is not self: # property value was given as attribute
|
||||
self.properties[pn] = value
|
||||
elif not po.mandatory:
|
||||
self.properties[pn] = po.default
|
||||
self.propertyValues = {}
|
||||
# pre-init
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.value is not UNSET:
|
||||
self.setProperty(pn, po.value)
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
super().__init_subclass__()
|
||||
# raise an error when an attribute is a tuple with one single descriptor as element
|
||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||
bad = [k for k, v in cls.__dict__.items()
|
||||
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||
if bad:
|
||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||
properties = {}
|
||||
for base in cls.__bases__:
|
||||
properties.update(getattr(base, 'propertyDict', {}))
|
||||
properties.update(cls.filterDescriptors(Property))
|
||||
cls.propertyDict = properties
|
||||
# treat overriding properties with bare values
|
||||
for pn, po in properties.items():
|
||||
value = cls.__dict__.get(pn, po)
|
||||
if not isinstance(value, Property): # attribute is a bare value
|
||||
po = Property(**po.__dict__)
|
||||
try:
|
||||
po.value = po.datatype(value)
|
||||
except BadValueError:
|
||||
for base in cls.__bases__:
|
||||
if pn in getattr(base, 'propertyDict', {}):
|
||||
if callable(value):
|
||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||
(cls.__name__, pn, base.__name__))
|
||||
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||
(cls.__name__, pn, value))
|
||||
cls.propertyDict[pn] = po
|
||||
|
||||
def checkProperties(self):
|
||||
"""validates properties and checks for min... <= max..."""
|
||||
for pn, po in self.__class__.properties.items():
|
||||
if po.export and po.mandatory:
|
||||
if pn not in self.properties:
|
||||
name = getattr(self, 'name', repr(self))
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.mandatory:
|
||||
if pn not in self.propertyDict:
|
||||
name = getattr(self, 'name', self.__class__.__name__)
|
||||
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||
# apply validator (which may complain further)
|
||||
self.properties[pn] = po.datatype(self.properties[pn])
|
||||
for pn, po in self.__class__.properties.items():
|
||||
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
||||
for pn, po in self.propertyDict.items():
|
||||
if pn.startswith('min'):
|
||||
maxname = 'max' + pn[3:]
|
||||
minval = self.properties[pn]
|
||||
maxval = self.properties.get(maxname, minval)
|
||||
minval = self.propertyValues.get(pn, po.default)
|
||||
maxval = self.propertyValues.get(maxname, minval)
|
||||
if minval > maxval:
|
||||
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
||||
|
||||
|
||||
def getProperties(self):
|
||||
return self.__class__.properties
|
||||
return self.propertyDict
|
||||
|
||||
def exportProperties(self):
|
||||
# export properties which have
|
||||
# export=True and
|
||||
# mandatory=True or non_default=True
|
||||
res = {}
|
||||
for pn, po in self.__class__.properties.items():
|
||||
val = self.properties.get(pn, None)
|
||||
if po.export and (po.mandatory or val != po.default):
|
||||
for pn, po in self.propertyDict.items():
|
||||
val = self.propertyValues.get(pn, po.default)
|
||||
if po.export and (po.export == 'always' or val != po.default):
|
||||
try:
|
||||
val = po.datatype.export_value(val)
|
||||
except AttributeError:
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
res[po.extname] = val
|
||||
return res
|
||||
|
||||
def setProperty(self, key, value):
|
||||
self.properties[key] = self.__class__.properties[key].datatype(value)
|
||||
# this is overwritten by Param.setProperty and DataType.setProperty
|
||||
# in oder to extend setting to inner properties
|
||||
# otherwise direct setting of self.<key> = value is preferred
|
||||
self.propertyValues[key] = self.propertyDict[key].datatype(value)
|
||||
|
@ -42,8 +42,8 @@ import threading
|
||||
from collections import OrderedDict
|
||||
from time import time as currenttime
|
||||
|
||||
from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError, fmt_error
|
||||
from secop.errors import NoSuchCommandError, NoSuchModuleError, \
|
||||
NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError
|
||||
from secop.params import Parameter
|
||||
from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \
|
||||
DISABLEEVENTSREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, \
|
||||
@ -54,9 +54,9 @@ def make_update(modulename, pobj):
|
||||
if pobj.readerror:
|
||||
return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||
# error-report !
|
||||
[pobj.readerror.name, fmt_error(pobj.readerror), dict(t=pobj.timestamp)])
|
||||
[pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)])
|
||||
return (EVENTREPLY, '%s:%s' % (modulename, pobj.export),
|
||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||
[pobj.export_value(), dict(t=pobj.timestamp)])
|
||||
|
||||
|
||||
class Dispatcher:
|
||||
@ -109,7 +109,7 @@ class Dispatcher:
|
||||
self._subscriptions.setdefault(eventname, set()).add(conn)
|
||||
|
||||
def unsubscribe(self, conn, eventname):
|
||||
if not ':' in eventname:
|
||||
if ':' not in eventname:
|
||||
# also remove 'more specific' subscriptions
|
||||
for k, v in self._subscriptions.items():
|
||||
if k.startswith('%s:' % eventname):
|
||||
@ -177,7 +177,7 @@ class Dispatcher:
|
||||
result = {'modules': OrderedDict()}
|
||||
for modulename in self._export:
|
||||
module = self.get_module(modulename)
|
||||
if not module.properties.get('export', False):
|
||||
if not module.export:
|
||||
continue
|
||||
# some of these need rework !
|
||||
mod_desc = {'accessibles': self.export_accessibles(modulename)}
|
||||
@ -186,7 +186,7 @@ class Dispatcher:
|
||||
result['modules'][modulename] = mod_desc
|
||||
result['equipment_id'] = self.equipment_id
|
||||
result['firmware'] = 'FRAPPY - The Python Framework for SECoP'
|
||||
result['version'] = '2019.08'
|
||||
result['version'] = '2021.02'
|
||||
result.update(self.nodeprops)
|
||||
return result
|
||||
|
||||
@ -195,40 +195,24 @@ class Dispatcher:
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
cmdname = moduleobj.commands.exported.get(exportedname, None)
|
||||
if cmdname is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname))
|
||||
cmdspec = moduleobj.commands[cmdname]
|
||||
if argument is None and cmdspec.datatype.argument is not None:
|
||||
raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname))
|
||||
|
||||
if argument is not None and cmdspec.datatype.argument is None:
|
||||
raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname))
|
||||
|
||||
if cmdspec.datatype.argument:
|
||||
# validate!
|
||||
argument = cmdspec.datatype(argument)
|
||||
cname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
cobj = moduleobj.commands.get(cname)
|
||||
if cobj is None:
|
||||
raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname))
|
||||
|
||||
# now call func
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
func = getattr(moduleobj, 'do_' + cmdname)
|
||||
res = func() if argument is None else func(argument)
|
||||
|
||||
# pipe through cmdspec.datatype.result
|
||||
if cmdspec.datatype.result:
|
||||
res = cmdspec.datatype.result(res)
|
||||
|
||||
return res, dict(t=currenttime())
|
||||
return cobj.do(moduleobj, argument), dict(t=currenttime())
|
||||
|
||||
def _setParameterValue(self, modulename, exportedname, value):
|
||||
moduleobj = self.get_module(modulename)
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
pobj = moduleobj.parameters.get(pname)
|
||||
if pobj is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||
if pobj.constant is not None:
|
||||
raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely"
|
||||
% (modulename, pname))
|
||||
@ -252,10 +236,10 @@ class Dispatcher:
|
||||
if moduleobj is None:
|
||||
raise NoSuchModuleError('Module %r does not exist' % modulename)
|
||||
|
||||
pname = moduleobj.parameters.exported.get(exportedname, None)
|
||||
if pname is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname))
|
||||
pobj = moduleobj.parameters[pname]
|
||||
pname = moduleobj.accessiblename2attr.get(exportedname)
|
||||
pobj = moduleobj.parameters.get(pname)
|
||||
if pobj is None:
|
||||
raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname))
|
||||
if pobj.constant is not None:
|
||||
# really needed? we could just construct a readreply instead....
|
||||
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||
@ -321,15 +305,13 @@ class Dispatcher:
|
||||
return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data)))
|
||||
|
||||
def handle_do(self, conn, specifier, data):
|
||||
# XXX: should this be done asyncron? we could just return the reply in
|
||||
# that case
|
||||
modulename, cmd = specifier.split(':', 1)
|
||||
return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data)))
|
||||
|
||||
def handle_ping(self, conn, specifier, data):
|
||||
if data:
|
||||
raise ProtocolError('ping requests don\'t take data!')
|
||||
return (HEARTBEATREPLY, specifier, [None, {'t':currenttime()}])
|
||||
return (HEARTBEATREPLY, specifier, [None, {'t': currenttime()}])
|
||||
|
||||
def handle_activate(self, conn, specifier, data):
|
||||
if data:
|
||||
|
@ -25,6 +25,7 @@ import json
|
||||
|
||||
EOL = b'\n'
|
||||
|
||||
|
||||
def encode_msg_frame(action, specifier=None, data=None):
|
||||
""" encode a msg_triple into an msg_frame, ready to be sent
|
||||
|
||||
|
@ -21,13 +21,12 @@
|
||||
# *****************************************************************************
|
||||
"""provides tcp interface to the SECoP Server"""
|
||||
|
||||
import sys
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from secop.datatypes import StringType, BoolType
|
||||
from secop.datatypes import BoolType, StringType
|
||||
from secop.errors import SECoPError
|
||||
from secop.lib import formatException, \
|
||||
formatExtendedStack, formatExtendedTraceback
|
||||
@ -36,7 +35,6 @@ from secop.protocol.interface import decode_msg, encode_msg_frame, get_msg
|
||||
from secop.protocol.messages import ERRORPREFIX, \
|
||||
HELPREPLY, HELPREQUEST, HelpMessage
|
||||
|
||||
|
||||
DEF_PORT = 10767
|
||||
MESSAGE_READ_SIZE = 1024
|
||||
HELP = HELPREQUEST.encode()
|
||||
@ -134,7 +132,6 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
|
||||
if result[0].startswith(ERRORPREFIX) and not detailed_errors:
|
||||
# strip extra information
|
||||
result[2][2].clear()
|
||||
result[2][2]['t'] = time.time()
|
||||
self.send_reply(result)
|
||||
|
||||
def send_reply(self, data):
|
||||
|
@ -80,7 +80,6 @@ REQUEST2REPLY = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
HelpMessage = """Try one of the following:
|
||||
'%s' to query protocol version
|
||||
'%s' to read the description
|
||||
|
@ -34,12 +34,12 @@ simplifications:
|
||||
|
||||
import time
|
||||
|
||||
import secop.protocol.dispatcher
|
||||
import secop.errors
|
||||
from secop.protocol.messages import DESCRIPTIONREPLY, ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY,\
|
||||
READREQUEST, WRITEREQUEST, COMMANDREQUEST
|
||||
import secop.client
|
||||
import secop.errors
|
||||
import secop.protocol.dispatcher
|
||||
from secop.lib.multievent import MultiEvent
|
||||
from secop.protocol.messages import COMMANDREQUEST, DESCRIPTIONREPLY, \
|
||||
ENABLEEVENTSREPLY, ERRORPREFIX, EVENTREPLY, READREQUEST, WRITEREQUEST
|
||||
|
||||
|
||||
class SecopClient(secop.client.SecopClient):
|
||||
|
@ -21,21 +21,19 @@
|
||||
# *****************************************************************************
|
||||
"""SECoP proxy modules"""
|
||||
|
||||
from secop.params import Parameter, Command
|
||||
from secop.modules import Module, Writable, Readable, Drivable
|
||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||
from secop.datatypes import StringType
|
||||
from secop.errors import BadValueError, \
|
||||
CommunicationFailedError, ConfigError, make_secop_error
|
||||
from secop.lib import get_class
|
||||
from secop.modules import Drivable, Module, Readable, Writable
|
||||
from secop.params import Command, Parameter
|
||||
from secop.properties import Property
|
||||
from secop.stringio import HasIodev
|
||||
from secop.lib import get_class
|
||||
from secop.client import SecopClient, decode_msg, encode_msg_frame
|
||||
from secop.errors import ConfigError, make_secop_error, CommunicationFailedError
|
||||
|
||||
|
||||
class ProxyModule(HasIodev, Module):
|
||||
properties = {
|
||||
'module':
|
||||
Property('remote module name', datatype=StringType(), default=''),
|
||||
}
|
||||
module = Property('remote module name', datatype=StringType(), default='')
|
||||
|
||||
pollerClass = None
|
||||
_consistency_check_done = False
|
||||
@ -55,7 +53,7 @@ class ProxyModule(HasIodev, Module):
|
||||
|
||||
def initModule(self):
|
||||
if not self.module:
|
||||
self.properties['module'] = self.name
|
||||
self.module = self.name
|
||||
self._secnode = self._iodev.secnode
|
||||
self._secnode.register_callback(self.module, self.updateEvent,
|
||||
self.descriptiveDataChange, self.nodeStateChange)
|
||||
@ -90,10 +88,10 @@ class ProxyModule(HasIodev, Module):
|
||||
dt.compatible(pobj.datatype)
|
||||
except Exception:
|
||||
self.log.warning('remote parameter %s:%s is not fully compatible: %r != %r'
|
||||
% (self.module, pname, pobj.datatype, dt))
|
||||
% (self.module, pname, pobj.datatype, dt))
|
||||
except Exception:
|
||||
self.log.warning('remote parameter %s:%s has an incompatible datatype: %r != %r'
|
||||
% (self.module, pname, pobj.datatype, dt))
|
||||
% (self.module, pname, pobj.datatype, dt))
|
||||
while cmds:
|
||||
cname, cobj = cmds.popitem()
|
||||
props = remotecmds.get(cname)
|
||||
@ -103,9 +101,9 @@ class ProxyModule(HasIodev, Module):
|
||||
dt = props['datatype']
|
||||
try:
|
||||
cobj.datatype.compatible(dt)
|
||||
except Exception:
|
||||
except BadValueError:
|
||||
self.log.warning('remote command %s:%s is not compatible: %r != %r'
|
||||
% (self.module, pname, pobj.datatype, dt))
|
||||
% (self.module, cname, cobj.datatype, dt))
|
||||
# what to do if descriptive data does not match?
|
||||
# we might raise an exception, but this would lead to a reconnection,
|
||||
# which might not help.
|
||||
@ -141,14 +139,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule]
|
||||
|
||||
|
||||
class SecNode(Module):
|
||||
properties = {
|
||||
'uri':
|
||||
Property('uri of a SEC node', datatype=StringType()),
|
||||
}
|
||||
commands = {
|
||||
'request':
|
||||
Command('send a request', argument=StringType(), result=StringType())
|
||||
}
|
||||
uri = Property('uri of a SEC node', datatype=StringType())
|
||||
|
||||
def earlyInit(self):
|
||||
self.secnode = SecopClient(self.uri, self.log)
|
||||
@ -156,8 +147,9 @@ class SecNode(Module):
|
||||
def startModule(self, started_callback):
|
||||
self.secnode.spawn_connect(started_callback)
|
||||
|
||||
def do_request(self, msg):
|
||||
"""for test purposes"""
|
||||
@Command(StringType(), result=StringType())
|
||||
def request(self, msg):
|
||||
"""send a request, for debugging purposes"""
|
||||
reply = self.secnode.request(*decode_msg(msg.encode('utf-8')))
|
||||
return encode_msg_frame(*reply).decode('utf-8')
|
||||
|
||||
@ -184,17 +176,12 @@ def proxy_class(remote_class, name=None):
|
||||
else:
|
||||
raise ConfigError('%r is no SECoP module class' % remote_class)
|
||||
|
||||
parameters = {}
|
||||
commands = {}
|
||||
attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties)
|
||||
attrs = rcls.propertyDict.copy()
|
||||
|
||||
for aname, aobj in rcls.accessibles.items():
|
||||
if isinstance(aobj, Parameter):
|
||||
pobj = aobj.copy()
|
||||
parameters[aname] = pobj
|
||||
pobj.properties['poll'] = False
|
||||
pobj.properties['handler'] = None
|
||||
pobj.properties['needscfg'] = False
|
||||
pobj = aobj.override(poll=False, handler=None, needscfg=False)
|
||||
attrs[aname] = pobj
|
||||
|
||||
def rfunc(self, pname=aname):
|
||||
value, _, readerror = self._secnode.getParameter(self.name, pname)
|
||||
@ -216,12 +203,11 @@ def proxy_class(remote_class, name=None):
|
||||
|
||||
elif isinstance(aobj, Command):
|
||||
cobj = aobj.copy()
|
||||
commands[aname] = cobj
|
||||
|
||||
def cfunc(self, arg=None, cname=aname):
|
||||
return self._secnode.execCommand(self.name, cname, arg)
|
||||
|
||||
attrs['do_' + aname] = cfunc
|
||||
attrs[aname] = cobj(cfunc)
|
||||
|
||||
else:
|
||||
raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class))
|
||||
|
@ -23,13 +23,17 @@
|
||||
# *****************************************************************************
|
||||
"""Define helpers"""
|
||||
|
||||
import os
|
||||
from os.path import join, exists, dirname, isdir
|
||||
import ast
|
||||
import time
|
||||
import threading
|
||||
import configparser
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.errors import ConfigError
|
||||
from secop.lib import formatException, get_class, getGeneralConfig
|
||||
from secop.modules import Attached
|
||||
|
||||
try:
|
||||
from daemon import DaemonContext
|
||||
try:
|
||||
@ -39,10 +43,6 @@ try:
|
||||
except ImportError:
|
||||
DaemonContext = None
|
||||
|
||||
from secop.errors import ConfigError
|
||||
from secop.lib import formatException, get_class, getGeneralConfig
|
||||
from secop.modules import Attached
|
||||
from secop.params import PREDEFINED_ACCESSIBLES
|
||||
|
||||
try:
|
||||
import systemd.daemon
|
||||
@ -95,7 +95,11 @@ class Server:
|
||||
merged_cfg = OrderedDict()
|
||||
ambiguous_sections = set()
|
||||
for cfgfile in cfgfiles.split(','):
|
||||
cfgdict = self.loadCfgFile(cfgfile)
|
||||
if cfgfile.endswith('.cfg') and os.path.exists(cfgfile):
|
||||
filename = cfgfile
|
||||
else:
|
||||
filename = os.path.join(cfg['confdir'], cfgfile + '.cfg')
|
||||
cfgdict = self.loadCfgFile(filename)
|
||||
ambiguous_sections |= set(merged_cfg) & set(cfgdict)
|
||||
merged_cfg.update(cfgdict)
|
||||
self.node_cfg = merged_cfg.pop('NODE', {})
|
||||
@ -112,22 +116,9 @@ class Server:
|
||||
if ambiguous_sections:
|
||||
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
|
||||
self._cfgfiles = cfgfiles
|
||||
self._pidfile = join(cfg['piddir'], name + '.pid')
|
||||
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
|
||||
|
||||
def loadCfgFile(self, cfgfile):
|
||||
if not cfgfile.endswith('.cfg'):
|
||||
cfgfile += '.cfg'
|
||||
if '/' in cfgfile: # specified as full path
|
||||
filename = cfgfile if exists(cfgfile) else None
|
||||
else:
|
||||
cfg = getGeneralConfig()
|
||||
for filename in [join(d, cfgfile) for d in cfg['confdir'].split(':')]:
|
||||
if exists(filename):
|
||||
break
|
||||
else:
|
||||
filename = None
|
||||
if filename is None:
|
||||
raise ConfigError("Couldn't find cfg file %r in %s" % (cfgfile, cfg['confdir']))
|
||||
def loadCfgFile(self, filename):
|
||||
self.log.debug('Parse config file %s ...' % filename)
|
||||
result = OrderedDict()
|
||||
parser = configparser.ConfigParser()
|
||||
@ -165,8 +156,8 @@ class Server:
|
||||
def start(self):
|
||||
if not DaemonContext:
|
||||
raise ConfigError('can not daemonize, as python-daemon is not installed')
|
||||
piddir = dirname(self._pidfile)
|
||||
if not isdir(piddir):
|
||||
piddir = os.path.dirname(self._pidfile)
|
||||
if not os.path.isdir(piddir):
|
||||
os.makedirs(piddir)
|
||||
pidfile = pidlockfile.TimeoutPIDLockFile(self._pidfile)
|
||||
|
||||
@ -238,7 +229,7 @@ class Server:
|
||||
# all objs created, now start them up and interconnect
|
||||
for modname, modobj in self.modules.items():
|
||||
self.log.info('registering module %r' % modname)
|
||||
self.dispatcher.register_module(modobj, modname, modobj.properties['export'])
|
||||
self.dispatcher.register_module(modobj, modname, modobj.export)
|
||||
if modobj.pollerClass is not None:
|
||||
# a module might be explicitly excluded from polling by setting pollerClass to None
|
||||
modobj.pollerClass.add_to_table(poll_table, modobj)
|
||||
@ -247,14 +238,16 @@ class Server:
|
||||
|
||||
# handle attached modules
|
||||
for modname, modobj in self.modules.items():
|
||||
for propname, propobj in modobj.__class__.properties.items():
|
||||
for propname, propobj in modobj.propertyDict.items():
|
||||
if isinstance(propobj, Attached):
|
||||
setattr(modobj, propobj.attrname or '_' + propname,
|
||||
self.dispatcher.get_module(modobj.properties[propname]))
|
||||
self.dispatcher.get_module(getattr(modobj, propname)))
|
||||
# call init on each module after registering all
|
||||
for modname, modobj in self.modules.items():
|
||||
modobj.initModule()
|
||||
|
||||
if self._testonly:
|
||||
return
|
||||
start_events = []
|
||||
for modname, modobj in self.modules.items():
|
||||
event = threading.Event()
|
||||
@ -271,10 +264,3 @@ class Server:
|
||||
if not event.wait(timeout=max(0, deadline - time.time())):
|
||||
self.log.info('WARNING: timeout when starting %s' % name)
|
||||
self.log.info('all modules and pollers started')
|
||||
history_path = os.environ.get('FRAPPY_HISTORY')
|
||||
if history_path:
|
||||
from secop.histwriter import HistWriter
|
||||
writer = HistWriter(history_path, PREDEFINED_ACCESSIBLES.keys(), self.dispatcher)
|
||||
# treat writer as a connection
|
||||
self.dispatcher.add_connection(writer)
|
||||
writer.init(self.dispatcher.handle_describe(writer, None, None))
|
||||
|
@ -22,12 +22,15 @@
|
||||
"""Define Simulation classes"""
|
||||
|
||||
|
||||
# TODO: rework after syntax change!
|
||||
|
||||
import random
|
||||
from time import sleep
|
||||
|
||||
from secop.datatypes import FloatRange
|
||||
from secop.lib import mkthread
|
||||
from secop.modules import Drivable, Module, Parameter, Readable, Writable, BasicPoller
|
||||
from secop.modules import BasicPoller, Drivable, \
|
||||
Module, Parameter, Readable, Writable
|
||||
|
||||
|
||||
class SimBase:
|
||||
|
@ -23,15 +23,18 @@
|
||||
implements TCP/IP and is be used as a base for SerialIO
|
||||
"""
|
||||
|
||||
import time
|
||||
import threading
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from secop.datatypes import ArrayOf, BoolType, \
|
||||
FloatRange, StringType, TupleOf, ValueType
|
||||
from secop.errors import CommunicationFailedError, \
|
||||
CommunicationSilentError, ConfigError
|
||||
from secop.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Override
|
||||
from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType
|
||||
from secop.errors import CommunicationFailedError, CommunicationSilentError
|
||||
from secop.modules import Attached, Command, \
|
||||
Communicator, Done, Module, Parameter, Property
|
||||
from secop.poller import REGULAR
|
||||
from secop.metaclass import Done
|
||||
|
||||
|
||||
class StringIO(Communicator):
|
||||
@ -39,48 +42,22 @@ class StringIO(Communicator):
|
||||
|
||||
self healing is assured by polling the parameter 'is_connected'
|
||||
"""
|
||||
properties = {
|
||||
'uri':
|
||||
Property('hostname:portnumber', datatype=StringType()),
|
||||
'end_of_line':
|
||||
Property('end_of_line character', datatype=ValueType(),
|
||||
default='\n', settable=True),
|
||||
'encoding':
|
||||
Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True),
|
||||
'identification':
|
||||
Property('identification\n\n'
|
||||
'a list of tuples with commands and expected responses as regexp, '
|
||||
'to be sent on connect',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False),
|
||||
}
|
||||
parameters = {
|
||||
'timeout':
|
||||
Parameter('timeout', datatype=FloatRange(0), default=2),
|
||||
'wait_before':
|
||||
Parameter('wait time before sending', datatype=FloatRange(), default=0),
|
||||
'is_connected':
|
||||
Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR),
|
||||
'pollinterval':
|
||||
Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10),
|
||||
}
|
||||
commands = {
|
||||
'communicate':
|
||||
Override('''
|
||||
send a command and receive a reply
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
end_of_line = Property('end_of_line character', datatype=ValueType(),
|
||||
default='\n', settable=True)
|
||||
encoding = Property('used encoding', datatype=StringType(),
|
||||
default='ascii', settable=True)
|
||||
identification = Property('''
|
||||
identification
|
||||
|
||||
- using end_of_line, encoding and self._lock
|
||||
- for commands without reply, the command must be joined with a query command,
|
||||
- wait_before is respected for end_of_lines within a command
|
||||
'''),
|
||||
'multicomm':
|
||||
Command('''
|
||||
execute multiple commands in one go
|
||||
|
||||
assuring that no other thread calls commands in between
|
||||
''',
|
||||
argument=ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
}
|
||||
a list of tuples with commands and expected responses as regexp,
|
||||
to be sent on connect''',
|
||||
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
|
||||
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
|
||||
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||
|
||||
_reconnectCallbacks = None
|
||||
|
||||
@ -115,11 +92,12 @@ class StringIO(Communicator):
|
||||
self._conn = AsynConn(uri, self._eol_read)
|
||||
self.is_connected = True
|
||||
for command, regexp in self.identification:
|
||||
reply = self.do_communicate(command)
|
||||
reply = self.communicate(command)
|
||||
if not re.match(regexp, reply):
|
||||
self.closeConnection()
|
||||
raise CommunicationFailedError('bad response: %s does not match %s' %
|
||||
(reply, regexp))
|
||||
|
||||
def closeConnection(self):
|
||||
"""close connection
|
||||
|
||||
@ -135,7 +113,7 @@ class StringIO(Communicator):
|
||||
self.is_connected is changed only by self.connectStart or self.closeConnection
|
||||
"""
|
||||
if self.is_connected:
|
||||
return Done # no need for intermediate updates
|
||||
return Done # no need for intermediate updates
|
||||
try:
|
||||
self.connectStart()
|
||||
if self._last_error:
|
||||
@ -180,9 +158,17 @@ class StringIO(Communicator):
|
||||
if removeme:
|
||||
self._reconnectCallbacks.pop(key)
|
||||
|
||||
def do_communicate(self, command):
|
||||
def communicate(self, command):
|
||||
"""send a command and receive a reply
|
||||
|
||||
using end_of_line, encoding and self._lock
|
||||
for commands without reply, the command must be joined with a query command,
|
||||
wait_before is respected for end_of_lines within a command.
|
||||
"""
|
||||
if not self.is_connected:
|
||||
self.read_is_connected() # try to reconnect
|
||||
if not self._conn:
|
||||
raise CommunicationSilentError('can not connect to %r' % self.uri)
|
||||
try:
|
||||
with self._lock:
|
||||
# read garbage and wait before send
|
||||
@ -214,11 +200,13 @@ class StringIO(Communicator):
|
||||
self.log.error(self._last_error)
|
||||
raise
|
||||
|
||||
def do_multicomm(self, commands):
|
||||
@Command(ArrayOf(StringType()), result=ArrayOf(StringType()))
|
||||
def multicomm(self, commands):
|
||||
"""communicate multiple request/replies in one row"""
|
||||
replies = []
|
||||
with self._lock:
|
||||
for cmd in commands:
|
||||
replies.append(self.do_communicate(cmd))
|
||||
replies.append(self.communicate(cmd))
|
||||
return replies
|
||||
|
||||
|
||||
@ -227,16 +215,15 @@ class HasIodev(Module):
|
||||
|
||||
not only StringIO !
|
||||
"""
|
||||
properties = {
|
||||
'iodev': Attached(),
|
||||
'uri': Property('uri for automatic creation of the attached communication module', StringType(), default=''),
|
||||
}
|
||||
iodev = Attached()
|
||||
uri = Property('uri for automatic creation of the attached communication module',
|
||||
StringType(), default='')
|
||||
|
||||
iodevDict = {}
|
||||
|
||||
def __init__(self, name, logger, opts, srv):
|
||||
iodev = opts.get('iodev')
|
||||
super().__init__(name, logger, opts, srv)
|
||||
Module.__init__(self, name, logger, opts, srv)
|
||||
if self.uri:
|
||||
opts = {'uri': self.uri, 'description': 'communication device for %s' % name,
|
||||
'export': False}
|
||||
@ -246,7 +233,9 @@ class HasIodev(Module):
|
||||
iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv)
|
||||
srv.modules[ioname] = iodev
|
||||
self.iodevDict[self.uri] = ioname
|
||||
self.setProperty('iodev', ioname)
|
||||
self.iodev = ioname
|
||||
elif not self.iodev:
|
||||
raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name)
|
||||
|
||||
def initModule(self):
|
||||
try:
|
||||
@ -257,4 +246,4 @@ class HasIodev(Module):
|
||||
super().initModule()
|
||||
|
||||
def sendRecv(self, command):
|
||||
return self._iodev.do_communicate(command)
|
||||
return self._iodev.communicate(command)
|
||||
|
@ -47,7 +47,7 @@ def get_git_version(abbrev=4, cwd=None):
|
||||
# mangle version to comply with pep440
|
||||
if version.count('-'):
|
||||
version, patchcount, githash = version.split('-')
|
||||
version += '.post%s+%s' %(patchcount, githash)
|
||||
version += '.post%s+%s' % (patchcount, githash)
|
||||
return version
|
||||
except Exception:
|
||||
return None
|
||||
|
Reference in New Issue
Block a user