diff --git a/secop/core.py b/secop/core.py index 1da4370..118cd7a 100644 --- a/secop/core.py +++ b/secop/core.py @@ -31,7 +31,7 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \ from secop.iohandler import IOHandler, IOHandlerBase from secop.lib.enum import Enum from secop.modules import Attached, Communicator, \ - Done, Drivable, Module, Readable, Writable, HasAccessibles + Done, Drivable, Feature, Module, Readable, Writable, HasAccessibles from secop.params import Command, Parameter from secop.properties import Property from secop.proxy import Proxy, SecNode, proxy_class diff --git a/secop/features.py b/secop/features.py index 522ac53..b25e72d 100644 --- a/secop/features.py +++ b/secop/features.py @@ -17,6 +17,7 @@ # # Module authors: # Enrico Faulhaber +# Markus Zolliker # # ***************************************************************************** """Define Mixin Features for real Modules implemented in the server""" @@ -24,12 +25,91 @@ from secop.datatypes import ArrayOf, BoolType, EnumType, \ FloatRange, StringType, StructOf, TupleOf -from secop.modules import Command, HasAccessibles, Parameter +from secop.core import Command, Done, Drivable, Feature, \ + Parameter, Property, PersistentParam, Readable +from secop.errors import BadValueError, ConfigError +from secop.lib import clamp -class Feature(HasAccessibles): - """all things belonging to a small, predefined functionality influencing the working of a module""" +# --- proposals, to be used at SINQ (not agreed as standard yet) --- +class HasOffset(Feature): + """has an offset parameter + + implementation to be done in the subclass + """ + offset = PersistentParam('offset (physical value + offset = HW value)', + FloatRange(unit='deg'), readonly=False, default=0) + + def write_offset(self, value): + self.offset = value + if isinstance(self, HasLimits): + self.read_limits() + if isinstance(self, Readable): + self.read_value() + if isinstance(self, Drivable): + self.read_target() + self.saveParameters() + return Done + + +class HasLimits(Feature): + """user limits + + implementation to be done in the subclass + + for a drivable, abslimits is roughly the same as the target datatype limits, + except for the offset + """ + abslimits = Property('abs limits (raw values)', default=(-9e99, 9e99), extname='abslimits', export=True, + datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg'))) + limits = PersistentParam('user limits', readonly=False, default=(-9e99, 9e99), initwrite=True, + datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg'))) + _limits = None + + def apply_offset(self, sign, *values): + if isinstance(self, HasOffset): + return tuple(v + sign * self.offset for v in values) + return values + + def earlyInit(self): + super().earlyInit() + # make limits valid + _limits = self.apply_offset(1, *self.limits) + self._limits = tuple(clamp(self.abslimits[0], v, self.abslimits[1]) for v in _limits) + self.read_limits() + + def checkProperties(self): + pname = 'target' if isinstance(self, Drivable) else 'value' + dt = self.parameters[pname].datatype + min_, max_ = self.abslimits + t_min, t_max = self.apply_offset(1, dt.min, dt.max) + if t_min > max_ or t_max < min_: + raise ConfigError('abslimits not within %s range' % pname) + self.abslimits = clamp(t_min, min_, t_max), clamp(t_min, max_, t_max) + super().checkProperties() + + def read_limits(self): + return self.apply_offset(-1, *self._limits) + + def write_limits(self, value): + min_, max_ = self.apply_offset(-1, *self.abslimits) + if not min_ <= value[0] <= value[1] <= max_: + if value[0] > value[1]: + raise BadValueError('invalid interval: %r' % value) + raise BadValueError('limits not within abs limits [%g, %g]' % (min_, max_)) + self.limits = value + self.saveParameters() + return Done + + def check_limits(self, value): + """check if value is valid""" + min_, max_ = self.limits + if not min_ <= value <= max_: + raise BadValueError('limits violation: %g outside [%g, %g]' % (value, min_, max_)) + + +# --- not used, not tested yet --- class HAS_PID(Feature): # note: implementors should either use p,i,d or pid, but ECS must be handle both cases @@ -56,7 +136,6 @@ class HAS_PID(Feature): output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False) - class Has_PIDTable(HAS_PID): # parameters @@ -69,8 +148,6 @@ class Has_PIDTable(HAS_PID): _base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange' - - class HAS_Persistent(Feature): #extra_Status { # 'decoupled' : Status.IDLE+1, # to be discussed. @@ -85,11 +162,10 @@ class HAS_Persistent(Feature): default=0, readonly=False) is_persistent = Parameter('current state of persistence', datatype=BoolType(), optional=True) - stored_value = Parameter('current persistence value, often used as the modules value', - datatype='main', unit='$', optional=True) - driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)', - datatype='main', unit='$' ) - + # stored_value = Parameter('current persistence value, often used as the modules value', + # datatype='main', unit='$', optional=True) + # driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)', + # datatype='main', unit='$' ) class HAS_Tolerance(Feature): @@ -105,7 +181,6 @@ class HAS_Tolerance(Feature): optional=True) - class HAS_Timeout(Feature): # parameters @@ -113,7 +188,6 @@ class HAS_Timeout(Feature): datatype=FloatRange(0), default=0, unit='s') - class HAS_Pause(Feature): # just a proposal, can't agree on it.... @@ -139,7 +213,6 @@ class HAS_Ramp(Feature): readonly=True, ) - class HAS_Speed(Feature): # parameters @@ -147,7 +220,6 @@ class HAS_Speed(Feature): unit='$/s', datatype=FloatRange(0)) - class HAS_Accel(HAS_Speed): # parameters @@ -157,7 +229,6 @@ class HAS_Accel(HAS_Speed): datatype=FloatRange(0), optional=True) - class HAS_MotorCurrents(Feature): # parameters @@ -167,9 +238,8 @@ class HAS_MotorCurrents(Feature): datatype=FloatRange(0), optional=True) - class HAS_Curve(Feature): # proposed, not yet agreed upon! # parameters - curve = Parameter('Calibration curve', datatype=StringType(80), default='') + curve = Parameter('Calibration curve', datatype=StringType(), default='') diff --git a/secop/modules.py b/secop/modules.py index 7bca6de..ae3ea85 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -228,6 +228,14 @@ class HasAccessibles(HasProperties): cls.configurables = res +class Feature(HasAccessibles): + """all things belonging to a small, predefined functionality influencing the working of a module + + a mixin with Feature as a direct base class is recognized as a SECoP feature + and reported in the module property 'features' + """ + + class PollInfo: def __init__(self, pollinterval, trigger_event): self.interval = pollinterval @@ -292,6 +300,7 @@ class Module(HasAccessibles): extname='implementation') interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()), extname='interface_classes') + features = Property('list of features', ArrayOf(StringType()), extname='features') pollinterval = Property('poll interval for parameters handled by doPoll', FloatRange(0.1, 120), default=5) slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15) enablePoll = True @@ -350,10 +359,10 @@ class Module(HasAccessibles): # b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')] # list of only the 'highest' secop module class self.interface_classes = [ - b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1] + b.__name__ for b in mycls.__mro__ if issubclass(Drivable, b)][0:1] # handle Features - # XXX: todo + self.features = [b.__name__ for b in mycls.__mro__ if Feature in b.__bases__] # handle accessibles # 1) make local copies of parameter objects diff --git a/test/test_modules.py b/test/test_modules.py index c2230e1..007a1ed 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -235,7 +235,7 @@ def test_ModuleMagic(): assert o2.parameters['a1'].datatype.unit == 'mm/s' cfg = Newclass2.configurables assert set(cfg.keys()) == { - 'export', 'group', 'description', 'disable_value_range_check', + 'export', 'group', 'description', 'disable_value_range_check', 'features', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop', 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2', 'cmd2', 'value', 'a1'}