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
|
||||
- all other commits are to be cherry-picked
|
||||
7) when arrived at the point where the new working version should be,
|
||||
copy new_wip branch to work with 'git checkout -B work'.
|
||||
Not sure if this works, as work is to be pushed to git.psi.ch.
|
||||
We might first remove the remote branch with 'git push origin --delete work'.
|
||||
And then create again (git push origin work)?
|
||||
8) continue with (6) if wip and work should differ, and do like (7) for wip branch
|
||||
9) delete new_wip branch, push master, wip and work branches
|
||||
copy new_wip branch to work with 'git checkout work;git checkout new_wip .'
|
||||
(note the dot!) and then commit this.
|
||||
8) continue with (6) if wip and work should differ
|
||||
9) do like (7), but for wip branch
|
||||
10) delete new_wip branch, push master, wip and work branches
|
||||
|
||||
|
||||
## Procedure to update PPMS
|
||||
|
@ -495,9 +495,12 @@ class SecopClient(ProxyClient):
|
||||
def _set_state(self, online, state=None):
|
||||
# treat reconnecting as online!
|
||||
state = state or self.state
|
||||
try:
|
||||
self.callback(None, 'nodeStateChange', online, state)
|
||||
for mname in self.modules:
|
||||
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
|
||||
self.online = online
|
||||
self.state = state
|
||||
|
@ -98,14 +98,17 @@ def clamp(_min, value, _max):
|
||||
|
||||
|
||||
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)
|
||||
if modname.startswith('secop'):
|
||||
module = importlib.import_module(modname)
|
||||
else:
|
||||
# rarely needed by now....
|
||||
module = importlib.import_module('secop.' + modname)
|
||||
try:
|
||||
return getattr(module, classname)
|
||||
except AttributeError:
|
||||
raise AttributeError('no such class') from None
|
||||
|
||||
|
||||
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
|
||||
from secop.errors import BadValueError, ConfigError, InternalError, \
|
||||
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.params import PREDEFINED_ACCESSIBLES, Accessible, Command, Parameter
|
||||
from secop.params import Accessible, Command, Parameter
|
||||
from secop.poller import BasicPoller, Poller
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
@ -89,16 +89,21 @@ class HasAccessibles(HasProperties):
|
||||
if isinstance(pobj, Command):
|
||||
# nothing to do for now
|
||||
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
|
||||
wrapped = hasattr(rfunc, '__wrapped__')
|
||||
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 "
|
||||
"and read_%s" % (pname, pname))
|
||||
# read_<pname> overwrites inherited handler
|
||||
else:
|
||||
rfunc = rfunc_handler
|
||||
wrapped = False
|
||||
|
||||
# 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):
|
||||
if rfunc:
|
||||
@ -126,11 +131,14 @@ class HasAccessibles(HasProperties):
|
||||
|
||||
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
|
||||
wrapped = hasattr(wfunc, '__wrapped__')
|
||||
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
|
||||
if wfunc is None or getattr(wfunc, '__wrapped__', False) is False:
|
||||
if not wrapped:
|
||||
|
||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
self.log.debug("check validity of %s = %r" % (pname, value))
|
||||
@ -232,29 +240,27 @@ class Module(HasAccessibles):
|
||||
self.name = name
|
||||
self.valueCallbacks = {}
|
||||
self.errorCallbacks = {}
|
||||
errors = []
|
||||
|
||||
# handle module properties
|
||||
# 1) make local copies of properties
|
||||
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.propertyDict:
|
||||
self.setProperty(k[1:], cfgdict.pop(k))
|
||||
else:
|
||||
raise ConfigError('Module %r has no property %r' %
|
||||
(self.name, k[1:]))
|
||||
# 2) check and apply properties specified in cfgdict as
|
||||
# '<propertyname> = <propertyvalue>'
|
||||
for key in self.propertyDict:
|
||||
value = cfgdict.pop(key, None)
|
||||
if value is None:
|
||||
# legacy cfg: specified as '.<propertyname> = <propertyvalue>'
|
||||
value = cfgdict.pop('.' + key, None)
|
||||
if value is not None:
|
||||
try:
|
||||
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
|
||||
# '<propertyname> = <propertyvalue>' (without '.' prefix)
|
||||
for k in self.propertyDict:
|
||||
if k in cfgdict:
|
||||
self.setProperty(k, cfgdict.pop(k))
|
||||
|
||||
# 4) set automatic properties
|
||||
# 3) set automatic properties
|
||||
mycls = self.__class__
|
||||
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
|
||||
self.implementation = myclassname
|
||||
@ -288,16 +294,6 @@ class Module(HasAccessibles):
|
||||
if not self.export: # do not export parameters of a module not exported
|
||||
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.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
|
||||
accessibles[aname] = aobj
|
||||
# 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
|
||||
if '.' in k[1:]:
|
||||
paramname, propname = k.split('.', 1)
|
||||
propvalue = cfgdict.pop(k)
|
||||
paramobj = self.accessibles.get(paramname, None)
|
||||
# paramobj might also be a command (not sure if this is needed)
|
||||
if paramobj:
|
||||
if propname == 'datatype':
|
||||
paramobj.setProperty('datatype', get_datatype(cfgdict.pop(k), k))
|
||||
elif propname in paramobj.getProperties():
|
||||
paramobj.setProperty(propname, cfgdict.pop(k))
|
||||
else:
|
||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||
propvalue = get_datatype(propvalue, k)
|
||||
try:
|
||||
paramobj.setProperty(propname, propvalue)
|
||||
except KeyError:
|
||||
errors.append('module %s: %s.%s does not exist' %
|
||||
(self.name, paramname, propname))
|
||||
except BadValueError as e:
|
||||
errors.append('module %s: %s.%s: %s' %
|
||||
(self.name, paramname, propname, str(e)))
|
||||
else:
|
||||
raise ConfigError('Module %s has no Parameter %r!' %
|
||||
(self.name, paramname))
|
||||
errors.append('module %s: %s not found' % (self.name, paramname))
|
||||
|
||||
# 3) check config for problems:
|
||||
# only accept remaining config items specified in parameters
|
||||
for k, v in cfgdict.items():
|
||||
if k not in self.parameters:
|
||||
raise ConfigError(
|
||||
'Module %s:config Parameter %r '
|
||||
'not understood! (use one of %s)' %
|
||||
(self.name, k, ', '.join(list(self.parameters) +
|
||||
bad = [k for k in cfgdict if k not in self.parameters]
|
||||
if bad:
|
||||
errors.append(
|
||||
'module %s: %s does not exist (use one of %s)' %
|
||||
(self.name, ', '.join(bad), ', '.join(list(self.parameters) +
|
||||
list(self.propertyDict))))
|
||||
|
||||
# 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?)
|
||||
try:
|
||||
pobj.value = pobj.datatype(cfgdict[pname])
|
||||
except BadValueError as e:
|
||||
raise ConfigError('%s.%s: %s' % (name, pname, e))
|
||||
self.writeDict[pname] = pobj.value
|
||||
except BadValueError as e:
|
||||
errors.append('module %s, parameter %s: %s' % (name, pname, e))
|
||||
else:
|
||||
if pobj.default is None:
|
||||
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!' %
|
||||
(self.name, pname))
|
||||
# we do not want to call the setter for this parameter for now,
|
||||
@ -368,8 +366,8 @@ class Module(HasAccessibles):
|
||||
try:
|
||||
value = pobj.datatype(pobj.default)
|
||||
except BadValueError as e:
|
||||
raise ProgrammingError('bad default for %s.%s: %s'
|
||||
% (name, pname, e))
|
||||
# this should not happen, as the default is already checked in Parameter
|
||||
raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None
|
||||
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
|
||||
@ -387,10 +385,8 @@ class Module(HasAccessibles):
|
||||
# note: this will NOT call write_* methods!
|
||||
setattr(self, k, v)
|
||||
except (ValueError, TypeError):
|
||||
self.log.exception(formatExtendedStack())
|
||||
raise
|
||||
# raise ConfigError('Module %s: config parameter %r:\n%r' %
|
||||
# (self.name, k, e))
|
||||
# self.log.exception(formatExtendedStack())
|
||||
errors.append('module %s, parameter %s: %s' % (self.name, k, e))
|
||||
cfgdict.pop(k)
|
||||
|
||||
# Modify units AFTER applying the cfgdict
|
||||
@ -400,9 +396,18 @@ class Module(HasAccessibles):
|
||||
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
|
||||
|
||||
# 6) check complete configuration of * properties
|
||||
if not errors:
|
||||
try:
|
||||
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()
|
||||
except ConfigError:
|
||||
errors.append('module %s, parameter %s: %s' % (name, pname, e))
|
||||
if errors:
|
||||
raise ConfigError('\n'.join(errors))
|
||||
|
||||
# helper cfg-editor
|
||||
def __iter__(self):
|
||||
|
@ -101,10 +101,10 @@ class Parameter(Accessible):
|
||||
|
||||
description = Property(
|
||||
'mandatory description of the parameter', TextType(),
|
||||
extname='description', mandatory=True)
|
||||
extname='description', mandatory=True, export='always')
|
||||
datatype = Property(
|
||||
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
|
||||
extname='datainfo', mandatory=True)
|
||||
extname='datainfo', mandatory=True, export='always')
|
||||
readonly = Property(
|
||||
'not changeable via SECoP (default True)', BoolType(),
|
||||
extname='readonly', default=True, export='always')
|
||||
@ -225,10 +225,13 @@ class Parameter(Accessible):
|
||||
self.propertyValues.pop('default')
|
||||
|
||||
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
|
||||
else:
|
||||
elif predefined_cls is None:
|
||||
self.export = '_' + name
|
||||
else:
|
||||
raise ProgrammingError('can not use %r as name of a Parameter' % name)
|
||||
|
||||
def copy(self):
|
||||
# deep copy, as datatype might be altered from config
|
||||
@ -280,7 +283,7 @@ class Command(Accessible):
|
||||
|
||||
description = Property(
|
||||
'description of the Command', TextType(),
|
||||
extname='description', export=True, mandatory=True)
|
||||
extname='description', export='always', mandatory=True)
|
||||
group = Property(
|
||||
'optional command group of the command.', StringType(),
|
||||
extname='group', export=True, default='')
|
||||
@ -339,10 +342,13 @@ class Command(Accessible):
|
||||
|
||||
self.datatype = CommandType(self.argument, self.result)
|
||||
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
|
||||
else:
|
||||
elif predefined_cls is None:
|
||||
self.export = '_' + name
|
||||
else:
|
||||
raise ProgrammingError('can not use %r as name of a Command' % name)
|
||||
|
||||
def __get__(self, obj, owner=None):
|
||||
if obj is None:
|
||||
|
@ -158,7 +158,7 @@ class HasProperties(HasDescriptors):
|
||||
cls.propertyDict = properties
|
||||
# treat overriding properties with bare values
|
||||
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
|
||||
po = Property(**po.__dict__)
|
||||
try:
|
||||
@ -177,11 +177,11 @@ class HasProperties(HasDescriptors):
|
||||
"""validates properties and checks for min... <= max..."""
|
||||
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)
|
||||
try:
|
||||
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():
|
||||
if pn.startswith('min'):
|
||||
maxname = 'max' + pn[3:]
|
||||
|
@ -122,6 +122,8 @@ class ProxyModule(HasIodev, Module):
|
||||
self.announceUpdate(pname, None, readerror)
|
||||
self.announceUpdate('status', newstatus)
|
||||
|
||||
def checkProperties(self):
|
||||
pass # skip
|
||||
|
||||
class ProxyReadable(ProxyModule, Readable):
|
||||
pass
|
||||
|
@ -26,11 +26,12 @@
|
||||
import ast
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
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.modules import Attached
|
||||
from secop.params import PREDEFINED_ACCESSIBLES
|
||||
@ -179,7 +180,7 @@ class Server:
|
||||
self.run()
|
||||
|
||||
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)))
|
||||
|
||||
def run(self):
|
||||
@ -201,7 +202,7 @@ class Server:
|
||||
cls = get_class(self.INTERFACES[scheme])
|
||||
with cls(scheme, self.log.getChild(scheme), opts, self) as self.interface:
|
||||
if opts:
|
||||
self.unknown_options(cls, opts)
|
||||
raise ConfigError(self.unknown_options(cls, opts))
|
||||
self.log.info('startup done, handling transport messages')
|
||||
if systemd:
|
||||
systemd.daemon.notify("READY=1\nSTATUS=accepting requests")
|
||||
@ -219,20 +220,38 @@ class Server:
|
||||
self.interface.shutdown()
|
||||
|
||||
def _processCfg(self):
|
||||
errors = []
|
||||
opts = dict(self.node_cfg)
|
||||
cls = get_class(opts.pop('class', 'protocol.dispatcher.Dispatcher'))
|
||||
self.dispatcher = cls(opts.pop('name', self._cfgfiles), self.log.getChild('dispatcher'), opts, self)
|
||||
if opts:
|
||||
self.unknown_options(cls, opts)
|
||||
errors.append(self.unknown_options(cls, opts))
|
||||
self.modules = OrderedDict()
|
||||
badclass = None
|
||||
failed = set() # python modules failed to load
|
||||
self.lastError = None
|
||||
for modname, options in self.module_cfg.items():
|
||||
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)
|
||||
# all used args should be popped from opts!
|
||||
if opts:
|
||||
self.unknown_options(cls, opts)
|
||||
errors.append(self.unknown_options(cls, opts))
|
||||
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()
|
||||
# all objs created, now start them up and interconnect
|
||||
@ -249,12 +268,28 @@ class Server:
|
||||
for modname, modobj in self.modules.items():
|
||||
for propname, propobj in modobj.propertyDict.items():
|
||||
if isinstance(propobj, Attached):
|
||||
try:
|
||||
setattr(modobj, propobj.attrname or '_' + 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
|
||||
for modname, modobj in self.modules.items():
|
||||
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:
|
||||
return
|
||||
start_events = []
|
||||
|
@ -45,7 +45,12 @@ def make_cvt_list(dt, tail=''):
|
||||
result = []
|
||||
for subkey, elmtype in items:
|
||||
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
|
||||
|
||||
|
||||
|
@ -128,8 +128,9 @@ class Main(Communicator):
|
||||
return data # return data as string
|
||||
|
||||
|
||||
class PpmsBase(HasIodev, Readable):
|
||||
class PpmsMixin:
|
||||
"""common base for all ppms modules"""
|
||||
|
||||
iodev = Attached()
|
||||
|
||||
pollerClass = Poller
|
||||
@ -139,7 +140,7 @@ class PpmsBase(HasIodev, Readable):
|
||||
|
||||
# as this pollinterval affects only the polling of settings
|
||||
# it would be confusing to export it.
|
||||
pollinterval = Parameter(export=False)
|
||||
pollinterval = Parameter('', FloatRange(), needscfg=False, export=False)
|
||||
|
||||
def initModule(self):
|
||||
self._iodev.register(self)
|
||||
@ -172,7 +173,7 @@ class PpmsBase(HasIodev, Readable):
|
||||
self.status = (self.Status.IDLE, '')
|
||||
|
||||
|
||||
class Channel(PpmsBase):
|
||||
class Channel(PpmsMixin, HasIodev, Readable):
|
||||
"""channel base class"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Level(PpmsBase):
|
||||
class Level(PpmsMixin, HasIodev, Readable):
|
||||
"""helium level"""
|
||||
|
||||
level = IOHandler('level', 'LEVEL?', '%g,%d')
|
||||
@ -293,7 +294,7 @@ class Level(PpmsBase):
|
||||
return dict(value=level, status=(self.Status.IDLE, ''))
|
||||
|
||||
|
||||
class Chamber(PpmsBase, Drivable):
|
||||
class Chamber(PpmsMixin, HasIodev, Drivable):
|
||||
"""sample chamber handling
|
||||
|
||||
value is an Enum, which is redundant with the status text
|
||||
@ -368,7 +369,7 @@ class Chamber(PpmsBase, Drivable):
|
||||
return (change.target,)
|
||||
|
||||
|
||||
class Temp(PpmsBase, Drivable):
|
||||
class Temp(PpmsMixin, HasIodev, Drivable):
|
||||
"""temperature"""
|
||||
|
||||
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
|
||||
@ -553,7 +554,7 @@ class Temp(PpmsBase, Drivable):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class Field(PpmsBase, Drivable):
|
||||
class Field(PpmsMixin, HasIodev, Drivable):
|
||||
"""magnetic field"""
|
||||
|
||||
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
|
||||
@ -562,6 +563,7 @@ class Field(PpmsBase, Drivable):
|
||||
PREPARED=150,
|
||||
PREPARING=340,
|
||||
RAMPING=370,
|
||||
STABILIZING=380,
|
||||
FINALIZING=390,
|
||||
)
|
||||
# pylint: disable=invalid-name
|
||||
@ -584,7 +586,7 @@ class Field(PpmsBase, Drivable):
|
||||
2: (Status.PREPARING, 'switch warming'),
|
||||
3: (Status.FINALIZING, 'switch cooling'),
|
||||
4: (Status.IDLE, 'driven stable'),
|
||||
5: (Status.FINALIZING, 'driven final'),
|
||||
5: (Status.STABILIZING, 'driven final'),
|
||||
6: (Status.RAMPING, 'charging'),
|
||||
7: (Status.RAMPING, 'discharging'),
|
||||
8: (Status.ERROR, 'current error'),
|
||||
@ -690,7 +692,7 @@ class Field(PpmsBase, Drivable):
|
||||
self._stopped = True
|
||||
|
||||
|
||||
class Position(PpmsBase, Drivable):
|
||||
class Position(PpmsMixin, HasIodev, Drivable):
|
||||
"""rotator position"""
|
||||
|
||||
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 threading
|
||||
import time
|
||||
from os.path import expanduser, join
|
||||
import os
|
||||
from os.path import expanduser, join, exists
|
||||
|
||||
from secop.client import ProxyClient
|
||||
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.asynconn import AsynConn, ConnectionClosed
|
||||
from secop.modules import Attached, Command, Done, Drivable, \
|
||||
Module, Parameter, Property, Readable, Writable
|
||||
Module, Parameter, Readable, Writable
|
||||
from secop.protocol.dispatcher import make_update
|
||||
|
||||
CFG_HEADER = """[NODE]
|
||||
@ -66,7 +67,12 @@ remote_paths = .
|
||||
|
||||
|
||||
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):
|
||||
@ -87,8 +93,6 @@ def get_sea_port(instance):
|
||||
class SeaClient(ProxyClient, Module):
|
||||
"""connection to SEA"""
|
||||
|
||||
json_path = Property('path to SEA json descriptors', StringType())
|
||||
|
||||
uri = Parameter('hostname:portnumber', datatype=StringType(), default='localhost:5000')
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=10)
|
||||
|
||||
@ -242,11 +246,11 @@ class SeaClient(ProxyClient, Module):
|
||||
samenv, reply = json.loads(reply)
|
||||
samenv = samenv.replace('/', '_')
|
||||
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))
|
||||
for filename, obj, descr in reply:
|
||||
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')
|
||||
if descr[0].get('cmd', '').startswith('run '):
|
||||
modcls = 'SeaDrivable'
|
||||
@ -291,7 +295,7 @@ class SeaModule(Module):
|
||||
remote_paths = cfgdict.pop('remote_paths', '')
|
||||
if 'description' not in cfgdict:
|
||||
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)
|
||||
remote_paths = remote_paths.split()
|
||||
if remote_paths:
|
||||
|
@ -74,13 +74,18 @@ class Parser340(StdParser):
|
||||
def parse(self, line):
|
||||
"""scan header for data format"""
|
||||
if self.header:
|
||||
if line.startswith("Data Format"):
|
||||
dataformat = line.split(":")[1].strip()[0]
|
||||
if dataformat == '4':
|
||||
key, _, value = line.partition(':')
|
||||
if value: # this is a header line, as it contains ':'
|
||||
value = value.split()[0]
|
||||
key = ''.join(key.split()).lower()
|
||||
if key == 'dataformat':
|
||||
if value == '4':
|
||||
self.logx, self.logy = True, False # logOhm
|
||||
elif dataformat == '5':
|
||||
elif value == '5':
|
||||
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
|
||||
return
|
||||
super().parse(line)
|
||||
@ -134,13 +139,26 @@ class CalCurve:
|
||||
cls, args = KINDS.get(kind, (StdParser, {}))
|
||||
args.update(optargs)
|
||||
|
||||
try:
|
||||
parser = cls(**args)
|
||||
with open(filename) as f:
|
||||
for line in f:
|
||||
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_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):
|
||||
"""convert value
|
||||
@ -161,9 +179,12 @@ class Sensor(Readable):
|
||||
status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
|
||||
|
||||
pollerClass = None
|
||||
description = 'a calibrated sensor value'
|
||||
_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):
|
||||
self._rawsensor.registerCallbacks(self, ['status']) # auto update status
|
||||
self._calib = CalCurve(self.calib)
|
||||
|
@ -38,12 +38,14 @@ class TestCmd(Module):
|
||||
result=StringType())
|
||||
def arg(self, *arg):
|
||||
"""5 args"""
|
||||
self.tuple = arg
|
||||
return repr(arg)
|
||||
|
||||
@Command(argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
|
||||
result=StringType())
|
||||
def keyed(self, **arg):
|
||||
"""keyworded arg"""
|
||||
self.struct = arg
|
||||
return repr(arg)
|
||||
|
||||
@Command(argument=FloatRange(), result=StringType())
|
||||
|
Loading…
x
Reference in New Issue
Block a user