diff --git a/doc/source/programming.rst b/doc/source/programming.rst index e3f03c9..5c2e186 100644 --- a/doc/source/programming.rst +++ b/doc/source/programming.rst @@ -31,9 +31,6 @@ def read\_\ **\ (self): Called on a ``read`` SECoP message and whenever the internal poll mechanism of Frappy tries to get a new value. The return value should be the retrieved value. - In special cases :data:`Done ` might be returned instead, - when the internal code has already updated the parameter, or - when the value has not changed and no updates should be emitted. This method might also be called internally, in case a fresh value of the parameter is needed. @@ -55,9 +52,6 @@ def write\_\ **\ (self, value): value would be the same, as if it would be done by the ``read_`` method. Often the easiest implementation is just returning the result of a call to the ``read_`` method. - Also, :ref:`Done ` might be returned in special - cases, e.g. when the code was written in a way, when self. is - assigned already before returning from the method. .. admonition:: behind the scenes diff --git a/doc/source/reference.rst b/doc/source/reference.rst index a469432..19360c2 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -11,10 +11,6 @@ imported from the frappy.core module. Module Base Classes ................... -.. _done unique: - -.. autodata:: frappy.modules.Done - .. autoclass:: frappy.modules.Module :members: earlyInit, initModule, startModule diff --git a/frappy/config.py b/frappy/config.py index 90a67ad..333c818 100644 --- a/frappy/config.py +++ b/frappy/config.py @@ -36,7 +36,6 @@ class Node(dict): description, interface=None, cls='frappy.protocol.dispatcher.Dispatcher', - omit_unchanged_within=1.1, **kwds ): super().__init__( @@ -44,7 +43,6 @@ class Node(dict): description=description, interface=interface, cls=cls, - omit_unchanged_within=omit_unchanged_within, **kwds ) diff --git a/frappy/datatypes.py b/frappy/datatypes.py index 438d8e8..90c8b2c 100644 --- a/frappy/datatypes.py +++ b/frappy/datatypes.py @@ -1212,7 +1212,7 @@ class OrType(DataType): """accepts any of the given types, takes the first valid""" for t in self.types: try: - return t(value) + return t.validate(value) # use always strict validation except Exception: pass raise WrongTypeError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types)))) diff --git a/frappy/features.py b/frappy/features.py index deffad1..afbcacd 100644 --- a/frappy/features.py +++ b/frappy/features.py @@ -24,7 +24,7 @@ from frappy.datatypes import ArrayOf, BoolType, EnumType, \ FloatRange, StringType, StructOf, TupleOf -from frappy.core import Command, Done, Drivable, Feature, \ +from frappy.core import Command, Drivable, Feature, \ Parameter, Property, PersistentParam, Readable from frappy.errors import RangeError, ConfigError from frappy.lib import clamp @@ -39,6 +39,17 @@ class HasSimpleOffset(Feature): 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 value + class HasTargetLimits(Feature): """user limits @@ -83,7 +94,7 @@ class HasTargetLimits(Feature): raise RangeError('limits not within abs limits [%g, %g]' % (min_, max_)) self.limits = value self.saveParameters() - return Done + return self.limits def check_limits(self, value): """check if value is valid""" @@ -123,4 +134,4 @@ class HasLimits(Feature): """check if value is valid""" min_, max_ = self.target_limits if not min_ <= value <= max_: - raise BadValueError('limits violation: %g outside [%g, %g]' % (value, min_, max_)) + raise RangeError('limits violation: %g outside [%g, %g]' % (value, min_, max_)) diff --git a/frappy/io.py b/frappy/io.py index b83390e..4903ea4 100644 --- a/frappy/io.py +++ b/frappy/io.py @@ -33,7 +33,7 @@ from frappy.datatypes import ArrayOf, BLOBType, BoolType, FloatRange, IntRange, StringType, TupleOf, ValueType from frappy.errors import CommunicationFailedError, ConfigError, ProgrammingError from frappy.modules import Attached, Command, \ - Communicator, Done, Module, Parameter, Property + Communicator, Module, Parameter, Property from frappy.lib import generalConfig generalConfig.set_default('legacy_hasiodev', False) @@ -110,7 +110,8 @@ class IOBase(Communicator): """, datatype=StringType()) timeout = Parameter('timeout', datatype=FloatRange(0), default=2) wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0) - is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False) + is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False, + update_unchanged='never') pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10) #: a dict of default settings for a device, e.g. for a LakeShore 336: #: @@ -151,20 +152,20 @@ class IOBase(Communicator): self.is_connected is changed only by self.connectStart or self.closeConnection """ if self.is_connected: - return Done # no need for intermediate updates + return True try: self.connectStart() if self._last_error: self.log.info('connected') self._last_error = 'connected' self.callCallbacks() - return Done + return self.is_connected except Exception as e: if str(e) != self._last_error: self._last_error = str(e) self.log.error(self._last_error) raise SilentError(repr(e)) from e - return Done + return self.is_connected def write_is_connected(self, value): """value = True: connect if not yet done diff --git a/frappy/modules.py b/frappy/modules.py index 3566bce..1c5aa86 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -28,7 +28,8 @@ import threading from collections import OrderedDict from frappy.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \ - IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion + IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion, \ + NoneOr from frappy.errors import BadValueError, CommunicationFailedError, ConfigError, \ ProgrammingError, SECoPError, secop_error from frappy.lib import formatException, mkthread, UniqueObject @@ -131,7 +132,7 @@ class HasAccessibles(HasProperties): self.log.debug("read_%s failed with %r", pname, e) self.announceUpdate(pname, None, e) raise - if value is Done: + if value is Done: # TODO: to be removed when all code using Done is updated return getattr(self, pname) setattr(self, pname, value) # important! trigger the setter return value @@ -163,7 +164,7 @@ class HasAccessibles(HasProperties): new_value = pobj.datatype(value) new_value = wfunc(self, new_value) self.log.debug('write_%s(%r) returned %r', pname, value, new_value) - if new_value is Done: + if new_value is Done: # TODO: to be removed when all code using Done is updated return getattr(self, pname) if new_value is None: new_value = value @@ -300,6 +301,8 @@ class Module(HasAccessibles): 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) + omit_unchanged_within = Property('default for minimum time between updates of unchanged values', + NoneOr(FloatRange(0)), export=False, default=None) enablePoll = True # properties, parameters and commands are auto-merged upon subclassing @@ -315,7 +318,6 @@ class Module(HasAccessibles): def __init__(self, name, logger, cfgdict, srv): # remember the dispatcher object (for the async callbacks) self.DISPATCHER = srv.dispatcher - self.omit_unchanged_within = getattr(self.DISPATCHER, 'omit_unchanged_within', 0.1) self.log = logger self.name = name self.valueCallbacks = {} @@ -443,7 +445,7 @@ class Module(HasAccessibles): # 5) ensure consistency for aobj in self.accessibles.values(): - aobj.finish() + aobj.finish(self) # Modify units AFTER applying the cfgdict mainvalue = self.parameters.get('value') @@ -502,7 +504,7 @@ class Module(HasAccessibles): err = secop_error(err) if str(err) == str(pobj.readerror): return # no updates for repeated errors - elif not changed and timestamp < (pobj.timestamp or 0) + self.omit_unchanged_within: + elif not changed and timestamp < (pobj.timestamp or 0) + pobj.omit_unchanged_within: # no change within short time -> omit return pobj.timestamp = timestamp or time.time() diff --git a/frappy/params.py b/frappy/params.py index 2f36fdf..7f452d3 100644 --- a/frappy/params.py +++ b/frappy/params.py @@ -26,13 +26,14 @@ import inspect from frappy.datatypes import BoolType, CommandType, DataType, \ - DataTypeType, EnumType, NoneOr, OrType, \ + DataTypeType, EnumType, NoneOr, OrType, FloatRange, \ StringType, StructOf, TextType, TupleOf, ValueType from frappy.errors import BadValueError, WrongTypeError, ProgrammingError from frappy.properties import HasProperties, Property from frappy.lib import generalConfig generalConfig.set_default('tolerate_poll_property', False) +generalConfig.set_default('omit_unchanged_within', 0.1) class Accessible(HasProperties): @@ -77,7 +78,7 @@ class Accessible(HasProperties): """ raise NotImplementedError - def finish(self): + def finish(self, modobj=None): """ensure consistency""" raise NotImplementedError @@ -160,14 +161,19 @@ class Parameter(Accessible): needscfg = Property( '[internal] needs value in config', NoneOr(BoolType()), export=False, default=False) - # optional = Property( - # '[internal] is this parameter optional?', BoolType(), - # export=False, settable=False, default=False) + update_unchanged = Property( + '''[internal] updates of unchanged values + + - one of the values 'always', 'never', 'default' + or the minimum time between updates of equal values [sec]''', + OrType(FloatRange(0), EnumType(always=0, never=999999999, default=-1)), + export=False, default=-1) # used on the instance copy only # value = None timestamp = 0 readerror = None + omit_unchanged_within = 0 def __init__(self, description=None, datatype=None, inherit=True, **kwds): super().__init__() @@ -261,8 +267,11 @@ class Parameter(Accessible): self.init(merged_properties) self.finish() - def finish(self): - """ensure consistency""" + def finish(self, modobj=None): + """ensure consistency + + :param modobj: final call, called from Module.__init__ + """ if self.constant is not None: constant = self.datatype(self.constant) @@ -279,6 +288,14 @@ class Parameter(Accessible): except BadValueError: # clear, if it does not match datatype pass + if modobj: + if self.update_unchanged == -1: + t = modobj.omit_unchanged_within + print(self, t) + self.omit_unchanged_within = generalConfig.omit_unchanged_within if t is None else t + else: + self.omit_unchanged_within = float(self.update_unchanged) + print(self, self.omit_unchanged_within, generalConfig.defaults, generalConfig._config) def export_value(self): return self.datatype.export_value(self.value) @@ -450,7 +467,7 @@ class Command(Accessible): self.init(merged_properties) self.finish() - def finish(self): + def finish(self, modobj=None): """ensure consistency""" self.datatype = CommandType(self.argument, self.result) diff --git a/frappy/protocol/dispatcher.py b/frappy/protocol/dispatcher.py index d3b4a63..9b479b6 100644 --- a/frappy/protocol/dispatcher.py +++ b/frappy/protocol/dispatcher.py @@ -55,17 +55,15 @@ def make_update(modulename, pobj): if pobj.readerror: return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export), # error-report ! - [pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)]) + [pobj.readerror.name, repr(pobj.readerror), {'t': pobj.timestamp}]) return (EVENTREPLY, '%s:%s' % (modulename, pobj.export), - [pobj.export_value(), dict(t=pobj.timestamp)]) + [pobj.export_value(), {'t': pobj.timestamp}]) class Dispatcher: def __init__(self, name, logger, options, srv): # to avoid errors, we want to eat all options here self.equipment_id = options.pop('equipment_id', name) - # time interval for omitting updates of unchanged values - self.omit_unchanged_within = options.pop('omit_unchanged_within', 0.1) self.nodeprops = {} for k in list(options): self.nodeprops[k] = options.pop(k) @@ -230,7 +228,7 @@ class Dispatcher: result = cobj.do(moduleobj, argument) if cobj.result: result = cobj.result.export_value(result) - return result, dict(t=currenttime()) + return result, {'t': currenttime()} def _setParameterValue(self, modulename, exportedname, value): moduleobj = self.get_module(modulename) @@ -253,7 +251,7 @@ class Dispatcher: # note: exceptions are handled in handle_request, not here! getattr(moduleobj, 'write_' + pname)(value) # return value is ignored here, as already handled - return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {} + return pobj.export_value(), {'t': pobj.timestamp} if pobj.timestamp else {} def _getParameterValue(self, modulename, exportedname): moduleobj = self.get_module(modulename) @@ -272,7 +270,7 @@ class Dispatcher: # note: exceptions are handled in handle_request, not here! getattr(moduleobj, 'read_' + pname)() # return value is ignored here, as already handled - return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {} + return pobj.export_value(), {'t': pobj.timestamp} if pobj.timestamp else {} # # api to be called from the 'interface' diff --git a/frappy/rwhandler.py b/frappy/rwhandler.py index 195a0c2..0784522 100644 --- a/frappy/rwhandler.py +++ b/frappy/rwhandler.py @@ -58,7 +58,6 @@ Example 2: addressable HW parameters """ import functools -from frappy.modules import Done from frappy.errors import ProgrammingError @@ -134,8 +133,6 @@ class ReadHandler(Handler): def method(module, pname=key, func=self.func): with module.accessLock: value = func(module, pname) - if value is Done: - return getattr(module, pname) setattr(module, pname, value) return value @@ -156,7 +153,7 @@ class CommonReadHandler(ReadHandler): def method(module, pname=key, func=self.func): with module.accessLock: ret = func(module) - if ret not in (None, Done): + if ret is not None: raise ProgrammingError('a method wrapped with CommonReadHandler must not return any value') return getattr(module, pname) @@ -174,8 +171,7 @@ class WriteHandler(Handler): def method(module, value, pname=key, func=self.func): with module.accessLock: value = func(module, pname, value) - if value is not Done: - setattr(module, pname, value) + setattr(module, pname, value) return value return method @@ -217,7 +213,7 @@ class CommonWriteHandler(WriteHandler): values = WriteParameters(module) values[pname] = value ret = func(module, values) - if ret not in (None, Done): + if ret is not None: raise ProgrammingError('a method wrapped with CommonWriteHandler must not return any value') # remove pname from writeDict. this was not removed in WriteParameters, as it was not missing module.writeDict.pop(pname, None) diff --git a/frappy/states.py b/frappy/states.py index 3fbc67c..db3f99d 100644 --- a/frappy/states.py +++ b/frappy/states.py @@ -26,7 +26,7 @@ handles status depending on statemachine state """ -from frappy.core import BUSY, IDLE, ERROR, Parameter, Command, Done +from frappy.core import BUSY, IDLE, ERROR, Parameter, Command from frappy.lib.statemachine import StateMachine, Finish, Start, Stop, \ Retry # pylint: disable=unused-import @@ -53,7 +53,7 @@ def status_code(code, text=None): class HasStates: """mixin for modules needing a statemachine""" - status = Parameter() # make sure this is a parameter + status = Parameter(update_unchanged='never') all_status_changes = True # when True, send also updates for status changes within a cycle _state_machine = None _status = IDLE, '' @@ -132,10 +132,7 @@ class HasStates: return status def read_status(self): - sm = self._state_machine - if sm.status == self.status: - return Done - return sm.status + return self._state_machine.status def cycle_machine(self): sm = self._state_machine diff --git a/frappy_psi/ah2700.py b/frappy_psi/ah2700.py index dbcae27..a04b56f 100644 --- a/frappy_psi/ah2700.py +++ b/frappy_psi/ah2700.py @@ -20,7 +20,7 @@ # ***************************************************************************** """Andeen Hagerling capacitance bridge""" -from frappy.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO, nopoll +from frappy.core import FloatRange, HasIO, Parameter, Readable, StringIO, nopoll class Ah2700IO(StringIO): @@ -43,7 +43,7 @@ class Capacitance(HasIO, Readable): reply = self.communicate('SI') if not reply.startswith('F='): # this is probably an error message like "LOSS TOO HIGH" self.status = [self.Status.ERROR, reply] - return + return self.value self.status = [self.Status.IDLE, ''] # examples of replies: # 'F= 1000.0 HZ C= 0.000001 PF L> 0.0 DS V= 15.0 V' @@ -55,7 +55,6 @@ class Capacitance(HasIO, Readable): _, freq, _, _, cap, _, _, loss, lossunit, _, volt = reply[:11] self.freq = freq self.voltage = volt - self.value = cap if lossunit == 'DS': self.loss = loss else: # the unit was wrong, we want DS = tan(delta), not NS = nanoSiemens @@ -64,30 +63,30 @@ class Capacitance(HasIO, Readable): self.loss = reply[7] except IndexError: pass # don't worry, loss will be updated next time + return cap def read_value(self): - self.parse_reply(self.communicate('SI')) # SI = single trigger - return Done + return self.parse_reply(self.communicate('SI')) # SI = single trigger @nopoll def read_freq(self): self.read_value() - return Done + return self.freq @nopoll def read_loss(self): self.read_value() - return Done + return self.loss @nopoll def read_voltage(self): self.read_value() - return Done + return self.voltage def write_freq(self, value): - self.parse_reply(self.communicate('FR %g;SI' % value)) - return Done + self.value = self.parse_reply(self.communicate('FR %g;SI' % value)) + return self.freq def write_voltage(self, value): - self.parse_reply(self.communicate('V %g;SI' % value)) - return Done + self.value = self.parse_reply(self.communicate('V %g;SI' % value)) + return self.voltage diff --git a/frappy_psi/channelswitcher.py b/frappy_psi/channelswitcher.py index 1f81394..187a879 100644 --- a/frappy_psi/channelswitcher.py +++ b/frappy_psi/channelswitcher.py @@ -39,7 +39,7 @@ switcher=sw import time from frappy.datatypes import IntRange, BoolType, FloatRange -from frappy.core import Attached, Property, Drivable, Parameter, Readable, Done +from frappy.core import Attached, Property, Drivable, Parameter, Readable class ChannelSwitcher(Drivable): @@ -55,6 +55,7 @@ class ChannelSwitcher(Drivable): """ value = Parameter('the current channel number', IntRange(), needscfg=False) target = Parameter('channel to select', IntRange(), needscfg=False) + status = Parameter(update_unchanged='never') autoscan = Parameter('whether to scan automatically', BoolType(), readonly=False, default=True) pollinterval = Parameter(default=1, export=False) @@ -108,7 +109,7 @@ class ChannelSwitcher(Drivable): if self.status[0] == 'BUSY': chan = self._channels[self.target] if chan.is_switching(now, self._start_switch, self.switch_delay): - return Done + return self.status self.setFastPoll(False) self.status = 'IDLE', 'measure' self.value = self.target @@ -116,7 +117,7 @@ class ChannelSwitcher(Drivable): chan.read_value() chan.read_status() if self.measure_delay > self._time_tol: - return Done + return self.status else: chan = self._channels[self.value] self.read_value() # this might modify autoscan or deadline! @@ -129,7 +130,7 @@ class ChannelSwitcher(Drivable): chan.read_status() self._last_measure = next_measure if not self.autoscan or now + self._time_tol < self._start_measure + self.measure_delay: - return Done + return self.status next_channel = self.next_channel(self.value) if next_channel == self.value: return 'IDLE', 'single channel' diff --git a/frappy_psi/mercury.py b/frappy_psi/mercury.py index 975dddd..9378835 100644 --- a/frappy_psi/mercury.py +++ b/frappy_psi/mercury.py @@ -26,7 +26,7 @@ import re import time from frappy.core import Drivable, HasIO, Writable, \ - Parameter, Property, Readable, StringIO, Attached, Done, IDLE, nopoll + Parameter, Property, Readable, StringIO, Attached, IDLE, nopoll from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType from frappy.errors import HardwareError from frappy_psi.convergence import HasConvergence @@ -70,7 +70,7 @@ class MercuryChannel(HasIO): example: DB6.T1,DB1.H1 slot ids for sensor (and control output)''', StringType()) - channel_name = Parameter('mercury nick name', StringType(), default='') + channel_name = Parameter('mercury nick name', StringType(), default='', update_unchanged='never') channel_type = '' #: channel type(s) for sensor (and control) e.g. TEMP,HTR or PRES,AUX def _complete_adr(self, adr): @@ -158,7 +158,7 @@ class MercuryChannel(HasIO): def read_channel_name(self): if self.channel_name: - return Done # channel name will not change + return self.channel_name # channel name will not change return self.query('0:NICK', as_string) @@ -189,14 +189,14 @@ class HasInput(MercuryChannel): def write_controlled_by(self, value): if self.controlled_by == value: - return Done + return value self.controlled_by = value if value == SELF: self.log.warning('switch to manual mode') for input_module in self.input_modules: if input_module.control_active: input_module.write_control_active(False) - return Done + return value class Loop(HasConvergence, MercuryChannel, Drivable): @@ -261,7 +261,8 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): """ channel_type = 'HTR' value = Parameter('heater output', FloatRange(unit='W'), readonly=False) - target = Parameter('heater output', FloatRange(0, 100, unit='$'), readonly=False) + status = Parameter(update_unchanged='never') + target = Parameter('heater output', FloatRange(0, 100, unit='$'), readonly=False, update_unchanged='never') resistivity = Parameter('heater resistivity', FloatRange(10, 1000, unit='Ohm'), readonly=False) true_power = Parameter('calculate power from measured current', BoolType(), readonly=False, default=True) @@ -288,13 +289,10 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): if not self.true_power: self._volt_target = math.sqrt(self._last_target * self.resistivity) self.change('HTR:SIG:VOLT', self._volt_target) - return Done + return self.resistivity def read_status(self): - status = IDLE, ('true power' if self.true_power else 'fixed resistivity') - if self.status != status: - return status - return Done + return IDLE, ('true power' if self.true_power else 'fixed resistivity') def read_value(self): if self._last_target is None: # on init @@ -317,7 +315,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): if self.controlled_by != 0 and self.target: return 0 if self._last_target is not None: - return Done + return self.target self._volt_target = self.query('HTR:SIG:VOLT') self.resistivity = max(10, self.query('HTR:RES')) self._last_target = self._volt_target ** 2 / max(10, self.resistivity) @@ -389,7 +387,7 @@ class TemperatureLoop(TemperatureSensor, Loop, Drivable): self.set_target(value) else: self.set_target(target) - return Done + return self.target def read_enable_ramp(self): return self.query('TEMP:LOOP:RENA', off_on) @@ -472,7 +470,7 @@ class PressureLoop(PressureSensor, Loop, Drivable): def write_target(self, value): target = self.change('PRES:LOOP:PRST', value) self.set_target(target) - return Done + return self.target class HeLevel(MercuryChannel, Readable): diff --git a/frappy_psi/motorvalve.py b/frappy_psi/motorvalve.py index 3d0008e..d7570b2 100644 --- a/frappy_psi/motorvalve.py +++ b/frappy_psi/motorvalve.py @@ -44,7 +44,7 @@ lowspeed = 50 # speed for final closing / reference run """ from frappy.core import Drivable, Parameter, EnumType, Attached, FloatRange, \ - Command, IDLE, BUSY, WARN, ERROR, Done, PersistentParam, PersistentMixin + Command, IDLE, BUSY, WARN, ERROR, PersistentParam, PersistentMixin from frappy.errors import HardwareError from frappy_psi.trinamic import Motor from frappy.lib.statemachine import StateMachine, Retry, Stop @@ -53,7 +53,7 @@ from frappy.lib.statemachine import StateMachine, Retry, Stop class MotorValve(PersistentMixin, Drivable): motor = Attached(Motor) value = Parameter('current state', EnumType( - closed=0, opened=1, undefined=-1), default=-1) + closed=0, opened=1, undefined=-1), default=-1, update_unchanged='never') target = Parameter('target state', EnumType(close=0, open=1)) turns = Parameter('number of turns to open', FloatRange(), readonly=False, group='settings') speed = Parameter('speed for far moves', FloatRange(), readonly=False, group='settings') @@ -75,7 +75,7 @@ class MotorValve(PersistentMixin, Drivable): raise HardwareError('%s: need refrun' % self.status[1]) self.target = target self._state.start(self.goto_target, count=3) - return Done + return self.target def goto_target(self, state): self.value = 'undefined' @@ -90,7 +90,7 @@ class MotorValve(PersistentMixin, Drivable): if self.status[0] == ERROR: return 'undefined' if self.motor.isBusy(): - return Done + return self.value motpos = self.motor.read_value() if self.motor.read_home(): if motpos > 360: diff --git a/frappy_psi/ppms.py b/frappy_psi/ppms.py index e0a3063..6c71cca 100644 --- a/frappy_psi/ppms.py +++ b/frappy_psi/ppms.py @@ -40,7 +40,7 @@ from frappy.datatypes import BoolType, EnumType, \ from frappy.errors import HardwareError from frappy.lib import clamp from frappy.lib.enum import Enum -from frappy.modules import Communicator, Done, \ +from frappy.modules import Communicator, \ Drivable, Parameter, Property, Readable from frappy.io import HasIO from frappy.rwhandler import CommonReadHandler, CommonWriteHandler @@ -478,16 +478,14 @@ class Temp(PpmsDrivable): def write_approachmode(self, value): if self.isDriving(): self._write_params(self.setpoint, self.ramp, value) - return Done - self.approachmode = value - return Done # do not execute TEMP command, as this would trigger an unnecessary T change + return self.approachmode + return value # do not execute TEMP command, as this would trigger an unnecessary T change def write_ramp(self, value): if self.isDriving(): self._write_params(self.setpoint, value, self.approachmode) - return Done - self.ramp = value - return Done # do not execute TEMP command, as this would trigger an unnecessary T change + return self.ramp + return value # do not execute TEMP command, as this would trigger an unnecessary T change def calc_expected(self, target, ramp): self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) @@ -606,7 +604,7 @@ class Field(PpmsDrivable): self._last_change = time.time() self.status = (self.Status.BUSY, 'changed target') self._write_params(target, self.ramp, self.approachmode, self.persistentmode) - return Done + return self.target def write_persistentmode(self, mode): if abs(self.target - self.value) <= 2e-5 and mode == self.persistentmode: @@ -617,20 +615,18 @@ class Field(PpmsDrivable): self._stopped = False self.status = (self.Status.BUSY, 'changed persistent mode') self._write_params(self.target, self.ramp, self.approachmode, mode) - return Done + return self.persistentmode def write_ramp(self, value): - self.ramp = value if self.isDriving(): self._write_params(self.target, value, self.approachmode, self.persistentmode) - return Done - return None # do not execute FIELD command, as this would trigger a ramp up of leads current + return self.ramp + return value # do not execute FIELD command, as this would trigger a ramp up of leads current def write_approachmode(self, value): if self.isDriving(): self._write_params(self.target, self.ramp, value, self.persistentmode) - return Done - return None # do not execute FIELD command, as this would trigger a ramp up of leads current + # do not execute FIELD command, as this would trigger a ramp up of leads current def stop(self): if not self.isDriving(): @@ -728,14 +724,13 @@ class Position(PpmsDrivable): self._status_before_change = self.status self.status = (self.Status.BUSY, 'changed target') self._write_params(target, self.speed) - return Done + return self.target def write_speed(self, value): if self.isDriving(): self._write_params(self.target, value) - return Done - self.speed = value - return None # do not execute MOVE command, as this would trigger an unnecessary move + return self.speed + return value # do not execute MOVE command, as this would trigger an unnecessary move def stop(self): if not self.isDriving(): diff --git a/frappy_psi/softcal.py b/frappy_psi/softcal.py index c09b3fe..4c8781b 100644 --- a/frappy_psi/softcal.py +++ b/frappy_psi/softcal.py @@ -28,7 +28,7 @@ import numpy as np from scipy.interpolate import splev, splrep # pylint: disable=import-error from frappy.core import Attached, BoolType, Parameter, Readable, StringType, \ - FloatRange, Done + FloatRange def linear(x): @@ -95,7 +95,7 @@ class Parser340(StdParser): KINDS = { "340": (Parser340, {}), # lakeshore 340 format "inp": (StdParser, {}), # M. Zollikers *.inp calcurve format - "caldat": (StdParser, dict(x=1, y=2)), # format from sea/tcl/startup/calib_ext.tcl + "caldat": (StdParser, {'x': 1, 'y': 2}), # format from sea/tcl/startup/calib_ext.tcl "dat": (StdParser, {}), # lakeshore raw data *.dat format } @@ -179,7 +179,7 @@ class Sensor(Readable): abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True) value = Parameter(datatype=FloatRange(unit='K')) pollinterval = Parameter(export=False) - status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) + status = Parameter(default=(Readable.Status.ERROR, 'unintialized'), update_unchanged='never') description = 'a calibrated sensor value' _value_error = None @@ -227,4 +227,4 @@ class Sensor(Readable): def read_status(self): self.update_status(self.rawsensor.status) - return Done + return self.status diff --git a/test/test_handler.py b/test/test_handler.py index db154c1..2215673 100644 --- a/test/test_handler.py +++ b/test/test_handler.py @@ -23,7 +23,8 @@ from frappy.rwhandler import ReadHandler, WriteHandler, \ CommonReadHandler, CommonWriteHandler, nopoll -from frappy.core import Module, Parameter, FloatRange, Done +from frappy.core import Module, Parameter, FloatRange +from frappy.lib import generalConfig class DispatcherStub: @@ -31,9 +32,9 @@ class DispatcherStub: # initial value from the timestamp. However, in the test below # the second update happens after the updates dict is cleared # -> we have to inhibit the 'omit unchanged update' feature - omit_unchanged_within = 0 def __init__(self, updates): + generalConfig.testinit(omit_unchanged_within=0) self.updates = updates def announce_update(self, modulename, pname, pobj): @@ -104,7 +105,7 @@ def test_handler(): assert m.b == 7 assert data.pop() == 'b' - data.append(Done) + data.append(m.b) assert m.read_b() == 7 assert data.pop() == 'b' diff --git a/test/test_modules.py b/test/test_modules.py index 2f47f5b..65e2129 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -31,6 +31,7 @@ from frappy.errors import ProgrammingError, ConfigError from frappy.modules import Communicator, Drivable, Readable, Module from frappy.params import Command, Parameter from frappy.rwhandler import ReadHandler, WriteHandler, nopoll +from frappy.lib import generalConfig class DispatcherStub: @@ -38,9 +39,9 @@ class DispatcherStub: # initial value from the timestamp. However, in the test below # the second update happens after the updates dict is cleared # -> we have to inhibit the 'omit unchanged update' feature - omit_unchanged_within = 0 def __init__(self, updates): + generalConfig.testinit(omit_unchanged_within=0) self.updates = updates def announce_update(self, modulename, pname, pobj): @@ -239,12 +240,12 @@ def test_ModuleMagic(): 'export', 'group', 'description', 'features', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop', 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2', - 'cmd2', 'value', 'a1'} + 'cmd2', 'value', 'a1', 'omit_unchanged_within'} assert set(cfg['value'].keys()) == { 'group', 'export', 'relative_resolution', 'visibility', 'unit', 'default', 'value', 'datatype', 'fmtstr', 'absolute_resolution', 'max', 'min', 'readonly', 'constant', - 'description', 'needscfg'} + 'description', 'needscfg', 'update_unchanged'} # check on the level of classes # this checks Newclass1 too, as it is inherited by Newclass2 @@ -739,3 +740,46 @@ def test_write_method_returns_none(): mod = Mod('mod', LoggerStub(), {'description': ''}, ServerStub({})) mod.write_a(1.5) assert mod.a == 1.5 + + +@pytest.mark.parametrize('arg, value', [ + ('always', 0), + (0, 0), + ('never', 999999999), + (999999999, 999999999), + ('default', 0.25), + (1, 1), +]) +def test_update_unchanged_ok(arg, value): + srv = ServerStub({}) + generalConfig.testinit(omit_unchanged_within=0.25) # override value from DispatcherStub + + class Mod1(Module): + a = Parameter('', FloatRange(), default=0, update_unchanged=arg) + + mod1 = Mod1('mod1', LoggerStub(), {'description': ''}, srv) + par = mod1.parameters['a'] + assert par.omit_unchanged_within == value + assert Mod1.a.omit_unchanged_within == 0 + + class Mod2(Module): + a = Parameter('', FloatRange(), default=0) + + mod2 = Mod2('mod2', LoggerStub(), {'description': '', 'a': {'update_unchanged': arg}}, srv) + par = mod2.parameters['a'] + assert par.omit_unchanged_within == value + assert Mod2.a.omit_unchanged_within == 0 + + +def test_omit_unchanged_within(): + srv = ServerStub({}) + generalConfig.testinit(omit_unchanged_within=0.25) # override call from DispatcherStub + + class Mod(Module): + a = Parameter('', FloatRange()) + + mod1 = Mod('mod1', LoggerStub(), {'description': ''}, srv) + assert mod1.parameters['a'].omit_unchanged_within == 0.25 + + mod2 = Mod('mod2', LoggerStub(), {'description': '', 'omit_unchanged_within': 0.125}, srv) + assert mod2.parameters['a'].omit_unchanged_within == 0.125 diff --git a/test/test_params.py b/test/test_params.py index 7de5f87..7d15033 100644 --- a/test/test_params.py +++ b/test/test_params.py @@ -109,3 +109,21 @@ def test_Export(): class Mod(HasAccessibles): param = Parameter('description1', datatype=BoolType, default=False) assert Mod.param.export == '_param' + + +@pytest.mark.parametrize('arg, value', [ + ('always', 0), + (0, 0), + ('never', 999999999), + (999999999, 999999999), + (1, 1), +]) +def test_update_unchanged_ok(arg, value): + par = Parameter('', datatype=FloatRange(), default=0, update_unchanged=arg) + assert par.update_unchanged == value + + +@pytest.mark.parametrize('arg', ['alws', '', -2, -0.1, None]) +def test_update_unchanged_fail(arg): + with pytest.raises(ProgrammingError): + Parameter('', datatype=FloatRange(), default=0, update_unchanged=arg) diff --git a/test/test_poller.py b/test/test_poller.py index b5a682e..b47d38e 100644 --- a/test/test_poller.py +++ b/test/test_poller.py @@ -31,6 +31,7 @@ import pytest from frappy.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll from frappy.lib.multievent import MultiEvent +from frappy.lib import generalConfig class Time: @@ -66,13 +67,14 @@ class DispatcherStub: class ServerStub: def __init__(self): + generalConfig.testinit() self.dispatcher = DispatcherStub() class Base(Module): def __init__(self): srv = ServerStub() - super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv) + super().__init__('mod', logging.getLogger('dummy'), {'description': ''}, srv) self.dispatcher = srv.dispatcher def run(self, maxcycles): diff --git a/test/test_statemachine.py b/test/test_statemachine.py index a928ad2..2e2efa2 100644 --- a/test/test_statemachine.py +++ b/test/test_statemachine.py @@ -24,6 +24,7 @@ from frappy.core import Drivable, Parameter from frappy.datatypes import StatusType, Enum from frappy.states import StateMachine, Stop, Retry, Finish, Start, HasStates, status_code +from frappy.lib import generalConfig class LoggerStub: @@ -189,9 +190,9 @@ class DispatcherStub: # initial value from the timestamp. However, in the test below # the second update happens after the updates dict is cleared # -> we have to inhibit the 'omit unchanged update' feature - omit_unchanged_within = 0 def __init__(self, updates): + generalConfig.testinit(omit_unchanged_within=0) self.updates = updates def announce_update(self, modulename, pname, pobj):