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
- 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

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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:

View File

@ -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:]

View File

@ -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

View File

@ -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 = []

View File

@ -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

View File

@ -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')

View File

@ -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:

View File

@ -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)

View File

@ -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())