Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
b88750ae2b | |||
9c68f582b7 | |||
c89b4a44bb | |||
330aa15400 | |||
3435107948 | |||
7c0101f6bd | |||
6ea97b0012 | |||
3dc159a9a4 | |||
bd14e0a0e5 | |||
![]() |
4922f0d664 | ||
9b71d3621d | |||
![]() |
bec3359069 |
11
README.md
11
README.md
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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):
|
||||||
|
121
secop/modules.py
121
secop/modules.py
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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:]
|
||||||
|
@ -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
|
||||||
|
@ -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 = []
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
|
Loading…
x
Reference in New Issue
Block a user