make return value 'Done' unneccessary

'Done' was introduced in order to suppress unneccessary
duplicate updates. However, since super calls on access methods are
allowed, it is not nice when such a method returns Done, as this
is not automagically replaced by the current parameter value.
As a consequence:

- using Done is discouraged, but not (yet) removed in all code
- the 'omit_unchanged_within' property is moved from Module to an
  internal Parameter property 'update_unchanged'
- its default is moved from a SEC node property to generalConfig
- the 'update_unchanged' parameter property may be set to
  'never' for parameters where duplicate updates make no sense
- this property might be set to 'always', for measurements, where
  even unchanged values taken from HW should be transmitted

Change-Id: I2847c983ca09c2c4098e402edd08d0c96c3913f4
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30672
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2023-03-13 17:16:07 +01:00
parent 9cab6670b9
commit 0d265b9752
22 changed files with 188 additions and 119 deletions

View File

@ -31,9 +31,6 @@ def read\_\ *<parameter>*\ (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 <frappy.modules.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\_\ *<parameter>*\ (self, value):
value would be the same, as if it would be done by the ``read_<parameter>``
method. Often the easiest implementation is just returning the result of
a call to the ``read_<parameter>`` method.
Also, :ref:`Done <done unique>` might be returned in special
cases, e.g. when the code was written in a way, when self.<parameter> is
assigned already before returning from the method.
.. admonition:: behind the scenes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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