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..ef27224 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,93 @@ 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), + 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) + self.parameters['limits'].default = self.abslimits + 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) + print(self.parameters['limits'], 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 +138,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 +150,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 +164,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 +183,6 @@ class HAS_Tolerance(Feature): optional=True) - class HAS_Timeout(Feature): # parameters @@ -113,7 +190,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 +215,6 @@ class HAS_Ramp(Feature): readonly=True, ) - class HAS_Speed(Feature): # parameters @@ -147,7 +222,6 @@ class HAS_Speed(Feature): unit='$/s', datatype=FloatRange(0)) - class HAS_Accel(HAS_Speed): # parameters @@ -157,7 +231,6 @@ class HAS_Accel(HAS_Speed): datatype=FloatRange(0), optional=True) - class HAS_MotorCurrents(Feature): # parameters @@ -167,9 +240,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 48ebfa5..3806302 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 @@ -500,10 +509,11 @@ class Module(HasAccessibles): # TODO: remove readerror 'property' and replace value with exception pobj = self.parameters[pname] timestamp = timestamp or time.time() - changed = pobj.value != value try: + value = pobj.datatype(value) + changed = pobj.value != value # store the value even in case of error - pobj.value = pobj.datatype(value) + pobj.value = value except Exception as e: if isinstance(e, DiscouragedConversion): if DiscouragedConversion.log_message: @@ -597,7 +607,7 @@ class Module(HasAccessibles): self.io.polledModules.append(self) else: self.triggerPoll = threading.Event() - self.polledModules = [self] + self.polledModules.append(self) def startModule(self, start_events): """runs after init of all modules