Compare commits

...

12 Commits
wip ... work

Author SHA1 Message Date
b88750ae2b update checklist for git branch sync in README.md
Change-Id: I788fba8cd748be090f82442ad20c839f206e5e3c
2021-03-18 13:47:26 +01:00
9c68f582b7 remove debug print statements
Change-Id: I51dc66b9cecb0f5e84bef36d1b4a541ddefcfbc2
2021-03-18 13:45:46 +01:00
c89b4a44bb fix inheritance problem with mixin
- a mixin should not inherit from module then it has Parameters
- Parameters in mixins must be complete, not just overrides
- check precedence of read_<param> or handler

Change-Id: I72d9355a1982770d1a99d9552a20330103c97edb
2021-03-18 13:45:46 +01:00
330aa15400 more fixes for sea
Change-Id: I195bcdbe5f6b274e65dd431ed13a123c74a8d5bf
2021-03-18 13:45:46 +01:00
3435107948 fix access to sea config dir
- removed unused property json_path
- do not take the first directory in config path, but the first
  directory with a sea subdirectory

Change-Id: I4f0d72936ca616134c38568d88c57a33a3397ec6
2021-03-18 13:45:46 +01:00
7c0101f6bd fix user friendly reporting of config errors
Change-Id: I42ec1ad2e18a0363fb7317c86743ff2f6665f049
2021-03-18 13:45:46 +01:00
6ea97b0012 user friendly reporting of config errors
Config errors are collected first, and raised after processing
all modules. This is more user friendly.

+ remove redundant check for predefined accessibles in modules.py
+ fixed error handling for exporting parameters in params.py
+ fixed handling of bare attributes overwriting properties

Change-Id: I894bda291ab85ccec3d771c4903393c808af0a2a
2021-03-18 13:45:46 +01:00
3dc159a9a4 user friendly reporting of config errors
Config errors are collected first, and raised after processing
all modules. This is more user friendly.

+ remove redundant check for predefined accessibles in modules.py
+ fixed error handling for exporting parameters in params.py
+ fixed handling of bare attributes overwriting properties

Change-Id: I894bda291ab85ccec3d771c4903393c808af0a2a
2021-03-18 13:45:46 +01:00
bd14e0a0e5 improved softcal
- better error handling
- bug fix
Change-Id: I43bdd4aa35723f43f9e4baf2723af812f04689d3

Change-Id: I5990c75a7a8153e95abee9548475783ee893bd08
2021-03-18 13:45:46 +01:00
l_samenv
4922f0d664 fixed bugs from syntax migration
- a new wrapper for a read function is not only to be created when
  the a new read method is in the class dict, but also when
  it is inherited, but not yet wrapped
- a handler must not be ignored, when a write method is inherited
- a proxy class must not call checkProperties

+ remove trailing spaces in tutorial_helevel.rst

Change-Id: I16024c14232ea200db91a1bc07ec23326219ab68
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25093
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2021-03-18 13:45:46 +01:00
9b71d3621d make softcal more tolerant reading .340 files
accept tab instead of space in 'Data Format' header keyword

Change-Id: I77e5500da5eaed6fd1097723533bc86207fa73d8
2021-03-05 08:57:23 +01:00
l_samenv
bec3359069 fixed bugs from syntax migration
- a new wrapper for a read function is not only to be created when
  the a new read function is in the class dict, but also when
  it inherited, but not yet wrapped
- a proxy class must not call checkProperties
2021-03-03 14:35:21 +01:00
14 changed files with 230 additions and 143 deletions

View File

@ -42,12 +42,11 @@ changes are done, eventually a sync step should happen:
- core commits already pushed through gerrit are skipped - core commits already pushed through gerrit are skipped
- all other commits are to be cherry-picked - all other commits are to be cherry-picked
7) when arrived at the point where the new working version should be, 7) when arrived at the point where the new working version should be,
copy new_wip branch to work with 'git checkout -B work'. copy new_wip branch to work with 'git checkout work;git checkout new_wip .'
Not sure if this works, as work is to be pushed to git.psi.ch. (note the dot!) and then commit this.
We might first remove the remote branch with 'git push origin --delete work'. 8) continue with (6) if wip and work should differ
And then create again (git push origin work)? 9) do like (7), but for wip branch
8) continue with (6) if wip and work should differ, and do like (7) for wip branch 10) delete new_wip branch, push master, wip and work branches
9) delete new_wip branch, push master, wip and work branches
## Procedure to update PPMS ## Procedure to update PPMS

View File

@ -495,9 +495,12 @@ class SecopClient(ProxyClient):
def _set_state(self, online, state=None): def _set_state(self, online, state=None):
# treat reconnecting as online! # treat reconnecting as online!
state = state or self.state state = state or self.state
try:
self.callback(None, 'nodeStateChange', online, state) self.callback(None, 'nodeStateChange', online, state)
for mname in self.modules: for mname in self.modules:
self.callback(mname, 'nodeStateChange', online, state) self.callback(mname, 'nodeStateChange', online, state)
except Exception as e:
self.log.error('ERROR in nodeStateCallback %s', e)
# set online attribute after callbacks -> callback may check for old state # set online attribute after callbacks -> callback may check for old state
self.online = online self.online = online
self.state = state self.state = state

View File

@ -98,14 +98,17 @@ def clamp(_min, value, _max):
def get_class(spec): def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)""" """loads a class given by string in dotted notation (as python would do)"""
modname, classname = spec.rsplit('.', 1) modname, classname = spec.rsplit('.', 1)
if modname.startswith('secop'): if modname.startswith('secop'):
module = importlib.import_module(modname) module = importlib.import_module(modname)
else: else:
# rarely needed by now.... # rarely needed by now....
module = importlib.import_module('secop.' + modname) module = importlib.import_module('secop.' + modname)
try:
return getattr(module, classname) return getattr(module, classname)
except AttributeError:
raise AttributeError('no such class') from None
def mkthread(func, *args, **kwds): def mkthread(func, *args, **kwds):

View File

@ -30,9 +30,9 @@ from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
IntRange, StatusType, StringType, TextType, TupleOf, get_datatype IntRange, StatusType, StringType, TextType, TupleOf, get_datatype
from secop.errors import BadValueError, ConfigError, InternalError, \ from secop.errors import BadValueError, ConfigError, InternalError, \
ProgrammingError, SECoPError, SilentError, secop_error ProgrammingError, SECoPError, SilentError, secop_error
from secop.lib import formatException, formatExtendedStack, mkthread from secop.lib import formatException, mkthread
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.params import PREDEFINED_ACCESSIBLES, Accessible, Command, Parameter from secop.params import Accessible, Command, Parameter
from secop.poller import BasicPoller, Poller from secop.poller import BasicPoller, Poller
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
@ -89,16 +89,21 @@ class HasAccessibles(HasProperties):
if isinstance(pobj, Command): if isinstance(pobj, Command):
# nothing to do for now # nothing to do for now
continue continue
rfunc = cls.__dict__.get('read_' + pname, None) rfunc = getattr(cls, 'read_' + pname, None)
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
wrapped = hasattr(rfunc, '__wrapped__')
if rfunc_handler: if rfunc_handler:
if rfunc: if 'read_' + pname in cls.__dict__:
if pname in cls.__dict__:
raise ProgrammingError("parameter '%s' can not have a handler " raise ProgrammingError("parameter '%s' can not have a handler "
"and read_%s" % (pname, pname)) "and read_%s" % (pname, pname))
# read_<pname> overwrites inherited handler
else:
rfunc = rfunc_handler rfunc = rfunc_handler
wrapped = False
# create wrapper except when read function is already wrapped # create wrapper except when read function is already wrapped
if rfunc is None or getattr(rfunc, '__wrapped__', False) is False: if not wrapped:
def wrapped_rfunc(self, pname=pname, rfunc=rfunc): def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
if rfunc: if rfunc:
@ -126,11 +131,14 @@ class HasAccessibles(HasProperties):
if not pobj.readonly: if not pobj.readonly:
wfunc = getattr(cls, 'write_' + pname, None) wfunc = getattr(cls, 'write_' + pname, None)
if wfunc is None: # ignore the handler, if a write function is present wrapped = hasattr(wfunc, '__wrapped__')
wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None if (wfunc is None or wrapped) and pobj.handler:
# ignore the handler, if a write function is present
wfunc = pobj.handler.get_write_func(pname)
wrapped = False
# create wrapper except when write function is already wrapped # create wrapper except when write function is already wrapped
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False: if not wrapped:
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc): def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("check validity of %s = %r" % (pname, value)) self.log.debug("check validity of %s = %r" % (pname, value))
@ -232,29 +240,27 @@ class Module(HasAccessibles):
self.name = name self.name = name
self.valueCallbacks = {} self.valueCallbacks = {}
self.errorCallbacks = {} self.errorCallbacks = {}
errors = []
# handle module properties # handle module properties
# 1) make local copies of properties # 1) make local copies of properties
super().__init__() super().__init__()
# 2) check and apply properties specified in cfgdict # 2) check and apply properties specified in cfgdict as
# specified as '.<propertyname> = <propertyvalue>' # '<propertyname> = <propertyvalue>'
# (this is for legacy config files only) for key in self.propertyDict:
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter value = cfgdict.pop(key, None)
if k[0] == '.': if value is None:
if k[1:] in self.propertyDict: # legacy cfg: specified as '.<propertyname> = <propertyvalue>'
self.setProperty(k[1:], cfgdict.pop(k)) value = cfgdict.pop('.' + key, None)
else: if value is not None:
raise ConfigError('Module %r has no property %r' % try:
(self.name, k[1:])) self.setProperty(key, value)
except BadValueError:
errors.append('module %s, %s: value %r does not match %r!' %
(name, key, value, self.propertyDict[key].datatype))
# 3) check and apply properties specified in cfgdict as # 3) set automatic properties
# '<propertyname> = <propertyvalue>' (without '.' prefix)
for k in self.propertyDict:
if k in cfgdict:
self.setProperty(k, cfgdict.pop(k))
# 4) set automatic properties
mycls = self.__class__ mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.implementation = myclassname self.implementation = myclassname
@ -288,16 +294,6 @@ class Module(HasAccessibles):
if not self.export: # do not export parameters of a module not exported if not self.export: # do not export parameters of a module not exported
aobj.export = False aobj.export = False
if aobj.export: if aobj.export:
if aobj.export is True:
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
if predefined_obj:
if isinstance(aobj, predefined_obj):
aobj.export = aname
else:
raise ProgrammingError("can not use '%s' as name of a %s" %
(aname, aobj.__class__.__name__))
else: # create custom parameter
aobj.export = '_' + aname
accessiblename2attr[aobj.export] = aname accessiblename2attr[aobj.export] = aname
accessibles[aname] = aobj accessibles[aname] = aobj
# do not re-use self.accessibles as this is the same for all instances # do not re-use self.accessibles as this is the same for all instances
@ -312,28 +308,30 @@ class Module(HasAccessibles):
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if '.' in k[1:]: if '.' in k[1:]:
paramname, propname = k.split('.', 1) paramname, propname = k.split('.', 1)
propvalue = cfgdict.pop(k)
paramobj = self.accessibles.get(paramname, None) paramobj = self.accessibles.get(paramname, None)
# paramobj might also be a command (not sure if this is needed) # paramobj might also be a command (not sure if this is needed)
if paramobj: if paramobj:
if propname == 'datatype': if propname == 'datatype':
paramobj.setProperty('datatype', get_datatype(cfgdict.pop(k), k)) propvalue = get_datatype(propvalue, k)
elif propname in paramobj.getProperties(): try:
paramobj.setProperty(propname, cfgdict.pop(k)) paramobj.setProperty(propname, propvalue)
else: except KeyError:
raise ConfigError('Module %s: Parameter %r has no property %r!' % errors.append('module %s: %s.%s does not exist' %
(self.name, paramname, propname)) (self.name, paramname, propname))
except BadValueError as e:
errors.append('module %s: %s.%s: %s' %
(self.name, paramname, propname, str(e)))
else: else:
raise ConfigError('Module %s has no Parameter %r!' % errors.append('module %s: %s not found' % (self.name, paramname))
(self.name, paramname))
# 3) check config for problems: # 3) check config for problems:
# only accept remaining config items specified in parameters # only accept remaining config items specified in parameters
for k, v in cfgdict.items(): bad = [k for k in cfgdict if k not in self.parameters]
if k not in self.parameters: if bad:
raise ConfigError( errors.append(
'Module %s:config Parameter %r ' 'module %s: %s does not exist (use one of %s)' %
'not understood! (use one of %s)' % (self.name, ', '.join(bad), ', '.join(list(self.parameters) +
(self.name, k, ', '.join(list(self.parameters) +
list(self.propertyDict)))) list(self.propertyDict))))
# 4) complain if a Parameter entry has no default value and # 4) complain if a Parameter entry has no default value and
@ -349,13 +347,13 @@ class Module(HasAccessibles):
# TODO: not sure about readonly (why not a parameter which can only be written from config?) # TODO: not sure about readonly (why not a parameter which can only be written from config?)
try: try:
pobj.value = pobj.datatype(cfgdict[pname]) pobj.value = pobj.datatype(cfgdict[pname])
except BadValueError as e:
raise ConfigError('%s.%s: %s' % (name, pname, e))
self.writeDict[pname] = pobj.value self.writeDict[pname] = pobj.value
except BadValueError as e:
errors.append('module %s, parameter %s: %s' % (name, pname, e))
else: else:
if pobj.default is None: if pobj.default is None:
if pobj.needscfg: if pobj.needscfg:
raise ConfigError('Parameter %s.%s has no default ' errors.append('module %s, parameter %s has no default '
'value and was not given in config!' % 'value and was not given in config!' %
(self.name, pname)) (self.name, pname))
# we do not want to call the setter for this parameter for now, # we do not want to call the setter for this parameter for now,
@ -368,8 +366,8 @@ class Module(HasAccessibles):
try: try:
value = pobj.datatype(pobj.default) value = pobj.datatype(pobj.default)
except BadValueError as e: except BadValueError as e:
raise ProgrammingError('bad default for %s.%s: %s' # this should not happen, as the default is already checked in Parameter
% (name, pname, e)) raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None
if pobj.initwrite and not pobj.readonly: if pobj.initwrite and not pobj.readonly:
# we will need to call write_<pname> # we will need to call write_<pname>
# if this is not desired, the default must not be given # if this is not desired, the default must not be given
@ -387,10 +385,8 @@ class Module(HasAccessibles):
# note: this will NOT call write_* methods! # note: this will NOT call write_* methods!
setattr(self, k, v) setattr(self, k, v)
except (ValueError, TypeError): except (ValueError, TypeError):
self.log.exception(formatExtendedStack()) # self.log.exception(formatExtendedStack())
raise errors.append('module %s, parameter %s: %s' % (self.name, k, e))
# raise ConfigError('Module %s: config parameter %r:\n%r' %
# (self.name, k, e))
cfgdict.pop(k) cfgdict.pop(k)
# Modify units AFTER applying the cfgdict # Modify units AFTER applying the cfgdict
@ -400,9 +396,18 @@ class Module(HasAccessibles):
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit)) dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
# 6) check complete configuration of * properties # 6) check complete configuration of * properties
if not errors:
try:
self.checkProperties() self.checkProperties()
for p in self.parameters.values(): except ConfigError as e:
errors.append('module %s: %s' % (name, e))
for pname, p in self.parameters.items():
try:
p.checkProperties() p.checkProperties()
except ConfigError:
errors.append('module %s, parameter %s: %s' % (name, pname, e))
if errors:
raise ConfigError('\n'.join(errors))
# helper cfg-editor # helper cfg-editor
def __iter__(self): def __iter__(self):

View File

@ -101,10 +101,10 @@ class Parameter(Accessible):
description = Property( description = Property(
'mandatory description of the parameter', TextType(), 'mandatory description of the parameter', TextType(),
extname='description', mandatory=True) extname='description', mandatory=True, export='always')
datatype = Property( datatype = Property(
'datatype of the Parameter (SECoP datainfo)', DataTypeType(), 'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
extname='datainfo', mandatory=True) extname='datainfo', mandatory=True, export='always')
readonly = Property( readonly = Property(
'not changeable via SECoP (default True)', BoolType(), 'not changeable via SECoP (default True)', BoolType(),
extname='readonly', default=True, export='always') extname='readonly', default=True, export='always')
@ -225,10 +225,13 @@ class Parameter(Accessible):
self.propertyValues.pop('default') self.propertyValues.pop('default')
if self.export is True: if self.export is True:
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))): predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Parameter:
self.export = name self.export = name
else: elif predefined_cls is None:
self.export = '_' + name self.export = '_' + name
else:
raise ProgrammingError('can not use %r as name of a Parameter' % name)
def copy(self): def copy(self):
# deep copy, as datatype might be altered from config # deep copy, as datatype might be altered from config
@ -280,7 +283,7 @@ class Command(Accessible):
description = Property( description = Property(
'description of the Command', TextType(), 'description of the Command', TextType(),
extname='description', export=True, mandatory=True) extname='description', export='always', mandatory=True)
group = Property( group = Property(
'optional command group of the command.', StringType(), 'optional command group of the command.', StringType(),
extname='group', export=True, default='') extname='group', export=True, default='')
@ -339,10 +342,13 @@ class Command(Accessible):
self.datatype = CommandType(self.argument, self.result) self.datatype = CommandType(self.argument, self.result)
if self.export is True: if self.export is True:
if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))): predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Command:
self.export = name self.export = name
else: elif predefined_cls is None:
self.export = '_' + name self.export = '_' + name
else:
raise ProgrammingError('can not use %r as name of a Command' % name)
def __get__(self, obj, owner=None): def __get__(self, obj, owner=None):
if obj is None: if obj is None:

View File

@ -158,7 +158,7 @@ class HasProperties(HasDescriptors):
cls.propertyDict = properties cls.propertyDict = properties
# treat overriding properties with bare values # treat overriding properties with bare values
for pn, po in properties.items(): for pn, po in properties.items():
value = cls.__dict__.get(pn, po) value = getattr(cls, pn, po)
if not isinstance(value, Property): # attribute is a bare value if not isinstance(value, Property): # attribute is a bare value
po = Property(**po.__dict__) po = Property(**po.__dict__)
try: try:
@ -177,11 +177,11 @@ class HasProperties(HasDescriptors):
"""validates properties and checks for min... <= max...""" """validates properties and checks for min... <= max..."""
for pn, po in self.propertyDict.items(): for pn, po in self.propertyDict.items():
if po.mandatory: if po.mandatory:
if pn not in self.propertyDict: try:
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.propertyValues[pn] = po.datatype(self.propertyValues[pn]) self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
except (KeyError, BadValueError):
name = getattr(self, 'name', self.__class__.__name__)
raise ConfigError('%s.%s needs a value of type %r!' % (name, pn, po.datatype))
for pn, po in self.propertyDict.items(): for pn, po in self.propertyDict.items():
if pn.startswith('min'): if pn.startswith('min'):
maxname = 'max' + pn[3:] maxname = 'max' + pn[3:]

View File

@ -122,6 +122,8 @@ class ProxyModule(HasIodev, Module):
self.announceUpdate(pname, None, readerror) self.announceUpdate(pname, None, readerror)
self.announceUpdate('status', newstatus) self.announceUpdate('status', newstatus)
def checkProperties(self):
pass # skip
class ProxyReadable(ProxyModule, Readable): class ProxyReadable(ProxyModule, Readable):
pass pass

View File

@ -26,11 +26,12 @@
import ast import ast
import configparser import configparser
import os import os
import sys
import threading import threading
import time import time
from collections import OrderedDict from collections import OrderedDict
from secop.errors import ConfigError from secop.errors import ConfigError, SECoPError
from secop.lib import formatException, get_class, getGeneralConfig from secop.lib import formatException, get_class, getGeneralConfig
from secop.modules import Attached from secop.modules import Attached
from secop.params import PREDEFINED_ACCESSIBLES from secop.params import PREDEFINED_ACCESSIBLES
@ -179,7 +180,7 @@ class Server:
self.run() self.run()
def unknown_options(self, cls, options): def unknown_options(self, cls, options):
raise ConfigError("%s class don't know how to handle option(s): %s" % return ("%s class don't know how to handle option(s): %s" %
(cls.__name__, ', '.join(options))) (cls.__name__, ', '.join(options)))
def run(self): def run(self):
@ -201,7 +202,7 @@ class Server:
cls = get_class(self.INTERFACES[scheme]) cls = get_class(self.INTERFACES[scheme])
with cls(scheme, self.log.getChild(scheme), opts, self) as self.interface: with cls(scheme, self.log.getChild(scheme), opts, self) as self.interface:
if opts: if opts:
self.unknown_options(cls, opts) raise ConfigError(self.unknown_options(cls, opts))
self.log.info('startup done, handling transport messages') self.log.info('startup done, handling transport messages')
if systemd: if systemd:
systemd.daemon.notify("READY=1\nSTATUS=accepting requests") systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
@ -219,20 +220,38 @@ class Server:
self.interface.shutdown() self.interface.shutdown()
def _processCfg(self): def _processCfg(self):
errors = []
opts = dict(self.node_cfg) opts = dict(self.node_cfg)
cls = get_class(opts.pop('class', 'protocol.dispatcher.Dispatcher')) cls = get_class(opts.pop('class', 'protocol.dispatcher.Dispatcher'))
self.dispatcher = cls(opts.pop('name', self._cfgfiles), self.log.getChild('dispatcher'), opts, self) self.dispatcher = cls(opts.pop('name', self._cfgfiles), self.log.getChild('dispatcher'), opts, self)
if opts: if opts:
self.unknown_options(cls, opts) errors.append(self.unknown_options(cls, opts))
self.modules = OrderedDict() self.modules = OrderedDict()
badclass = None
failed = set() # python modules failed to load
self.lastError = None
for modname, options in self.module_cfg.items(): for modname, options in self.module_cfg.items():
opts = dict(options) opts = dict(options)
cls = get_class(opts.pop('class')) try:
classname = opts.pop('class')
pymodule = classname.rpartition('.')[0]
if pymodule in failed:
continue
cls = get_class(classname)
modobj = cls(modname, self.log.getChild(modname), opts, self) modobj = cls(modname, self.log.getChild(modname), opts, self)
# all used args should be popped from opts! # all used args should be popped from opts!
if opts: if opts:
self.unknown_options(cls, opts) errors.append(self.unknown_options(cls, opts))
self.modules[modname] = modobj self.modules[modname] = modobj
except ConfigError as e:
errors.append(str(e))
except Exception as e:
if str(e) == 'no such class':
errors.append('%s not found' % classname)
else:
failed.add(pymodule)
badclass = classname
errors.append('error importing %s' % pymodule)
poll_table = dict() poll_table = dict()
# all objs created, now start them up and interconnect # all objs created, now start them up and interconnect
@ -249,12 +268,28 @@ class Server:
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
for propname, propobj in modobj.propertyDict.items(): for propname, propobj in modobj.propertyDict.items():
if isinstance(propobj, Attached): if isinstance(propobj, Attached):
try:
setattr(modobj, propobj.attrname or '_' + propname, setattr(modobj, propobj.attrname or '_' + propname,
self.dispatcher.get_module(getattr(modobj, propname))) self.dispatcher.get_module(getattr(modobj, propname)))
except SECoPError as e:
errors.append('module %s, attached %s: %s' % (modname, propname, str(e)))
# call init on each module after registering all # call init on each module after registering all
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
modobj.initModule() modobj.initModule()
if errors:
for errtxt in errors:
for line in errtxt.split('\n'):
self.log.error(line)
# print a list of config errors to stderr
sys.stderr.write('\n'.join(errors))
sys.stderr.write('\n')
if badclass:
# force stack trace for import of last erroneous module
get_class(badclass)
sys.exit(1)
if self._testonly: if self._testonly:
return return
start_events = [] start_events = []

View File

@ -45,7 +45,12 @@ def make_cvt_list(dt, tail=''):
result = [] result = []
for subkey, elmtype in items: for subkey, elmtype in items:
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)): for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts)) def conv(value, key=subkey, func=fun):
try:
return value[key]
except KeyError: # can not use value.get() because value might be a list
return None
result.append((conv, tail_, opts))
return result return result

View File

@ -128,8 +128,9 @@ class Main(Communicator):
return data # return data as string return data # return data as string
class PpmsBase(HasIodev, Readable): class PpmsMixin:
"""common base for all ppms modules""" """common base for all ppms modules"""
iodev = Attached() iodev = Attached()
pollerClass = Poller pollerClass = Poller
@ -139,7 +140,7 @@ class PpmsBase(HasIodev, Readable):
# as this pollinterval affects only the polling of settings # as this pollinterval affects only the polling of settings
# it would be confusing to export it. # it would be confusing to export it.
pollinterval = Parameter(export=False) pollinterval = Parameter('', FloatRange(), needscfg=False, export=False)
def initModule(self): def initModule(self):
self._iodev.register(self) self._iodev.register(self)
@ -172,7 +173,7 @@ class PpmsBase(HasIodev, Readable):
self.status = (self.Status.IDLE, '') self.status = (self.Status.IDLE, '')
class Channel(PpmsBase): class Channel(PpmsMixin, HasIodev, Readable):
"""channel base class""" """channel base class"""
value = Parameter('main value of channels', poll=True) value = Parameter('main value of channels', poll=True)
@ -270,7 +271,7 @@ class BridgeChannel(Channel):
return self.no, 0, 0, change.dcflag, change.readingmode, 0 return self.no, 0, 0, change.dcflag, change.readingmode, 0
class Level(PpmsBase): class Level(PpmsMixin, HasIodev, Readable):
"""helium level""" """helium level"""
level = IOHandler('level', 'LEVEL?', '%g,%d') level = IOHandler('level', 'LEVEL?', '%g,%d')
@ -293,7 +294,7 @@ class Level(PpmsBase):
return dict(value=level, status=(self.Status.IDLE, '')) return dict(value=level, status=(self.Status.IDLE, ''))
class Chamber(PpmsBase, Drivable): class Chamber(PpmsMixin, HasIodev, Drivable):
"""sample chamber handling """sample chamber handling
value is an Enum, which is redundant with the status text value is an Enum, which is redundant with the status text
@ -368,7 +369,7 @@ class Chamber(PpmsBase, Drivable):
return (change.target,) return (change.target,)
class Temp(PpmsBase, Drivable): class Temp(PpmsMixin, HasIodev, Drivable):
"""temperature""" """temperature"""
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d') temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
@ -553,7 +554,7 @@ class Temp(PpmsBase, Drivable):
self._stopped = True self._stopped = True
class Field(PpmsBase, Drivable): class Field(PpmsMixin, HasIodev, Drivable):
"""magnetic field""" """magnetic field"""
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d') field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
@ -562,6 +563,7 @@ class Field(PpmsBase, Drivable):
PREPARED=150, PREPARED=150,
PREPARING=340, PREPARING=340,
RAMPING=370, RAMPING=370,
STABILIZING=380,
FINALIZING=390, FINALIZING=390,
) )
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -584,7 +586,7 @@ class Field(PpmsBase, Drivable):
2: (Status.PREPARING, 'switch warming'), 2: (Status.PREPARING, 'switch warming'),
3: (Status.FINALIZING, 'switch cooling'), 3: (Status.FINALIZING, 'switch cooling'),
4: (Status.IDLE, 'driven stable'), 4: (Status.IDLE, 'driven stable'),
5: (Status.FINALIZING, 'driven final'), 5: (Status.STABILIZING, 'driven final'),
6: (Status.RAMPING, 'charging'), 6: (Status.RAMPING, 'charging'),
7: (Status.RAMPING, 'discharging'), 7: (Status.RAMPING, 'discharging'),
8: (Status.ERROR, 'current error'), 8: (Status.ERROR, 'current error'),
@ -690,7 +692,7 @@ class Field(PpmsBase, Drivable):
self._stopped = True self._stopped = True
class Position(PpmsBase, Drivable): class Position(PpmsMixin, HasIodev, Drivable):
"""rotator position""" """rotator position"""
move = IOHandler('move', 'MOVE?', '%g,%g,%g') move = IOHandler('move', 'MOVE?', '%g,%g,%g')

View File

@ -35,7 +35,8 @@ rx:bla rx bla /some/rx_a/bla rx bla /some/rx_a
import json import json
import threading import threading
import time import time
from os.path import expanduser, join import os
from os.path import expanduser, join, exists
from secop.client import ProxyClient from secop.client import ProxyClient
from secop.datatypes import ArrayOf, BoolType, \ from secop.datatypes import ArrayOf, BoolType, \
@ -44,7 +45,7 @@ from secop.errors import ConfigError, HardwareError, secop_error
from secop.lib import getGeneralConfig, mkthread from secop.lib import getGeneralConfig, mkthread
from secop.lib.asynconn import AsynConn, ConnectionClosed from secop.lib.asynconn import AsynConn, ConnectionClosed
from secop.modules import Attached, Command, Done, Drivable, \ from secop.modules import Attached, Command, Done, Drivable, \
Module, Parameter, Property, Readable, Writable Module, Parameter, Readable, Writable
from secop.protocol.dispatcher import make_update from secop.protocol.dispatcher import make_update
CFG_HEADER = """[NODE] CFG_HEADER = """[NODE]
@ -66,7 +67,12 @@ remote_paths = .
SEA_DIR = expanduser('~/sea') SEA_DIR = expanduser('~/sea')
confdir = getGeneralConfig()['confdir'].split(':', 1)[0] for confdir in getGeneralConfig()['confdir'].split(os.pathsep):
seaconfdir = join(confdir, 'sea')
if exists(seaconfdir):
break
else:
seaconfdir = None
def get_sea_port(instance): def get_sea_port(instance):
@ -87,8 +93,6 @@ def get_sea_port(instance):
class SeaClient(ProxyClient, Module): class SeaClient(ProxyClient, Module):
"""connection to SEA""" """connection to SEA"""
json_path = Property('path to SEA json descriptors', StringType())
uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000') uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000')
timeout = Parameter('timeout', datatype=FloatRange(0), default=10) timeout = Parameter('timeout', datatype=FloatRange(0), default=10)
@ -242,11 +246,11 @@ class SeaClient(ProxyClient, Module):
samenv, reply = json.loads(reply) samenv, reply = json.loads(reply)
samenv = samenv.replace('/', '_') samenv = samenv.replace('/', '_')
result = [] result = []
with open(join(confdir, 'sea', samenv + '.cfg'), 'w') as cfp: with open(join(seaconfdir, samenv + '.cfg'), 'w') as cfp:
cfp.write(CFG_HEADER % dict(samenv=samenv)) cfp.write(CFG_HEADER % dict(samenv=samenv))
for filename, obj, descr in reply: for filename, obj, descr in reply:
content = json.dumps([obj, descr]).replace('}, {', '},\n{') content = json.dumps([obj, descr]).replace('}, {', '},\n{')
with open(join(confdir, 'sea', filename + '.json'), 'w') as fp: with open(join(seaconfdir, filename + '.json'), 'w') as fp:
fp.write(content + '\n') fp.write(content + '\n')
if descr[0].get('cmd', '').startswith('run '): if descr[0].get('cmd', '').startswith('run '):
modcls = 'SeaDrivable' modcls = 'SeaDrivable'
@ -291,7 +295,7 @@ class SeaModule(Module):
remote_paths = cfgdict.pop('remote_paths', '') remote_paths = cfgdict.pop('remote_paths', '')
if 'description' not in cfgdict: if 'description' not in cfgdict:
cfgdict['description'] = '%s (remote_paths=%s)' % (json_descr, remote_paths) cfgdict['description'] = '%s (remote_paths=%s)' % (json_descr, remote_paths)
with open(join(confdir, 'sea', json_descr + '.json')) as fp: with open(join(seaconfdir, json_descr + '.json')) as fp:
sea_object, descr = json.load(fp) sea_object, descr = json.load(fp)
remote_paths = remote_paths.split() remote_paths = remote_paths.split()
if remote_paths: if remote_paths:

View File

@ -74,13 +74,18 @@ class Parser340(StdParser):
def parse(self, line): def parse(self, line):
"""scan header for data format""" """scan header for data format"""
if self.header: if self.header:
if line.startswith("Data Format"): key, _, value = line.partition(':')
dataformat = line.split(":")[1].strip()[0] if value: # this is a header line, as it contains ':'
if dataformat == '4': value = value.split()[0]
key = ''.join(key.split()).lower()
if key == 'dataformat':
if value == '4':
self.logx, self.logy = True, False # logOhm self.logx, self.logy = True, False # logOhm
elif dataformat == '5': elif value == '5':
self.logx, self.logy = True, True # logOhm, logK self.logx, self.logy = True, True # logOhm, logK
elif line.startswith("No."): elif value not in ('1', '2', '3'):
raise ValueError('invalid Data Format')
elif 'No.' in line:
self.header = False self.header = False
return return
super().parse(line) super().parse(line)
@ -134,13 +139,26 @@ class CalCurve:
cls, args = KINDS.get(kind, (StdParser, {})) cls, args = KINDS.get(kind, (StdParser, {}))
args.update(optargs) args.update(optargs)
try:
parser = cls(**args) parser = cls(**args)
with open(filename) as f: with open(filename) as f:
for line in f: for line in f:
parser.parse(line) parser.parse(line)
except Exception as e:
raise ValueError('calib curve %s: %s' % (calibspec, e))
self.convert_x = nplog if parser.logx else linear self.convert_x = nplog if parser.logx else linear
self.convert_y = npexp if parser.logy else linear self.convert_y = npexp if parser.logy else linear
self.spline = splrep(np.asarray(parser.xdata), np.asarray(parser.ydata), s=0) x = np.asarray(parser.xdata)
y = np.asarray(parser.ydata)
if np.all(x[:-1] > x[1:]): # all decreasing
x = np.flip(x)
y = np.flip(y)
elif np.any(x[:-1] >= x[1:]): # some not increasing
raise ValueError('calib curve %s is not monotonic' % calibspec)
try:
self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1))
except (ValueError, TypeError):
raise ValueError('invalid calib curve %s' % calibspec)
def __call__(self, value): def __call__(self, value):
"""convert value """convert value
@ -161,9 +179,12 @@ class Sensor(Readable):
status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
pollerClass = None pollerClass = None
description = 'a calibrated sensor value'
_value_error = None _value_error = None
def __init__(self, name, logger, cfgdict, srv):
cfgdict.setdefault('description', 'calibrated value of module %r' % cfgdict['rawsensor'])
super().__init__(name, logger, cfgdict, srv)
def initModule(self): def initModule(self):
self._rawsensor.registerCallbacks(self, ['status']) # auto update status self._rawsensor.registerCallbacks(self, ['status']) # auto update status
self._calib = CalCurve(self.calib) self._calib = CalCurve(self.calib)

View File

@ -38,12 +38,14 @@ class TestCmd(Module):
result=StringType()) result=StringType())
def arg(self, *arg): def arg(self, *arg):
"""5 args""" """5 args"""
self.tuple = arg
return repr(arg) return repr(arg)
@Command(argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']), @Command(argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
result=StringType()) result=StringType())
def keyed(self, **arg): def keyed(self, **arg):
"""keyworded arg""" """keyworded arg"""
self.struct = arg
return repr(arg) return repr(arg)
@Command(argument=FloatRange(), result=StringType()) @Command(argument=FloatRange(), result=StringType())