mercury, ips, sea, triton, convergence

after gerrit

Change-Id: Iff14047ecc476589aef10c96fae9970133b8bd14
This commit is contained in:
zolliker 2023-05-09 14:57:34 +02:00
parent 750b5a7794
commit 8039351395
12 changed files with 426 additions and 357 deletions

View File

@ -250,16 +250,9 @@ class ProxyClient:
return bool(cblist) return bool(cblist)
def updateValue(self, module, param, value, timestamp, readerror): def updateValue(self, module, param, value, timestamp, readerror):
entry = CacheItem(value, timestamp, readerror, self.callback(None, 'updateEvent', module, param, value, timestamp, readerror)
self.modules[module]['parameters'][param]['datatype']) self.callback(module, 'updateEvent', module, param, value, timestamp, readerror)
self.cache[(module, param)] = entry self.callback((module, param), 'updateEvent', module, param,value, timestamp, readerror)
self.callback(None, 'updateItem', module, param, entry)
self.callback(module, 'updateItem', module, param, entry)
self.callback((module, param), 'updateItem', module, param, entry)
# TODO: change clients to use updateItem instead of updateEvent
self.callback(None, 'updateEvent', module, param, *entry)
self.callback(module, 'updateEvent', module, param, *entry)
self.callback((module, param), 'updateEvent', module, param, *entry)
class SecopClient(ProxyClient): class SecopClient(ProxyClient):
@ -651,6 +644,16 @@ class SecopClient(ProxyClient):
data = datatype.import_value(data) data = datatype.import_value(data)
return data, qualifiers return data, qualifiers
def updateValue(self, module, param, value, timestamp, readerror):
entry = CacheItem(value, timestamp, readerror,
self.modules[module]['parameters'][param]['datatype'])
self.cache[(module, param)] = entry
self.callback(None, 'updateItem', module, param, entry)
self.callback(module, 'updateItem', module, param, entry)
self.callback((module, param), 'updateItem', module, param, entry)
# TODO: change clients to use updateItem instead of updateEvent
super().updateValue(module, param, value, timestamp, readerror)
# the following attributes may be/are intended to be overwritten by a subclass # the following attributes may be/are intended to be overwritten by a subclass
PREDEFINED_NAMES = set(frappy.params.PREDEFINED_ACCESSIBLES) PREDEFINED_NAMES = set(frappy.params.PREDEFINED_ACCESSIBLES)

View File

@ -126,13 +126,15 @@ class Config(dict):
self.modules.append(mod) self.modules.append(mod)
def process_file(config_text): def process_file(filename):
with open(filename, 'rb') as f:
config_text = f.read()
node = NodeCollector() node = NodeCollector()
mods = Collector(Mod) mods = Collector(Mod)
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group} ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group}
# pylint: disable=exec-used # pylint: disable=exec-used
exec(config_text, ns) exec(compile(config_text, filename, 'exec'), ns)
return Config(node, mods) return Config(node, mods)
@ -175,9 +177,7 @@ def load_config(cfgfiles, log):
for cfgfile in cfgfiles.split(','): for cfgfile in cfgfiles.split(','):
filename = to_config_path(cfgfile, log) filename = to_config_path(cfgfile, log)
log.debug('Parsing config file %s...', filename) log.debug('Parsing config file %s...', filename)
with open(filename, 'rb') as f: cfg = process_file(filename)
config_text = f.read()
cfg = process_file(config_text)
if config: if config:
config.merge_modules(cfg) config.merge_modules(cfg)
else: else:

View File

@ -274,9 +274,8 @@ class FloatRange(HasUnit, DataType):
def compatible(self, other): def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)): if not isinstance(other, (FloatRange, ScaledInteger)):
raise WrongTypeError('incompatible datatypes') raise WrongTypeError('incompatible datatypes')
# avoid infinity other.validate(self.min)
other.validate(max(sys.float_info.min, self.min)) other.validate(self.max)
other.validate(min(sys.float_info.max, self.max))
class IntRange(DataType): class IntRange(DataType):

View File

@ -396,3 +396,13 @@ class UniqueObject:
def __repr__(self): def __repr__(self):
return self.name return self.name
def merge_status(*args):
"""merge status
the status with biggest code wins
texts matching maximal code are joined with ', '
"""
maxcode = max(a[0] for a in args)
return maxcode, ', '.join([a[1] for a in args if a[0] == maxcode and a[1]])

View File

@ -21,16 +21,17 @@
# ***************************************************************************** # *****************************************************************************
from frappy.datatypes import BoolType, EnumType, Enum from frappy.datatypes import BoolType, EnumType, Enum
from frappy.core import Parameter, Writable, Attached from frappy.core import Parameter, Attached
class HasControlledBy(Writable): class HasControlledBy:
"""mixin for modules with controlled_by """mixin for modules with controlled_by
in the :meth:`write_target` the hardware action to switch to own control should be done in the :meth:`write_target` the hardware action to switch to own control should be done
and in addition self.self_controlled() should be called and in addition self.self_controlled() should be called
""" """
controlled_by = Parameter('source of target value', EnumType(members={'self': 0}), default=0) controlled_by = Parameter('source of target value', EnumType(members={'self': 0}), default=0)
target = Parameter() # make sure target is a parameter
inputCallbacks = () inputCallbacks = ()
def register_input(self, name, deactivate_control): def register_input(self, name, deactivate_control):
@ -57,7 +58,7 @@ class HasControlledBy(Writable):
deactivate_control(self.name) deactivate_control(self.name)
class HasOutputModule(Writable): class HasOutputModule:
"""mixin for modules having an output module """mixin for modules having an output module
in the :meth:`write_target` the hardware action to switch to own control should be done in the :meth:`write_target` the hardware action to switch to own control should be done
@ -66,6 +67,7 @@ class HasOutputModule(Writable):
# mandatory=False: it should be possible to configure a module with fixed control # mandatory=False: it should be possible to configure a module with fixed control
output_module = Attached(HasControlledBy, mandatory=False) output_module = Attached(HasControlledBy, mandatory=False)
control_active = Parameter('control mode', BoolType(), default=False) control_active = Parameter('control mode', BoolType(), default=False)
target = Parameter() # make sure target is a parameter
def initModule(self): def initModule(self):
super().initModule() super().initModule()
@ -85,8 +87,8 @@ class HasOutputModule(Writable):
out.controlled_by = self.name out.controlled_by = self.name
self.control_active = True self.control_active = True
def deactivate_control(self, switched_by): def deactivate_control(self, source):
"""called when an other module takes over control""" """called when an other module takes over control"""
if self.control_active: if self.control_active:
self.control_active = False self.control_active = False
self.log.warning(f'switched to manual mode by {switched_by}') self.log.warning(f'switched to manual mode by {source}')

View File

@ -245,7 +245,6 @@ class Feature(HasAccessibles):
a mixin with Feature as a direct base class is recognized as a SECoP feature a mixin with Feature as a direct base class is recognized as a SECoP feature
and reported in the module property 'features' and reported in the module property 'features'
""" """
featureName = None
class PollInfo: class PollInfo:
@ -377,10 +376,10 @@ class Module(HasAccessibles):
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('frappy.modules')] # b.__name__ for b in mycls.__mro__ if b.__module__.startswith('frappy.modules')]
# list of only the 'highest' secop module class # list of only the 'highest' secop module class
self.interface_classes = [ self.interface_classes = [
b.__name__ for b in mycls.__mro__ if issubclass(Drivable, b)][0:1] b.__name__ for b in mycls.__mro__ if b in SECoP_BASE_CLASSES][:1]
# handle Features # handle Features
self.features = [b.featureName or b.__name__ for b in mycls.__mro__ if Feature in b.__bases__] self.features = [b.__name__ for b in mycls.__mro__ if Feature in b.__bases__]
# handle accessibles # handle accessibles
# 1) make local copies of parameter objects # 1) make local copies of parameter objects
@ -898,6 +897,7 @@ class Communicator(HasComlog, Module):
""" """
raise NotImplementedError() raise NotImplementedError()
SECoP_BASE_CLASSES = {Readable, Writable, Drivable, Communicator}
class Attached(Property): class Attached(Property):
"""a special property, defining an attached module """a special property, defining an attached module

View File

@ -21,11 +21,11 @@
# ***************************************************************************** # *****************************************************************************
from frappy.core import Parameter, FloatRange, BUSY, IDLE, WARN from frappy.core import Parameter, FloatRange, BUSY, IDLE, WARN
from frappy.states import HasStates
from frappy.lib.statemachine import StateMachine, Retry, Stop from frappy.lib.statemachine import StateMachine, Retry, Stop
from frappy.lib import merge_status
class HasConvergence(HasStates): class HasConvergence:
"""mixin for convergence checks """mixin for convergence checks
Implementation based on tolerance, settling time and timeout. Implementation based on tolerance, settling time and timeout.
@ -33,6 +33,8 @@ class HasConvergence(HasStates):
fly. However, the full history is not considered, which means for example fly. However, the full history is not considered, which means for example
that the spent time inside tolerance stored already is not altered when that the spent time inside tolerance stored already is not altered when
changing tolerance. changing tolerance.
does not inherit from HasStates (own state machine!)
""" """
tolerance = Parameter('absolute tolerance', FloatRange(0, unit='$'), readonly=False, default=0) tolerance = Parameter('absolute tolerance', FloatRange(0, unit='$'), readonly=False, default=0)
settling_time = Parameter( settling_time = Parameter(
@ -51,118 +53,131 @@ class HasConvergence(HasStates):
As soon as the value is the first time within tolerance, the timeout criterium is changed: As soon as the value is the first time within tolerance, the timeout criterium is changed:
then the timeout event happens after this time + <settling_time> + <timeout>. then the timeout event happens after this time + <settling_time> + <timeout>.
''', FloatRange(0, unit='sec'), readonly=False, default=3600) ''', FloatRange(0, unit='sec'), readonly=False, default=3600)
status = Parameter('status determined from convergence checks', default=(IDLE, '')) status = Parameter() # make sure status is a parameter
convergence_state = None convergence_state = None # the state machine
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
self.convergence_state = StateMachine(threaded=False, logger=self.log, self.convergence_state = StateMachine(
cleanup=self.cleanup, spent_inside=0) threaded=False, logger=self.log, cleanup=self.convergence_cleanup,
status=(IDLE, ''), spent_inside=0, stop_status=(IDLE, 'stopped'))
def cleanup(self, state): def convergence_cleanup(self, state):
state.default_cleanup(state) state.default_cleanup(state)
if state.stopped: if state.stopped:
if state.stopped is Stop: # and not Restart if state.stopped is Stop: # and not Restart
self.status = WARN, 'stopped' self.__set_status(WARN, 'stopped')
else: else:
self.status = WARN, repr(state.last_error) self.__set_status(WARN, repr(state.last_error))
def doPoll(self): def doPoll(self):
super().doPoll() super().doPoll()
state = self.convergence_state state = self.convergence_state
state.cycle() state.cycle()
def get_min_slope(self, dif): def __set_status(self, *status):
if status != self.convergence_state.status:
self.convergence_state.status = status
self.read_status()
def read_status(self):
return merge_status(super().read_status(), self.convergence_state.status)
#self.log.warn('inner %r conv %r merged %r', super().read_status(), self.convergence_state.status, merged)
#return merged
def convergence_min_slope(self, dif):
"""calculate minimum expected slope"""
slope = getattr(self, 'workingramp', 0) or getattr(self, 'ramp', 0) slope = getattr(self, 'workingramp', 0) or getattr(self, 'ramp', 0)
if slope or not self.timeout: if slope or not self.timeout:
return slope return slope
return dif / self.timeout # assume exponential decay of dif, with time constant <tolerance> return dif / self.timeout # assume exponential decay of dif, with time constant <tolerance>
def get_dif_tol(self): def convergence_dif(self):
value = self.read_value() """get difference target - value and tolerance"""
tol = self.tolerance tol = self.tolerance
if not tol: if not tol:
tol = 0.01 * max(abs(self.target), abs(value)) tol = 0.01 * max(abs(self.target), abs(self.value))
dif = abs(self.target - value) dif = abs(self.target - self.value)
return dif, tol return dif, tol
def start_state(self): def convergence_start(self):
"""to be called from write_target""" """to be called from write_target"""
self.convergence_state.start(self.state_approach) self.__set_status(BUSY, 'changed_target')
self.convergence_state.start(self.convergence_approach)
def state_approach(self, state): def convergence_approach(self, state):
"""approaching, checking progress (busy)""" """approaching, checking progress (busy)"""
state.spent_inside = 0 state.spent_inside = 0
dif, tol = self.get_dif_tol() dif, tol = self.convergence_dif()
if dif < tol: if dif < tol:
state.timeout_base = state.now state.timeout_base = state.now
return self.state_inside return self.convergence_inside
if not self.timeout: if not self.timeout:
return Retry return Retry
if state.init: if state.init:
state.timeout_base = state.now state.timeout_base = state.now
state.dif_crit = dif # criterium for resetting timeout base state.dif_crit = dif # criterium for resetting timeout base
self.status = BUSY, 'approaching' self.__set_status(BUSY, 'approaching')
state.dif_crit -= self.get_min_slope(dif) * state.delta() state.dif_crit -= self.convergence_min_slope(dif) * state.delta()
if dif < state.dif_crit: # progress is good: reset timeout base if dif < state.dif_crit: # progress is good: reset timeout base
state.timeout_base = state.now state.timeout_base = state.now
elif state.now > state.timeout_base + self.timeout: elif state.now > state.timeout_base + self.timeout:
self.status = WARN, 'convergence timeout' self.__set_status(WARN, 'convergence timeout')
return self.state_instable return self.convergence_instable
return Retry return Retry
def state_inside(self, state): def convergence_inside(self, state):
"""inside tolerance, still busy""" """inside tolerance, still busy"""
dif, tol = self.get_dif_tol() dif, tol = self.convergence_dif()
if dif > tol: if dif > tol:
return self.state_outside return self.convergence_outside
state.spent_inside += state.delta() state.spent_inside += state.delta()
if state.spent_inside > self.settling_time: if state.spent_inside > self.settling_time:
self.status = IDLE, 'reached target' self.__set_status(IDLE, 'reached target')
return self.state_stable return self.convergence_stable
if state.init: if state.init:
self.status = BUSY, 'inside tolerance' self.__set_status(BUSY, 'inside tolerance')
return Retry return Retry
def state_outside(self, state): def convergence_outside(self, state):
"""temporarely outside tolerance, busy""" """temporarely outside tolerance, busy"""
dif, tol = self.get_dif_tol() dif, tol = self.convergence_dif()
if dif < tol: if dif < tol:
return self.state_inside return self.convergence_inside
if state.now > state.timeout_base + self.settling_time + self.timeout: if state.now > state.timeout_base + self.settling_time + self.timeout:
self.status = WARN, 'settling timeout' self.__set_status(WARN, 'settling timeout')
return self.state_instable return self.convergence_instable
if state.init: if state.init:
self.status = BUSY, 'outside tolerance' self.__set_status(BUSY, 'outside tolerance')
# do not reset the settling time on occasional outliers, count backwards instead # do not reset the settling time on occasional outliers, count backwards instead
state.spent_inside = max(0.0, state.spent_inside - state.delta()) state.spent_inside = max(0.0, state.spent_inside - state.delta())
return Retry return Retry
def state_stable(self, state): def convergence_stable(self, state):
"""stable, after settling_time spent within tolerance, idle""" """stable, after settling_time spent within tolerance, idle"""
dif, tol = self.get_dif_tol() dif, tol = self.convergence_dif()
if dif <= tol: if dif <= tol:
return Retry return Retry
self.status = WARN, 'instable' self.__set_status(WARN, 'instable')
state.spent_inside = max(self.settling_time, state.spent_inside) state.spent_inside = max(self.settling_time, state.spent_inside)
return self.state_instable return self.convergence_instable
def state_instable(self, state): def convergence_instable(self, state):
"""went outside tolerance from stable, warning""" """went outside tolerance from stable, warning"""
dif, tol = self.get_dif_tol() dif, tol = self.convergence_dif()
if dif <= tol: if dif <= tol:
state.spent_inside += state.delta() state.spent_inside += state.delta()
if state.spent_inside > self.settling_time: if state.spent_inside > self.settling_time:
self.status = IDLE, 'stable' # = recovered from instable self.__set_status(IDLE, 'stable') # = recovered from instable
return self.state_stable return self.convergence_stable
else: else:
state.spent_inside = max(0, state.spent_inside - state.delta()) state.spent_inside = max(0, state.spent_inside - state.delta())
return Retry return Retry
def state_interrupt(self, state): def convergence_interrupt(self, state):
"""stopping""" """stopping"""
self.status = IDLE, 'stopped' # stop called self.__set_status(state.stop_status) # stop called
return self.state_instable return self.convergence_instable
def stop(self): def stop(self):
"""set to idle when busy """set to idle when busy
@ -170,4 +185,14 @@ class HasConvergence(HasStates):
does not stop control! does not stop control!
""" """
if self.isBusy(): if self.isBusy():
self.convergence_state.start(self.state_interrupt) self.convergence_state.start(self.convergence_interrupt)
def write_settling_time(self, value):
if self.pollInfo:
self.pollInfo.trigger(True)
return value
def write_tolerance(self, value):
if self.pollInfo:
self.pollInfo.trigger(True)
return value

View File

@ -21,7 +21,7 @@
"""oxford instruments mercury IPS power supply""" """oxford instruments mercury IPS power supply"""
import time import time
from frappy.core import Parameter, EnumType, FloatRange, BoolType, IntRange, StringType, Property, BUSY from frappy.core import Parameter, EnumType, FloatRange, BoolType, IntRange, Property, Module
from frappy.lib.enum import Enum from frappy.lib.enum import Enum
from frappy.errors import BadValueError, HardwareError from frappy.errors import BadValueError, HardwareError
from frappy_psi.magfield import Magfield, SimpleMagfield, Status from frappy_psi.magfield import Magfield, SimpleMagfield, Status
@ -41,16 +41,16 @@ class SimpleField(MercuryChannel, SimpleMagfield):
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0) voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0) atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0) working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
channel_type = 'PSU' kind = 'PSU'
slave_currents = None slave_currents = None
classdict = {} classdict = {}
def __new__(cls, name, logger, cfgdict, srv): def __new__(cls, name, logger, cfgdict, srv): # pylint: disable=arguments-differ
base = cls.__bases__[1]
nunits = cfgdict.get('nunits', 1) nunits = cfgdict.get('nunits', 1)
if isinstance(nunits, dict):
nunits = nunits['value']
if nunits == 1: if nunits == 1:
obj = object.__new__(cls) return Module.__new__(cls, name, logger, cfgdict, srv)
return obj
classname = cls.__name__ + str(nunits) classname = cls.__name__ + str(nunits)
newclass = cls.classdict.get(classname) newclass = cls.classdict.get(classname)
if not newclass: if not newclass:
@ -62,8 +62,7 @@ class SimpleField(MercuryChannel, SimpleMagfield):
newclass = type(classname, (cls,), attrs) newclass = type(classname, (cls,), attrs)
cls.classdict[classname] = newclass cls.classdict[classname] = newclass
obj = object.__new__(newclass) return Module.__new__(newclass, name, logger, cfgdict, srv)
return obj
def initModule(self): def initModule(self):
super().initModule() super().initModule()
@ -73,34 +72,34 @@ class SimpleField(MercuryChannel, SimpleMagfield):
self.log.error('can not set to hold %r', e) self.log.error('can not set to hold %r', e)
def read_value(self): def read_value(self):
return self.query('PSU:SIG:FLD') return self.query('DEV::PSU:SIG:FLD')
def read_ramp(self): def read_ramp(self):
return self.query('PSU:SIG:RFST') return self.query('DEV::PSU:SIG:RFST')
def write_ramp(self, value): def write_ramp(self, value):
return self.change('PSU:SIG:RFST', value) return self.change('DEV::PSU:SIG:RFST', value)
def read_action(self): def read_action(self):
return self.query('PSU:ACTN', hold_rtoz_rtos_clmp) return self.query('DEV::PSU:ACTN', hold_rtoz_rtos_clmp)
def write_action(self, value): def write_action(self, value):
return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp) return self.change('DEV::PSU:ACTN', value, hold_rtoz_rtos_clmp)
def read_atob(self): def read_atob(self):
return self.query('PSU:ATOB') return self.query('DEV::PSU:ATOB')
def read_voltage(self): def read_voltage(self):
return self.query('PSU:SIG:VOLT') return self.query('DEV::PSU:SIG:VOLT')
def read_working_ramp(self): def read_working_ramp(self):
return self.query('PSU:SIG:RFLD') return self.query('DEV::PSU:SIG:RFLD')
def read_setpoint(self): def read_setpoint(self):
return self.query('PSU:SIG:FSET') return self.query('DEV::PSU:SIG:FSET')
def set_and_go(self, value): def set_and_go(self, value):
self.setpoint = self.change('PSU:SIG:FSET', value) self.setpoint = self.change('DEV::PSU:SIG:FSET', value)
assert self.write_action(Action.hold) == Action.hold assert self.write_action(Action.hold) == Action.hold
assert self.write_action(Action.run_to_set) == Action.run_to_set assert self.write_action(Action.run_to_set) == Action.run_to_set
@ -133,7 +132,7 @@ class SimpleField(MercuryChannel, SimpleMagfield):
class Field(SimpleField, Magfield): class Field(SimpleField, Magfield):
persistent_field = Parameter( persistent_field = Parameter(
'persistent field', FloatRange(unit='$'), readonly=False) 'persistent field at last switch off', FloatRange(unit='$'), readonly=False)
wait_switch_on = Parameter( wait_switch_on = Parameter(
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60) 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60)
wait_switch_off = Parameter( wait_switch_off = Parameter(
@ -142,7 +141,7 @@ class Field(SimpleField, Magfield):
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
_field_mismatch = None _field_mismatch = None
__init = True __persistent_field = None # internal value of persistent field
__switch_fixed_until = 0 __switch_fixed_until = 0
def doPoll(self): def doPoll(self):
@ -154,33 +153,41 @@ class Field(SimpleField, Magfield):
# will complain and this will be handled in start_ramp_to_field # will complain and this will be handled in start_ramp_to_field
self.switch_on_time = 0 self.switch_on_time = 0
self.switch_off_time = 0 self.switch_off_time = 0
self.switch_heater = self.query('PSU:SIG:SWHT', off_on) self.switch_heater = self.query('DEV::PSU:SIG:SWHT', off_on)
super().startModule(start_events) super().startModule(start_events)
def read_value(self): def read_value(self):
current = self.query('PSU:SIG:FLD') current = self.query('DEV::PSU:SIG:FLD')
pf = self.query('PSU:SIG:PFLD') if self.switch_heater == self.switch_heater.on:
if self.__init: self.__persistent_field = current
self.__init = False
self.persistent_field = pf
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
self.forced_persistent_field = False self.forced_persistent_field = False
self._field_mismatch = False
return current return current
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance pf = self.query('DEV::PSU:SIG:PFLD')
return pf if self.__persistent_field is None:
self.__persistent_field = pf
self._field_mismatch = False
else:
self._field_mismatch = abs(self.__persistent_field - pf) > self.tolerance * 10
self.persistent_field = self.__persistent_field
return self.__persistent_field
def _check_adr(self, adr):
"""avoid complains about bad slot"""
if adr.startswith('DEV:PSU.M'):
return
super()._check_adr(adr)
def read_current(self): def read_current(self):
if self.slave_currents is None: if self.slave_currents is None:
self.slave_currents = [[] for _ in range(self.nunits + 1)] self.slave_currents = [[] for _ in range(self.nunits + 1)]
if self.nunits > 1: if self.nunits > 1:
for i in range(1, self.nunits + 1): for i in range(1, self.nunits + 1):
curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i) curri = self.query(f'DEV:PSU.M{i}:PSU:SIG:CURR')
volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i) volti = self.query(f'DEV:PSU.M{i}:PSU:SIG:VOLT')
setattr(self, 'I%d' % i, curri) setattr(self, f'I{i}', curri)
setattr(self, 'V%d' % i, volti) setattr(self, f'V{i}', volti)
self.slave_currents[i].append(curri) self.slave_currents[i].append(curri)
current = self.query('PSU:SIG:CURR') current = self.query('DEV::PSU:SIG:CURR')
self.slave_currents[0].append(current) self.slave_currents[0].append(current)
min_ = min(self.slave_currents[0]) / self.nunits min_ = min(self.slave_currents[0]) / self.nunits
max_ = max(self.slave_currents[0]) / self.nunits max_ = max(self.slave_currents[0]) / self.nunits
@ -194,14 +201,15 @@ class Field(SimpleField, Magfield):
if min_i - 0.1 > max_ or min_ > max_i + 0.1: # use an arbitrary 0.1 A tolerance if min_i - 0.1 > max_ or min_ > max_i + 0.1: # use an arbitrary 0.1 A tolerance
self.log.warning('individual currents mismatch %r', self.slave_currents) self.log.warning('individual currents mismatch %r', self.slave_currents)
else: else:
current = self.query('PSU:SIG:CURR') current = self.query('DEV::PSU:SIG:CURR')
if self.atob: if self.atob:
return current / self.atob return current / self.atob
return 0 return 0
def write_persistent_field(self, value): def write_persistent_field(self, value):
if self.forced_persistent_field: if self.forced_persistent_field or abs(self.__persistent_field - value) <= self.tolerance * 10:
self._field_mismatch = False self._field_mismatch = False
self.__persistent_field = value
return value return value
raise BadValueError('changing persistent field needs forced_persistent_field=True') raise BadValueError('changing persistent field needs forced_persistent_field=True')
@ -212,7 +220,7 @@ class Field(SimpleField, Magfield):
return super().write_target(target) return super().write_target(target)
def read_switch_heater(self): def read_switch_heater(self):
value = self.query('PSU:SIG:SWHT', off_on) value = self.query('DEV::PSU:SIG:SWHT', off_on)
now = time.time() now = time.time()
if value != self.switch_heater: if value != self.switch_heater:
if now < self.__switch_fixed_until: if now < self.__switch_fixed_until:
@ -226,10 +234,10 @@ class Field(SimpleField, Magfield):
return value return value
def read_wait_switch_on(self): def read_wait_switch_on(self):
return self.query('PSU:SWONT') * 0.001 return self.query('DEV::PSU:SWONT') * 0.001
def read_wait_switch_off(self): def read_wait_switch_off(self):
return self.query('PSU:SWOFT') * 0.001 return self.query('DEV::PSU:SWOFT') * 0.001
def write_switch_heater(self, value): def write_switch_heater(self, value):
if value == self.read_switch_heater(): if value == self.read_switch_heater():
@ -238,20 +246,20 @@ class Field(SimpleField, Magfield):
return value return value
self.__switch_fixed_until = time.time() + 10 self.__switch_fixed_until = time.time() + 10
self.log.debug('switch time fixed for 10 sec') self.log.debug('switch time fixed for 10 sec')
result = self.change('PSU:SIG:SWHT', value, off_on, n_retry=0) # no readback check result = self.change('DEV::PSU:SIG:SWHT', value, off_on, n_retry=0) # no readback check
return result return result
def start_ramp_to_field(self, sm): def start_ramp_to_field(self, sm):
if abs(self.current - self.persistent_field) <= self.tolerance: if abs(self.current - self.__persistent_field) <= self.tolerance:
self.log.info('leads %g are already at %g', self.current, self.persistent_field) self.log.info('leads %g are already at %g', self.current, self.__persistent_field)
return self.ramp_to_field return self.ramp_to_field
try: try:
self.set_and_go(self.persistent_field) self.set_and_go(self.__persistent_field)
except (HardwareError, AssertionError) as e: except (HardwareError, AssertionError) as e:
if self.switch_heater: if self.switch_heater:
self.log.warn('switch is already on!') self.log.warn('switch is already on!')
return self.ramp_to_field return self.ramp_to_field
self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.persistent_field, e) self.log.warn('wait first for switch off current=%g pf=%g %r', self.current, self.__persistent_field, e)
sm.after_wait = self.ramp_to_field sm.after_wait = self.ramp_to_field
return self.wait_for_switch return self.wait_for_switch
return self.ramp_to_field return self.ramp_to_field
@ -274,7 +282,7 @@ class Field(SimpleField, Magfield):
sm.try_cnt -= 1 sm.try_cnt -= 1
if sm.try_cnt < 0: if sm.try_cnt < 0:
raise raise
self.set_and_go(sm.persistent_field) self.set_and_go(self.__persistent_field)
return Retry return Retry
def wait_for_switch(self, sm): def wait_for_switch(self, sm):
@ -283,14 +291,14 @@ class Field(SimpleField, Magfield):
try: try:
self.log.warn('try again') self.log.warn('try again')
# try again # try again
self.set_and_go(self.persistent_field) self.set_and_go(self.__persistent_field)
except (HardwareError, AssertionError) as e: except (HardwareError, AssertionError):
return Retry return Retry
return sm.after_wait return sm.after_wait
def wait_for_switch_on(self, sm): def wait_for_switch_on(self, sm):
self.read_switch_heater() # trigger switch_on/off_time self.read_switch_heater() # trigger switch_on/off_time
if self.switch_heater == self.switch_heater.OFF: if self.switch_heater == self.switch_heater.off:
if sm.init: # avoid too many states chained if sm.init: # avoid too many states chained
return Retry return Retry
self.log.warning('switch turned off manually?') self.log.warning('switch turned off manually?')
@ -299,7 +307,7 @@ class Field(SimpleField, Magfield):
def wait_for_switch_off(self, sm): def wait_for_switch_off(self, sm):
self.read_switch_heater() self.read_switch_heater()
if self.switch_heater == self.switch_heater.ON: if self.switch_heater == self.switch_heater.on:
if sm.init: # avoid too many states chained if sm.init: # avoid too many states chained
return Retry return Retry
self.log.warning('switch turned on manually?') self.log.warning('switch turned on manually?')
@ -307,6 +315,9 @@ class Field(SimpleField, Magfield):
return super().wait_for_switch_off(sm) return super().wait_for_switch_off(sm)
def start_ramp_to_zero(self, sm): def start_ramp_to_zero(self, sm):
pf = self.query('DEV::PSU:SIG:PFLD')
if abs(pf - self.value) > self.tolerance * 10:
self.log.warning('persistent field %g does not match %g after switch off', pf, self.value)
try: try:
assert self.write_action(Action.hold) == Action.hold assert self.write_action(Action.hold) == Action.hold
assert self.write_action(Action.run_to_zero) == Action.run_to_zero assert self.write_action(Action.run_to_zero) == Action.run_to_zero

View File

@ -20,12 +20,11 @@
"""generic persistent magnet driver""" """generic persistent magnet driver"""
import time import time
from frappy.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR from frappy.core import Drivable, Parameter, BUSY, Limit
from frappy.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType from frappy.datatypes import FloatRange, EnumType, TupleOf, StatusType
from frappy.features import HasTargetLimits from frappy.errors import ConfigError, HardwareError, DisabledError
from frappy.errors import ConfigError, ProgrammingError, HardwareError, BadValueError
from frappy.lib.enum import Enum from frappy.lib.enum import Enum
from frappy.states import Retry, HasStates, status_code, Start from frappy.states import Retry, HasStates, status_code
UNLIMITED = FloatRange() UNLIMITED = FloatRange()
@ -48,8 +47,10 @@ OFF = 0
ON = 1 ON = 1
class SimpleMagfield(HasStates, HasTargetLimits, Drivable): class SimpleMagfield(HasStates, Drivable):
value = Parameter('magnetic field', datatype=FloatRange(unit='T')) value = Parameter('magnetic field', datatype=FloatRange(unit='T'))
target_min = Limit()
target_max = Limit()
ramp = Parameter( ramp = Parameter(
'wanted ramp rate for field', FloatRange(unit='$/min'), readonly=False) 'wanted ramp rate for field', FloatRange(unit='$/min'), readonly=False)
# export only when different from ramp: # export only when different from ramp:
@ -68,6 +69,22 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
FloatRange(0, unit='s'), readonly=False, default=30) FloatRange(0, unit='s'), readonly=False, default=30)
_last_target = None _last_target = None
_symmetric_limits = False
def earlyInit(self):
super().earlyInit()
# when limits are symmetric at init, we want auto symmetric limits
self._symmetric_limits = self.target_min == -self.target_max
def write_target_max(self, value):
if self._symmetric_limits:
self.target_min = -value
return value
def write_target_min(self, value):
"""when modified to other than a symmetric value, we assume the user does not want auto symmetric limits"""
self._symmetric_limits = value == -self.target_max
return value
def checkProperties(self): def checkProperties(self):
dt = self.parameters['target'].datatype dt = self.parameters['target'].datatype
@ -76,6 +93,9 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
raise ConfigError('target.max not configured') raise ConfigError('target.max not configured')
if dt.min == UNLIMITED.min: # not given: assume bipolar symmetric if dt.min == UNLIMITED.min: # not given: assume bipolar symmetric
dt.min = -max_ dt.min = -max_
self.target_min = max(dt.min, self.target_min)
if 'target_max' in self.writeDict:
self.writeDict.setdefault('target_min', -self.writeDict['target_max'])
super().checkProperties() super().checkProperties()
def stop(self): def stop(self):
@ -101,7 +121,6 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
return last return last
def write_target(self, target): def write_target(self, target):
self.check_limits(target)
self.start_machine(self.start_field_change, target=target) self.start_machine(self.start_field_change, target=target)
return target return target
@ -147,8 +166,7 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
class Magfield(SimpleMagfield): class Magfield(SimpleMagfield):
status = Parameter(datatype=StatusType(Status)) status = Parameter(datatype=StatusType(Status))
mode = Parameter( mode = Parameter('persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT)
'persistent mode', EnumType(Mode), readonly=False, initwrite=False, default=Mode.PERSISTENT)
switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON), switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
readonly=False, default=0) readonly=False, default=0)
current = Parameter( current = Parameter(
@ -180,6 +198,9 @@ class Magfield(SimpleMagfield):
def doPoll(self): def doPoll(self):
if self.init_persistency: if self.init_persistency:
if self.read_switch_heater() and self.mode != Mode.DRIVEN: if self.read_switch_heater() and self.mode != Mode.DRIVEN:
# switch heater is on on startup: got to persistent mode
# do this after some delay, so the user might continue
# driving without delay after a restart
self.start_machine(self.go_persistent_soon, mode=self.mode) self.start_machine(self.go_persistent_soon, mode=self.mode)
self.init_persistency = False self.init_persistency = False
super().doPoll() super().doPoll()
@ -207,9 +228,7 @@ class Magfield(SimpleMagfield):
if self.mode == Mode.DISABLED: if self.mode == Mode.DISABLED:
if target == 0: if target == 0:
return 0 return 0
self.log.info('raise error %r', target) raise DisabledError('disabled')
raise BadValueError('disabled')
self.check_limits(target)
self.start_machine(self.start_field_change, target=target, mode=self.mode) self.start_machine(self.start_field_change, target=target, mode=self.mode)
return target return target

View File

@ -25,12 +25,13 @@ import math
import re import re
import time import time
from frappy.core import Drivable, HasIO, Writable, \ from frappy.core import Drivable, HasIO, Writable, StatusType, \
Parameter, Property, Readable, StringIO, Attached, IDLE, nopoll Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll
from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf
from frappy.errors import HardwareError, ProgrammingError, ConfigError from frappy.errors import HardwareError, ProgrammingError, ConfigError, RangeError
from frappy_psi.convergence import HasConvergence from frappy_psi.convergence import HasConvergence
from frappy.lib.enum import Enum from frappy.states import Retry, Finish
from frappy.mixins import HasOutputModule, HasControlledBy
VALUE_UNIT = re.compile(r'(.*\d|inf)([A-Za-z/%]*)$') VALUE_UNIT = re.compile(r'(.*\d|inf)([A-Za-z/%]*)$')
@ -126,7 +127,7 @@ class MercuryChannel(HasIO):
msg = f'invalid reply {reply!r} to cmd {cmd!r}' msg = f'invalid reply {reply!r} to cmd {cmd!r}'
raise HardwareError(msg) from None raise HardwareError(msg) from None
def multichange(self, adr, values, convert=as_float, tolerance=0, n_retry=3): def multichange(self, adr, values, convert=as_float, tolerance=0, n_retry=3, lazy=False):
"""set parameter(s) in mercury syntax """set parameter(s) in mercury syntax
:param adr: as in multiquery method. SET: is added automatically :param adr: as in multiquery method. SET: is added automatically
@ -134,6 +135,7 @@ class MercuryChannel(HasIO):
:param convert: a converter function (converts given value to string and replied string to value) :param convert: a converter function (converts given value to string and replied string to value)
:param tolerance: tolerance for readback check :param tolerance: tolerance for readback check
:param n_retry: number of retries or 0 for no readback check :param n_retry: number of retries or 0 for no readback check
:param lazy: check direct reply only (no additional query)
:return: the values as tuple :return: the values as tuple
Example (kind=TEMP, slot=DB6.T1: Example (kind=TEMP, slot=DB6.T1:
@ -145,6 +147,7 @@ class MercuryChannel(HasIO):
adr = self._complete_adr(adr) adr = self._complete_adr(adr)
params = [f'{k}:{convert(v)}' for k, v in values] params = [f'{k}:{convert(v)}' for k, v in values]
cmd = f"SET:{adr}:{':'.join(params)}" cmd = f"SET:{adr}:{':'.join(params)}"
givenkeys = tuple(v[0] for v in values)
for _ in range(max(1, n_retry)): # try n_retry times or until readback result matches for _ in range(max(1, n_retry)): # try n_retry times or until readback result matches
reply = self.communicate(cmd) reply = self.communicate(cmd)
head = f'STAT:SET:{adr}:' head = f'STAT:SET:{adr}:'
@ -153,29 +156,35 @@ class MercuryChannel(HasIO):
replyiter = iter(reply[len(head):].split(':')) replyiter = iter(reply[len(head):].split(':'))
# reshuffle reply=(k1, r1, v1, k2, r2, v1) --> keys = (k1, k2), result = (r1, r2), valid = (v1, v2) # reshuffle reply=(k1, r1, v1, k2, r2, v1) --> keys = (k1, k2), result = (r1, r2), valid = (v1, v2)
keys, result, valid = zip(*zip(replyiter, replyiter, replyiter)) keys, result, valid = zip(*zip(replyiter, replyiter, replyiter))
assert keys == tuple(k for k, _ in values) assert keys == givenkeys
assert any(v == 'VALID' for v in valid) assert any(v == 'VALID' for v in valid)
result = tuple(convert(r) for r in result) result = tuple(convert(r) for r in result)
except (AssertionError, AttributeError, ValueError) as e: except (AssertionError, AttributeError, ValueError) as e:
time.sleep(0.1) # in case of missed replies this might help to skip garbage time.sleep(0.1) # in case of missed replies this might help to skip garbage
raise HardwareError(f'invalid reply {reply!r} to cmd {cmd!r}') from e raise HardwareError(f'invalid reply {reply!r} to cmd {cmd!r}') from e
if n_retry == 0: if n_retry == 0:
return [v[1] for v in values] # no readback check return [v for _, v in values]
keys = [v[0] for v in values] if lazy:
debug = [reply]
readback = [v for _, v in values]
else:
debug = [] debug = []
readback = self.multiquery(adr, keys, convert, debug) readback = list(self.multiquery(adr, givenkeys, convert, debug))
for k, r, b in zip(keys, result, readback): failed = False
for i, ((k, v), r, b) in enumerate(zip(values, result, readback)):
if convert is as_float: if convert is as_float:
tol = max(abs(r) * 1e-3, abs(b) * 1e-3, tolerance) tol = max(abs(r) * 1e-3, abs(b) * 1e-3, tolerance)
if abs(r - b) > tol: if abs(b - v) > tol or abs(r - v) > tol:
break readback[i] = None
elif r != b: failed = True
break elif b != v or r != v:
else: readback[i] = None
failed = True
if not failed:
return readback return readback
self.log.warning('sent: %s', cmd) self.log.warning('sent: %s', cmd)
self.log.warning('got: %s', debug[0]) self.log.warning('got: %s', debug[0])
return readback return tuple(v[1] if b is None else b for b, v in zip(readback, values))
def query(self, adr, convert=as_float): def query(self, adr, convert=as_float):
"""query a single parameter """query a single parameter
@ -185,9 +194,9 @@ class MercuryChannel(HasIO):
adr, _, name = adr.rpartition(':') adr, _, name = adr.rpartition(':')
return self.multiquery(adr, [name], convert)[0] return self.multiquery(adr, [name], convert)[0]
def change(self, adr, value, convert=as_float, tolerance=0, n_retry=3): def change(self, adr, value, convert=as_float, tolerance=0, n_retry=3, lazy=False):
adr, _, name = adr.rpartition(':') adr, _, name = adr.rpartition(':')
return self.multichange(adr, [(name, value)], convert, tolerance, n_retry)[0] return self.multichange(adr, [(name, value)], convert, tolerance, n_retry, lazy)[0]
class TemperatureSensor(MercuryChannel, Readable): class TemperatureSensor(MercuryChannel, Readable):
@ -202,38 +211,14 @@ class TemperatureSensor(MercuryChannel, Readable):
return self.query('DEV::TEMP:SIG:RES') return self.query('DEV::TEMP:SIG:RES')
class HasInput(MercuryChannel): class HasInput(HasControlledBy, MercuryChannel):
controlled_by = Parameter('source of target value', EnumType(members={'self': SELF}), default=0) pass
# do not know why this? target = Parameter(readonly=False)
input_callbacks = ()
def register_input(self, name, control_off):
"""register input
:param name: the name of the module (for controlled_by enum)
:param control_off: a method on the input module to switch off control
"""
if not self.input_callbacks:
self.input_callbacks = []
self.input_callbacks.append(control_off)
prev_enum = self.parameters['controlled_by'].datatype._enum
# add enum member, using autoincrement feature of Enum
self.parameters['controlled_by'].datatype = EnumType(Enum(prev_enum, **{name: None}))
def write_controlled_by(self, value):
if self.controlled_by == value:
return value
self.controlled_by = value
if value == SELF:
for control_off in self.input_callbacks:
control_off()
return value
class Loop(HasConvergence, MercuryChannel, Drivable): class Loop(HasOutputModule, MercuryChannel, Drivable):
"""common base class for loops""" """common base class for loops"""
control_active = Parameter('control mode', BoolType())
output_module = Attached(HasInput, mandatory=False) output_module = Attached(HasInput, mandatory=False)
control_active = Parameter(readonly=False)
ctrlpars = Parameter( ctrlpars = Parameter(
'pid (proportional band, integral time, differential time', 'pid (proportional band, integral time, differential time',
StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')), StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')),
@ -241,35 +226,15 @@ class Loop(HasConvergence, MercuryChannel, Drivable):
) )
enable_pid_table = Parameter('', BoolType(), readonly=False) enable_pid_table = Parameter('', BoolType(), readonly=False)
def initModule(self): def set_output(self, active, source='HW'):
super().initModule()
if self.output_module:
self.output_module.register_input(self.name, self.control_off)
def control_off(self):
if self.control_active:
self.log.warning('switch to manual mode')
self.write_control_active(False)
def set_output(self, active):
if active: if active:
if self.output_module and self.output_module.controlled_by != self.name: self.activate_control()
self.output_module.write_controlled_by(self.name)
else: else:
if self.output_module and self.output_module.controlled_by != SELF: self.deactivate_control(source)
self.output_module.write_controlled_by(SELF)
status = IDLE, 'control inactive'
if self.status != status:
self.status = status
def set_target(self, target): def set_target(self, target):
if self.control_active:
self.set_output(True) self.set_output(True)
else:
self.log.warning('switch loop control on')
self.write_control_active(True)
self.target = target self.target = target
self.start_state()
def read_enable_pid_table(self): def read_enable_pid_table(self):
return self.query(f'DEV::{self.kinds[0]}:LOOP:PIDT', off_on) return self.query(f'DEV::{self.kinds[0]}:LOOP:PIDT', off_on)
@ -286,8 +251,24 @@ class Loop(HasConvergence, MercuryChannel, Drivable):
pid = self.multichange(f'DEV::{self.kinds[0]}:LOOP', [(k, value[k.lower()]) for k in 'PID']) pid = self.multichange(f'DEV::{self.kinds[0]}:LOOP', [(k, value[k.lower()]) for k in 'PID'])
return {k.lower(): v for k, v in zip('PID', pid)} return {k.lower(): v for k, v in zip('PID', pid)}
def read_status(self):
return IDLE, ''
class HeaterOutput(HasInput, MercuryChannel, Writable):
class ConvLoop(HasConvergence, Loop):
def deactivate_control(self, source):
if self.control_active:
super().deactivate_control(source)
self.convergence_state.start(self.inactive_state)
if self.pollInfo:
self.pollInfo.trigger(True)
def inactive_state(self, state):
self.convergence_state.status = IDLE, 'control inactive'
return Finish
class HeaterOutput(HasInput, Writable):
"""heater output """heater output
Remark: Remark:
@ -324,7 +305,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
if self._last_target is not None: if self._last_target is not None:
if not self.true_power: if not self.true_power:
self._volt_target = math.sqrt(self._last_target * self.resistivity) self._volt_target = math.sqrt(self._last_target * self.resistivity)
self.change('DEV::HTR:SIG:VOLT', self._volt_target) self.change('DEV::HTR:SIG:VOLT', self._volt_target, tolerance=2e-4)
return self.resistivity return self.resistivity
def read_status(self): def read_status(self):
@ -344,7 +325,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
self.write_resistivity(round(res, 1)) self.write_resistivity(round(res, 1))
if self.controlled_by == 0: if self.controlled_by == 0:
self._volt_target = math.sqrt(self._last_target * self.resistivity) self._volt_target = math.sqrt(self._last_target * self.resistivity)
self.change('DEV::HTR:SIG:VOLT', self._volt_target) self.change('DEV::HTR:SIG:VOLT', self._volt_target, tolerance=2e-4)
return volt * current return volt * current
def read_target(self): def read_target(self):
@ -362,23 +343,25 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
might be used by a software loop might be used by a software loop
""" """
self._volt_target = math.sqrt(target * self.resistivity) self._volt_target = math.sqrt(target * self.resistivity)
self.change('DEV::HTR:SIG:VOLT', self._volt_target) self.change('DEV::HTR:SIG:VOLT', self._volt_target, tolerance=2e-4)
self._last_target = target self._last_target = target
return target return target
def write_target(self, value): def write_target(self, value):
self.write_controlled_by(SELF) self.self_controlled()
return self.set_target(value) return self.set_target(value)
class TemperatureLoop(TemperatureSensor, Loop, Drivable): class TemperatureLoop(TemperatureSensor, ConvLoop):
kind = 'TEMP' kind = 'TEMP'
output_module = Attached(HasInput, mandatory=False) output_module = Attached(HasInput, mandatory=False)
ramp = Parameter('ramp rate', FloatRange(0, unit='$/min'), readonly=False) ramp = Parameter('ramp rate', FloatRange(0, unit='$/min'), readonly=False)
enable_ramp = Parameter('enable ramp rate', BoolType(), readonly=False) enable_ramp = Parameter('enable ramp rate', BoolType(), readonly=False)
setpoint = Parameter('working setpoint (differs from target when ramping)', FloatRange(0, unit='$')) setpoint = Parameter('working setpoint (differs from target when ramping)', FloatRange(0, unit='$'))
status = Parameter(datatype=StatusType(Drivable, 'RAMPING')) # add ramping status
tolerance = Parameter(default=0.1) tolerance = Parameter(default=0.1)
_last_setpoint_change = None _last_setpoint_change = None
__status = IDLE, ''
# overridden in subclass frappy_psi.triton.TemperatureLoop # overridden in subclass frappy_psi.triton.TemperatureLoop
ENABLE = 'TEMP:LOOP:ENAB' ENABLE = 'TEMP:LOOP:ENAB'
ENABLE_RAMP = 'TEMP:LOOP:RENA' ENABLE_RAMP = 'TEMP:LOOP:RENA'
@ -394,7 +377,9 @@ class TemperatureLoop(TemperatureSensor, Loop, Drivable):
return active return active
def write_control_active(self, value): def write_control_active(self, value):
self.set_output(value) if value:
raise RangeError('write to target to switch control on')
self.set_output(value, 'user')
return self.change(f'DEV::{self.ENABLE}', value, off_on) return self.change(f'DEV::{self.ENABLE}', value, off_on)
@nopoll # polled by read_setpoint @nopoll # polled by read_setpoint
@ -407,43 +392,65 @@ class TemperatureLoop(TemperatureSensor, Loop, Drivable):
def read_setpoint(self): def read_setpoint(self):
setpoint = self.query('DEV::TEMP:LOOP:TSET') setpoint = self.query('DEV::TEMP:LOOP:TSET')
if self.enable_ramp: if self.enable_ramp:
if setpoint == self.setpoint: if setpoint == self.target:
self.__ramping = False
elif setpoint == self.setpoint:
# update target when working setpoint does no longer change # update target when working setpoint does no longer change
if setpoint != self.target and self._last_setpoint_change is not None: if self._last_setpoint_change is not None:
unchanged_since = time.time() - self._last_setpoint_change unchanged_since = time.time() - self._last_setpoint_change
if unchanged_since > max(12.0, 0.06 / max(1e-4, self.ramp)): if unchanged_since > max(12.0, 0.06 / max(1e-4, self.ramp)):
self.__ramping = False
self.target = self.setpoint self.target = self.setpoint
return setpoint return setpoint
self._last_setpoint_change = time.time() self._last_setpoint_change = time.time()
else: else:
self.__ramping = False
self.target = setpoint self.target = setpoint
return setpoint return setpoint
def set_target(self, target):
self.change(f'DEV::{self.ENABLE}', True, off_on)
super().set_target(target)
def deactivate_control(self, source):
if self.__ramping:
self.__ramping = False
# stop ramping setpoint
self.change('DEV::TEMP:LOOP:TSET', self.read_setpoint(), lazy=True)
super().deactivate_control(source)
def ramping_state(self, state):
self.read_setpoint()
if self.__ramping:
return Retry
return self.convergence_approach
def write_target(self, value): def write_target(self, value):
target = self.change('DEV::TEMP:LOOP:TSET', value) target = self.change('DEV::TEMP:LOOP:TSET', value, lazy=True)
if self.enable_ramp: if self.enable_ramp:
self._last_setpoint_change = None self._last_setpoint_change = None
self.__ramping = True
self.set_target(value) self.set_target(value)
self.convergence_state.status = RAMPING, 'ramping'
self.read_status()
self.convergence_state.start(self.ramping_state)
else: else:
self.set_target(target) self.set_target(target)
self.convergence_start()
self.read_status()
return self.target return self.target
def read_enable_ramp(self): def read_enable_ramp(self):
return self.query(f'DEV::{self.ENABLE_RAMP}', off_on) return self.query(f'DEV::{self.ENABLE_RAMP}', off_on)
def write_enable_ramp(self, value): def write_enable_ramp(self, value):
return self.change(f'DEV::{self.ENABLE_RAMP}', value, off_on) if self.enable_ramp < value: # ramp_enable was off: start from current value
self.change('DEV::TEMP:LOOP:TSET', self.value, lazy=True)
def set_output(self, active): result = self.change(f'DEV::{self.ENABLE_RAMP}', value, off_on)
if active: if self.isDriving() and value != self.enable_ramp:
if self.output_module and self.output_module.controlled_by != self.name: self.enable_ramp = value
self.output_module.write_controlled_by(self.name) self.write_target(self.target)
else: return result
if self.output_module and self.output_module.controlled_by != SELF:
self.output_module.write_controlled_by(SELF)
status = IDLE, 'control inactive'
if self.status != status:
self.status = status
def read_ramp(self): def read_ramp(self):
result = self.query(f'DEV::{self.RAMP_RATE}') result = self.query(f'DEV::{self.RAMP_RATE}')
@ -470,7 +477,7 @@ class PressureSensor(MercuryChannel, Readable):
return self.query('DEV::PRES:SIG:PRES') return self.query('DEV::PRES:SIG:PRES')
class ValvePos(HasInput, MercuryChannel, Drivable): class ValvePos(HasInput, Drivable):
kind = 'PRES,AUX' kind = 'PRES,AUX'
value = Parameter('value pos', FloatRange(unit='%'), readonly=False) value = Parameter('value pos', FloatRange(unit='%'), readonly=False)
target = Parameter('valve pos target', FloatRange(0, 100, unit='$'), readonly=False) target = Parameter('valve pos target', FloatRange(0, 100, unit='$'), readonly=False)
@ -491,11 +498,11 @@ class ValvePos(HasInput, MercuryChannel, Drivable):
return self.query('DEV::PRES:LOOP:FSET') return self.query('DEV::PRES:LOOP:FSET')
def write_target(self, value): def write_target(self, value):
self.write_controlled_by(SELF) self.self_controlled()
return self.change('DEV::PRES:LOOP:FSET', value) return self.change('DEV::PRES:LOOP:FSET', value)
class PressureLoop(HasInput, PressureSensor, Loop, Drivable): class PressureLoop(PressureSensor, HasControlledBy, ConvLoop):
kind = 'PRES' kind = 'PRES'
output_module = Attached(ValvePos, mandatory=False) output_module = Attached(ValvePos, mandatory=False)
tolerance = Parameter(default=0.1) tolerance = Parameter(default=0.1)
@ -506,7 +513,7 @@ class PressureLoop(HasInput, PressureSensor, Loop, Drivable):
return active return active
def write_control_active(self, value): def write_control_active(self, value):
self.set_output(value) self.set_output(value, 'user')
return self.change('DEV::PRES:LOOP:FAUT', value, off_on) return self.change('DEV::PRES:LOOP:FAUT', value, off_on)
def read_target(self): def read_target(self):
@ -521,7 +528,7 @@ class PressureLoop(HasInput, PressureSensor, Loop, Drivable):
super().set_target(target) super().set_target(target)
def write_target(self, value): def write_target(self, value):
self.write_controlled_by(SELF) self.self_controlled()
self.set_target(value) self.set_target(value)
return value return value
@ -548,14 +555,13 @@ class HasAutoFlow:
self.needle_valve.register_input(self.name, self.auto_flow_off) self.needle_valve.register_input(self.name, self.auto_flow_off)
def write_auto_flow(self, value): def write_auto_flow(self, value):
if self.needle_valve:
if value: if value:
if self.needle_valve and self.needle_valve.controlled_by != self.name: self.needle_valve.controlled_by = self.name
self.needle_valve.write_controlled_by(self.name)
else: else:
if self.needle_valve and self.needle_valve.controlled_by != SELF: if self.needle_valve.controlled_by != SELF:
self.needle_valve.write_controlled_by(SELF) self.needle_valve.controlled_by = SELF
_, (fmin, _) = self.flowpars self.needle_valve.write_target(self.flowpars[1][0]) # flow min
self.needle_valve.write_target(fmin)
return value return value
def auto_flow_off(self): def auto_flow_off(self):

View File

@ -40,9 +40,8 @@ from os.path import expanduser, join, exists
from frappy.client import ProxyClient from frappy.client import ProxyClient
from frappy.datatypes import ArrayOf, BoolType, \ from frappy.datatypes import ArrayOf, BoolType, \
EnumType, FloatRange, IntRange, StringType EnumType, FloatRange, IntRange, StringType
from frappy.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError, \ from frappy.errors import ConfigError, HardwareError, secop_error, CommunicationFailedError
CommunicationFailedError from frappy.lib import generalConfig, mkthread
from frappy.lib import generalConfig, mkthread, formatExtendedStack
from frappy.lib.asynconn import AsynConn, ConnectionClosed from frappy.lib.asynconn import AsynConn, ConnectionClosed
from frappy.modules import Attached, Command, Done, Drivable, \ from frappy.modules import Attached, Command, Done, Drivable, \
Module, Parameter, Property, Readable, Writable Module, Parameter, Property, Readable, Writable
@ -62,7 +61,7 @@ Mod(%(seaconn)r,
""" """
CFG_MODULE = """Mod(%(module)r, CFG_MODULE = """Mod(%(module)r,
'frappy_psi.sea.%(modcls)s', 'frappy_psi.sea.%(modcls)s', '',
io = %(seaconn)r, io = %(seaconn)r,
sea_object = %(module)r, sea_object = %(module)r,
) )
@ -75,18 +74,18 @@ SERVICE_NAMES = {
} }
SEA_DIR = expanduser('~/sea') SEA_DIR = expanduser('~/sea')
seaconfdir = os.environ.get('FRAPPY_SEA_DIR')
if not exists(seaconfdir):
for confdir in generalConfig.confdir.split(os.pathsep): for confdir in generalConfig.confdir.split(os.pathsep):
seaconfdir = join(confdir, 'sea') seaconfdir = join(confdir, 'sea')
if exists(seaconfdir): if exists(seaconfdir):
break break
else:
seaconfdir = os.environ.get('FRAPPY_SEA_DIR')
def get_sea_port(instance): def get_sea_port(instance):
for filename in ('sea_%s.tcl' % instance, 'sea.tcl'): for filename in ('sea_%s.tcl' % instance, 'sea.tcl'):
try: try:
with open(join(SEA_DIR, filename)) as f: with open(join(SEA_DIR, filename), encoding='utf-8') as f:
for line in f: for line in f:
linesplit = line.split() linesplit = line.split()
if len(linesplit) == 3: if len(linesplit) == 3:
@ -115,18 +114,21 @@ class SeaClient(ProxyClient, Module):
_instance = None _instance = None
def __init__(self, name, log, opts, srv): def __init__(self, name, log, opts, srv):
instance = srv.node_cfg['name'].rsplit('_', 1)[0] nodename = srv.node_cfg.get('name') or srv.node_cfg.get('equipment_id')
instance = nodename.rsplit('_', 1)[0]
if 'uri' not in opts: if 'uri' not in opts:
self._instance = instance self._instance = instance
port = get_sea_port(instance) port = get_sea_port(instance)
if port is None: if port is None:
raise ConfigError('missing sea port for %s' % instance) raise ConfigError('missing sea port for %s' % instance)
opts['uri'] = 'tcp://localhost:%s' % port opts['uri'] = {'value': 'tcp://localhost:%s' % port}
self.objects = set() self.objects = set()
self.shutdown = False self.shutdown = False
self.path2param = {} self.path2param = {}
self._write_lock = threading.Lock() self._write_lock = threading.Lock()
config = opts.get('config') config = opts.get('config')
if isinstance(config, dict):
config = config['value']
if config: if config:
self.default_json_file[name] = config.split()[0] + '.json' self.default_json_file[name] = config.split()[0] + '.json'
self.syncio = None self.syncio = None
@ -147,14 +149,17 @@ class SeaClient(ProxyClient, Module):
def _connect(self, started_callback): def _connect(self, started_callback):
if self._instance: if self._instance:
if not self._service_manager: if not self._service_manager:
from servicemanager import SeaManager if self._service_manager is None:
try:
from servicemanager import SeaManager # pylint: disable=import-outside-toplevel
self._service_manager = SeaManager() self._service_manager = SeaManager()
except ImportError:
self._service_manager = False
if self._service_manager:
self._service_manager.do_start(self._instance) self._service_manager.do_start(self._instance)
if '//' not in self.uri: if '//' not in self.uri:
self.uri = 'tcp://' + self.uri self.uri = 'tcp://' + self.uri
self.asynio = AsynConn(self.uri) self.asynio = AsynConn(self.uri)
# print('CONNECT', self.uri, self.asynio)
# print(formatExtendedStack())
reply = self.asynio.readline() reply = self.asynio.readline()
if reply != b'OK': if reply != b'OK':
raise CommunicationFailedError('reply %r should be "OK"' % reply) raise CommunicationFailedError('reply %r should be "OK"' % reply)
@ -184,13 +189,11 @@ class SeaClient(ProxyClient, Module):
pass pass
self._connect(None) self._connect(None)
self.syncio = AsynConn(self.uri) self.syncio = AsynConn(self.uri)
# print('SYNCIO', self.uri)
assert self.syncio.readline() == b'OK' assert self.syncio.readline() == b'OK'
self.syncio.writeline(b'seauser seaser') self.syncio.writeline(b'seauser seaser')
assert self.syncio.readline() == b'Login OK' assert self.syncio.readline() == b'Login OK'
print('connected to %s' % self.uri) self.log.info('connected to %s', self.uri)
self.syncio.flush_recv() self.syncio.flush_recv()
# print('> %s' % command)
ft = 'fulltransAct' if quiet else 'fulltransact' ft = 'fulltransAct' if quiet else 'fulltransact'
self.syncio.writeline(('%s %s' % (ft, command)).encode()) self.syncio.writeline(('%s %s' % (ft, command)).encode())
result = None result = None
@ -203,19 +206,18 @@ class SeaClient(ProxyClient, Module):
except ConnectionClosed: except ConnectionClosed:
break break
reply = reply.decode() reply = reply.decode()
# print('< %s' % reply)
if reply.startswith('TRANSACTIONSTART'): if reply.startswith('TRANSACTIONSTART'):
result = [] result = []
continue continue
if reply == 'TRANSACTIONFINISHED': if reply == 'TRANSACTIONFINISHED':
if result is None: if result is None:
print('missing TRANSACTIONSTART on: %s' % command) self.log.info('missing TRANSACTIONSTART on: %s', command)
return '' return ''
if not result: if not result:
return '' return ''
return '\n'.join(result) return '\n'.join(result)
if result is None: if result is None:
print('swallow: %s' % reply) self.log.info('swallow: %s', reply)
continue continue
if not result: if not result:
result = [reply.split('=', 1)[-1]] result = [reply.split('=', 1)[-1]]
@ -234,7 +236,7 @@ class SeaClient(ProxyClient, Module):
try: try:
msg = json.loads(reply) msg = json.loads(reply)
except Exception as e: except Exception as e:
print(repr(e), reply) self.log.warn('bad reply %r %r', e, reply)
continue continue
if isinstance(msg, str): if isinstance(msg, str):
if msg.startswith('_E '): if msg.startswith('_E '):
@ -259,12 +261,12 @@ class SeaClient(ProxyClient, Module):
continue continue
if flag != 'hdbevent': if flag != 'hdbevent':
if obj not in ('frappy_async_client', 'get_all_param'): if obj not in ('frappy_async_client', 'get_all_param'):
print('SKIP', msg) self.log.debug('skip %r', msg)
continue continue
if not data: if not data:
continue continue
if not isinstance(data, dict): if not isinstance(data, dict):
print('what means %r' % msg) self.log.debug('what means %r', msg)
continue continue
now = time.time() now = time.time()
for path, value in data.items(): for path, value in data.items():
@ -318,7 +320,7 @@ class SeaClient(ProxyClient, Module):
class SeaConfigCreator(SeaClient): class SeaConfigCreator(SeaClient):
def startModule(self, started_callback): def startModule(self, start_events):
"""save objects (and sub-objects) description and exit""" """save objects (and sub-objects) description and exit"""
self._connect(None) self._connect(None)
reply = self.request('describe_all') reply = self.request('describe_all')
@ -343,20 +345,20 @@ class SeaConfigCreator(SeaClient):
service = SERVICE_NAMES[ext] service = SERVICE_NAMES[ext]
seaconn = 'sea_' + service seaconn = 'sea_' + service
cfgfile = join(seaconfdir, stripped + '_cfg.py') cfgfile = join(seaconfdir, stripped + '_cfg.py')
with open(cfgfile, 'w') as fp: with open(cfgfile, 'w', encoding='utf-8') as fp:
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service, fp.write(CFG_HEADER % {'config': filename, 'seaconn': seaconn, 'service': service,
nodedescr=description.get(filename, filename))) 'nodedescr': description.get(filename, filename)})
for obj in descr: for obj in descr:
fp.write(CFG_MODULE % dict(modcls=modcls[obj], module=obj, seaconn=seaconn)) fp.write(CFG_MODULE % {'modcls': modcls[obj], 'module': obj, 'seaconn': seaconn})
content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n') content = json.dumps(descr).replace('}, {', '},\n{').replace('[{', '[\n{').replace('}]}, ', '}]},\n\n')
result.append('%s\n' % cfgfile) result.append('%s\n' % cfgfile)
with open(join(seaconfdir, filename + '.json'), 'w') as fp: with open(join(seaconfdir, filename + '.json'), 'w', encoding='utf-8') as fp:
fp.write(content + '\n') fp.write(content + '\n')
result.append('%s: %s' % (filename, ','.join(n for n in descr))) result.append('%s: %s' % (filename, ','.join(n for n in descr)))
raise SystemExit('; '.join(result)) raise SystemExit('; '.join(result))
@Command(StringType(), result=StringType()) @Command(StringType(), result=StringType())
def query(self, cmd): def query(self, cmd, quiet=False):
"""a request checking for errors and accepting 0 or 1 line as result""" """a request checking for errors and accepting 0 or 1 line as result"""
errors = [] errors = []
reply = None reply = None
@ -400,12 +402,18 @@ class SeaModule(Module):
sea_object = None sea_object = None
hdbpath = None # hdbpath for main writable hdbpath = None # hdbpath for main writable
# pylint: disable=too-many-statements,arguments-differ,too-many-branches
def __new__(cls, name, logger, cfgdict, srv): def __new__(cls, name, logger, cfgdict, srv):
if hasattr(srv, 'extra_sea_modules'): if hasattr(srv, 'extra_sea_modules'):
extra_modules = srv.extra_sea_modules extra_modules = srv.extra_sea_modules
else: else:
extra_modules = {} extra_modules = {}
srv.extra_sea_modules = extra_modules srv.extra_sea_modules = extra_modules
for k, v in cfgdict.items():
try:
cfgdict[k] = v['value']
except (KeyError, TypeError):
pass
json_file = cfgdict.pop('json_file', None) or SeaClient.default_json_file[cfgdict['io']] json_file = cfgdict.pop('json_file', None) or SeaClient.default_json_file[cfgdict['io']]
visibility_level = cfgdict.pop('visibility_level', 2) visibility_level = cfgdict.pop('visibility_level', 2)
@ -416,25 +424,22 @@ class SeaModule(Module):
paramdesc['key'] = 'value' paramdesc['key'] = 'value'
if issubclass(cls, SeaWritable): if issubclass(cls, SeaWritable):
if paramdesc.get('readonly', True): if paramdesc.get('readonly', True):
raise ConfigError('%s/%s is not writable' % (sea_object, paramdesc['path'])) raise ConfigError(f"{sea_object}/{paramdesc['path']} is not writable")
params.insert(0, paramdesc.copy()) # copy value params.insert(0, paramdesc.copy()) # copy value
paramdesc['key'] = 'target' paramdesc['key'] = 'target'
paramdesc['readonly'] = False paramdesc['readonly'] = False
extra_module_set = () extra_module_set = ()
if 'description' not in cfgdict: if 'description' not in cfgdict:
cfgdict['description'] = '%s@%s' % (single_module, json_file) cfgdict['description'] = f'{single_module}@{json_file}'
else: else:
sea_object = cfgdict.pop('sea_object') sea_object = cfgdict.pop('sea_object')
rel_paths = cfgdict.pop('rel_paths', '.') rel_paths = cfgdict.pop('rel_paths', '.')
if 'description' not in cfgdict: if 'description' not in cfgdict:
cfgdict['description'] = '%s@%s%s' % ( cfgdict['description'] = '%s@%s%s' % (
name, json_file, '' if rel_paths == '.' else ' (rel_paths=%s)' % rel_paths) name, json_file, '' if rel_paths == '.' else f' (rel_paths={rel_paths})')
# with open(join(seaconfdir, json_file + '.json')) as fp: with open(join(seaconfdir, json_file), encoding='utf-8') as fp:
# sea_object, descr = json.load(fp)
with open(join(seaconfdir, json_file)) as fp:
content = json.load(fp) content = json.load(fp)
# print(json_file, content.keys())
descr = content[sea_object] descr = content[sea_object]
if rel_paths == '*' or not rel_paths: if rel_paths == '*' or not rel_paths:
# take all # take all
@ -478,7 +483,6 @@ class SeaModule(Module):
result.pop(0) result.pop(0)
else: else:
logger.error('%s: no value found', name) logger.error('%s: no value found', name)
# logger.info('PARAMS %s %r', name, result)
base = descr['base'] base = descr['base']
params = descr['params'] params = descr['params']
if issubclass(cls, SeaWritable): if issubclass(cls, SeaWritable):
@ -486,14 +490,14 @@ class SeaModule(Module):
assert paramdesc['key'] == 'value' assert paramdesc['key'] == 'value'
params.append(paramdesc.copy()) # copy value? params.append(paramdesc.copy()) # copy value?
if paramdesc.get('readonly', True): if paramdesc.get('readonly', True):
raise ConfigError('%s/%s is not writable' % (sea_object, paramdesc['path'])) raise ConfigError(f"{sea_object}/{paramdesc['path']} is not writable")
paramdesc['key'] = 'target' paramdesc['key'] = 'target'
paramdesc['readonly'] = False paramdesc['readonly'] = False
extra_module_set = cfgdict.pop('extra_modules', ()) extra_module_set = cfgdict.pop('extra_modules', ())
if extra_module_set: if extra_module_set:
extra_module_set = set(extra_module_set.replace(',', ' ').split()) extra_module_set = set(extra_module_set.replace(',', ' ').split())
path2param = {} path2param = {}
attributes = dict(sea_object=sea_object, path2param=path2param) attributes = {'sea_object': sea_object, 'path2param': path2param}
# some guesses about visibility (may be overriden in *_cfg.py): # some guesses about visibility (may be overriden in *_cfg.py):
if sea_object in ('table', 'cc'): if sea_object in ('table', 'cc'):
@ -504,12 +508,10 @@ class SeaModule(Module):
path = paramdesc['path'] path = paramdesc['path']
readonly = paramdesc.get('readonly', True) readonly = paramdesc.get('readonly', True)
dt = get_datatype(paramdesc) dt = get_datatype(paramdesc)
#print('----', sea_object) kwds = {'description': paramdesc.get('description', path),
#print(dt, paramdesc) 'datatype': dt,
kwds = dict(description=paramdesc.get('description', path), 'visibility': paramdesc.get('visibility', 1),
datatype=dt, 'needscfg': False, 'readonly': readonly}
visibility=paramdesc.get('visibility', 1),
needscfg=False, readonly=readonly)
if kwds['datatype'] is None: if kwds['datatype'] is None:
kwds.update(visibility=3, default='', datatype=StringType()) kwds.update(visibility=3, default='', datatype=StringType())
pathlist = path.split('/') if path else [] pathlist = path.split('/') if path else []
@ -555,10 +557,8 @@ class SeaModule(Module):
continue # skip this parameter continue # skip this parameter
path2param.setdefault(hdbpath, []).append((name, key)) path2param.setdefault(hdbpath, []).append((name, key))
attributes[key] = pobj attributes[key] = pobj
# if hasattr(cls, 'read_' + key):
# print('override %s.read_%s' % (cls.__name__, key))
def rfunc(self, cmd='hval %s/%s' % (base, path)): def rfunc(self, cmd=f'hval {base}/{path}'):
reply = self.io.query(cmd, True) reply = self.io.query(cmd, True)
try: try:
reply = float(reply) reply = float(reply)
@ -571,15 +571,13 @@ class SeaModule(Module):
attributes['read_' + key] = rfunc attributes['read_' + key] = rfunc
if not readonly: if not readonly:
# if hasattr(cls, 'write_' + key):
# print('override %s.write_%s' % (cls.__name__, key))
def wfunc(self, value, datatype=datatype, command=paramdesc['cmd']): def wfunc(self, value, datatype=datatype, command=paramdesc['cmd']):
value = datatype.export_value(value) value = datatype.export_value(value)
if isinstance(value, bool): if isinstance(value, bool):
value = int(value) value = int(value)
# TODO: check if more has to be done for valid tcl data (strings?) # TODO: check if more has to be done for valid tcl data (strings?)
cmd = "%s %s" % (command, value) cmd = f'{command} {value}'
self.io.query(cmd) self.io.query(cmd)
return Done return Done
@ -596,7 +594,7 @@ class SeaModule(Module):
attributes[pname] = pobj attributes[pname] = pobj
pobj.__set_name__(cls, pname) pobj.__set_name__(cls, pname)
classname = '%s_%s' % (cls.__name__, name) classname = f'{cls.__name__}_{name}'
newcls = type(classname, (cls,), attributes) newcls = type(classname, (cls,), attributes)
result = Module.__new__(newcls) result = Module.__new__(newcls)
return result return result
@ -609,11 +607,9 @@ class SeaModule(Module):
try: try:
pobj = self.parameters[parameter] pobj = self.parameters[parameter]
except KeyError: except KeyError:
print(self.name, parameter) self.log.error('do not know %s:%s', self.name, parameter)
raise raise
pobj.timestamp = timestamp pobj.timestamp = timestamp
#if not pobj.readonly and pobj.value != value:
# print('UPDATE', module, parameter, value)
# should be done here: deal with clock differences # should be done here: deal with clock differences
if not readerror: if not readerror:
try: try:
@ -668,7 +664,7 @@ class SeaDrivable(SeaModule, Drivable):
# return self.target # return self.target
def write_target(self, value): def write_target(self, value):
self.io.query('run %s %s' % (self.sea_object, value)) self.io.query(f'run {self.sea_object} {value}')
# self.status = [self.Status.BUSY, 'driving'] # self.status = [self.Status.BUSY, 'driving']
return value return value
@ -701,4 +697,4 @@ class SeaDrivable(SeaModule, Drivable):
- on stdsct drivables this will call the halt script - on stdsct drivables this will call the halt script
- on EaseDriv this will set the stopped state - on EaseDriv this will set the stopped state
""" """
self.io.query('%s is_running 0' % self.sea_object) self.io.query(f'{self.sea_object} is_running 0')

View File

@ -20,14 +20,13 @@
# ***************************************************************************** # *****************************************************************************
"""oxford instruments triton (kelvinoxjt dil)""" """oxford instruments triton (kelvinoxjt dil)"""
from math import sqrt, log10 from math import sqrt
from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, \ from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, \
Done, Property Done, Property
from frappy.datatypes import EnumType, FloatRange, StringType from frappy.datatypes import EnumType, FloatRange, StringType
from frappy.lib.enum import Enum from frappy.lib.enum import Enum
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF
from frappy.lib import clamp from frappy_psi import mercury
import frappy_psi.mercury as mercury
actions = Enum(none=0, condense=1, circulate=2, collect=3) actions = Enum(none=0, condense=1, circulate=2, collect=3)
open_close = Mapped(CLOSE=0, OPEN=1) open_close = Mapped(CLOSE=0, OPEN=1)
@ -36,7 +35,7 @@ actions_map.mapping['NONE'] = actions.none # when writing, STOP is used instead
class Action(MercuryChannel, Writable): class Action(MercuryChannel, Writable):
channel_type = 'ACTN' kind = 'ACTN'
cooldown_channel = Property('cool down channel', StringType(), 'T5') cooldown_channel = Property('cool down channel', StringType(), 'T5')
mix_channel = Property('mix channel', StringType(), 'T5') mix_channel = Property('mix channel', StringType(), 'T5')
value = Parameter('running action', EnumType(actions)) value = Parameter('running action', EnumType(actions))
@ -72,7 +71,7 @@ class Action(MercuryChannel, Writable):
class Valve(MercuryChannel, Drivable): class Valve(MercuryChannel, Drivable):
channel_type = 'VALV' kind = 'VALV'
value = Parameter('valve state', EnumType(closed=0, opened=1)) value = Parameter('valve state', EnumType(closed=0, opened=1))
target = Parameter('valve target', EnumType(close=0, open=1)) target = Parameter('valve target', EnumType(close=0, open=1))
@ -82,7 +81,7 @@ class Valve(MercuryChannel, Drivable):
self.read_status() self.read_status()
def read_value(self): def read_value(self):
return self.query('VALV:SIG:STATE', open_close) return self.query('DEV::VALV:SIG:STATE', open_close)
def read_status(self): def read_status(self):
pos = self.read_value() pos = self.read_value()
@ -92,41 +91,41 @@ class Valve(MercuryChannel, Drivable):
# success # success
if self._try_count: if self._try_count:
# make sure last sent command was not opposite # make sure last sent command was not opposite
self.change('VALV:SIG:STATE', self.target, open_close) self.change('DEV::VALV:SIG:STATE', self.target, open_close)
self._try_count = None self._try_count = None
self.setFastPoll(False) self.setFastPoll(False)
return IDLE, '' return IDLE, ''
self._try_count += 1 self._try_count += 1
if self._try_count % 4 == 0: if self._try_count % 4 == 0:
# send to opposite position in order to unblock # send to opposite position in order to unblock
self.change('VALV:SIG:STATE', pos, open_close) self.change('DEV::VALV:SIG:STATE', pos, open_close)
return BUSY, 'unblock' return BUSY, 'unblock'
if self._try_count > 9: if self._try_count > 9:
# make sure system does not toggle later # make sure system does not toggle later
self.change('VALV:SIG:STATE', pos, open_close) self.change('DEV::VALV:SIG:STATE', pos, open_close)
return ERROR, 'can not %s valve' % self.target.name return ERROR, 'can not %s valve' % self.target.name
self.change('VALV:SIG:STATE', self.target, open_close) self.change('DEV::VALV:SIG:STATE', self.target, open_close)
return BUSY, 'waiting' return BUSY, 'waiting'
def write_target(self, value): def write_target(self, value):
if value != self.read_value(): if value != self.read_value():
self._try_count = 0 self._try_count = 0
self.setFastPoll(True, 0.25) self.setFastPoll(True, 0.25)
self.change('VALV:SIG:STATE', value, open_close) self.change('DEV::VALV:SIG:STATE', value, open_close)
self.status = BUSY, self.target.name self.status = BUSY, self.target.name
return value return value
class Pump(MercuryChannel, Writable): class Pump(MercuryChannel, Writable):
channel_type = 'PUMP' kind = 'PUMP'
value = Parameter('pump state', EnumType(off=0, on=1)) value = Parameter('pump state', EnumType(off=0, on=1))
target = Parameter('pump target', EnumType(off=0, on=1)) target = Parameter('pump target', EnumType(off=0, on=1))
def read_value(self): def read_value(self):
return self.query('PUMP:SIG:STATE', off_on) return self.query('DEV::PUMP:SIG:STATE', off_on)
def write_target(self, value): def write_target(self, value):
return self.change('PUMP:SIG:STATE', value, off_on) return self.change('DEV::PUMP:SIG:STATE', value, off_on)
def read_status(self): def read_status(self):
return IDLE, '' return IDLE, ''
@ -142,35 +141,35 @@ class TurboPump(Pump):
electronics_temp = Parameter('temperature of electronics', FloatRange(unit='K')) electronics_temp = Parameter('temperature of electronics', FloatRange(unit='K'))
def read_status(self): def read_status(self):
status = self.query('PUMP:STATUS', str) status = self.query('DEV::PUMP:STATUS', str)
if status == 'OK': if status == 'OK':
return IDLE, '' return IDLE, ''
return WARN, status return WARN, status
def read_power(self): def read_power(self):
return self.query('PUMP:SIG:POWR') return self.query('DEV::PUMP:SIG:POWR')
def read_freq(self): def read_freq(self):
return self.query('PUMP:SIG:SPD') return self.query('DEV::PUMP:SIG:SPD')
def read_powerstage_temp(self): def read_powerstage_temp(self):
return self.query('PUMP:SIG:PST') return self.query('DEV::PUMP:SIG:PST')
def read_motor_temp(self): def read_motor_temp(self):
return self.query('PUMP:SIG:MT') return self.query('DEV::PUMP:SIG:MT')
def read_bearing_temp(self): def read_bearing_temp(self):
return self.query('PUMP:SIG:BT') return self.query('DEV::PUMP:SIG:BT')
def read_pumpbase_temp(self): def read_pumpbase_temp(self):
return self.query('PUMP:SIG:PBT') return self.query('DEV::PUMP:SIG:PBT')
def read_electronics_temp(self): def read_electronics_temp(self):
return self.query('PUMP:SIG:ET') return self.query('DEV::PUMP:SIG:ET')
# class PulseTubeCompressor(MercuryChannel, Writable): # class PulseTubeCompressor(MercuryChannel, Writable):
# channel_type = 'PTC' # kind = 'PTC'
# value = Parameter('compressor state', EnumType(closed=0, opened=1)) # value = Parameter('compressor state', EnumType(closed=0, opened=1))
# target = Parameter('compressor target', EnumType(close=0, open=1)) # target = Parameter('compressor target', EnumType(close=0, open=1))
# water_in_temp = Parameter('temperature of water inlet', FloatRange(unit='K')) # water_in_temp = Parameter('temperature of water inlet', FloatRange(unit='K'))
@ -181,42 +180,42 @@ class TurboPump(Pump):
# motor_current = Parameter('motor current', FloatRange(unit='A')) # motor_current = Parameter('motor current', FloatRange(unit='A'))
# #
# def read_value(self): # def read_value(self):
# return self.query('PTC:SIG:STATE', off_on) # return self.query('DEV::PTC:SIG:STATE', off_on)
# #
# def write_target(self, value): # def write_target(self, value):
# return self.change('PTC:SIG:STATE', value, off_on) # return self.change('DEV::PTC:SIG:STATE', value, off_on)
# #
# def read_status(self): # def read_status(self):
# # TODO: check possible status values # # TODO: check possible status values
# return self.WARN, self.query('PTC:SIG:STATUS') # return self.WARN, self.query('DEV::PTC:SIG:STATUS')
# #
# def read_water_in_temp(self): # def read_water_in_temp(self):
# return self.query('PTC:SIG:WIT') # return self.query('DEV::PTC:SIG:WIT')
# #
# def read_water_out_temp(self): # def read_water_out_temp(self):
# return self.query('PTC:SIG:WOT') # return self.query('DEV::PTC:SIG:WOT')
# #
# def read_helium_temp(self): # def read_helium_temp(self):
# return self.query('PTC:SIG:HT') # return self.query('DEV::PTC:SIG:HT')
# #
# def read_helium_low_pressure(self): # def read_helium_low_pressure(self):
# return self.query('PTC:SIG:HLP') # return self.query('DEV::PTC:SIG:HLP')
# #
# def read_helium_high_pressure(self): # def read_helium_high_pressure(self):
# return self.query('PTC:SIG:HHP') # return self.query('DEV::PTC:SIG:HHP')
# #
# def read_motor_current(self): # def read_motor_current(self):
# return self.query('PTC:SIG:MCUR') # return self.query('DEV::PTC:SIG:MCUR')
class FlowMeter(MercuryChannel, Readable): class FlowMeter(MercuryChannel, Readable):
channel_type = 'FLOW' kind = 'FLOW'
def read_value(self): def read_value(self):
return self.query('FLOW:SIG:FLOW') return self.query('DEV::FLOW:SIG:FLOW')
class ScannerChannel: class ScannerChannel(MercuryChannel):
# TODO: excitation, enable # TODO: excitation, enable
# TODO: switch on/off filter, check # TODO: switch on/off filter, check
filter_time = Parameter('filter time', FloatRange(1, 200, unit='sec'), readonly=False) filter_time = Parameter('filter time', FloatRange(1, 200, unit='sec'), readonly=False)
@ -224,24 +223,24 @@ class ScannerChannel:
pause_time = Parameter('pause time', FloatRange(3, 200, unit='sec'), readonly=False) pause_time = Parameter('pause time', FloatRange(3, 200, unit='sec'), readonly=False)
def read_filter_time(self): def read_filter_time(self):
return self.query('TEMP:FILT:TIME') return self.query('DEV::TEMP:FILT:TIME')
def write_filter_time(self, value): def write_filter_time(self, value):
self.change('TEMP:FILT:WIN', 80) self.change('DEV::TEMP:FILT:WIN', 80)
return self.change('TEMP:FILT:TIME', value) return self.change('DEV::TEMP:FILT:TIME', value)
def read_dwell_time(self): def read_dwell_time(self):
return self.query('TEMP:MEAS:DWEL') return self.query('DEV::TEMP:MEAS:DWEL')
def write_dwell_time(self, value): def write_dwell_time(self, value):
self.change('TEMP:FILT:WIN', 80) self.change('DEV::TEMP:FILT:WIN', 80)
return self.change('TEMP:MEAS:DWEL', value) return self.change('DEV::TEMP:MEAS:DWEL', value)
def read_pause_time(self): def read_pause_time(self):
return self.query('TEMP:MEAS:PAUS') return self.query('DEV::TEMP:MEAS:PAUS')
def write_pause_time(self, value): def write_pause_time(self, value):
return self.change('TEMP:MEAS:PAUS', value) return self.change('DEV::TEMP:MEAS:PAUS', value)
class TemperatureSensor(ScannerChannel, mercury.TemperatureSensor): class TemperatureSensor(ScannerChannel, mercury.TemperatureSensor):
@ -261,7 +260,7 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
if self.system_channel: if self.system_channel:
self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str) self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str)
if value: if value:
self.change('TEMP:LOOP:FILT:ENAB', 'ON', str) self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str)
if self.output_module: if self.output_module:
limit = self.output_module.read_limit() or None # None: max. limit limit = self.output_module.read_limit() or None # None: max. limit
self.output_module.write_limit(limit) self.output_module.write_limit(limit)
@ -270,16 +269,16 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
class HeaterOutput(HasInput, MercuryChannel, Writable): class HeaterOutput(HasInput, MercuryChannel, Writable):
"""heater output""" """heater output"""
channel_type = 'HTR' kind = 'HTR'
value = Parameter('heater output', FloatRange(unit='uW')) value = Parameter('heater output', FloatRange(unit='uW'))
target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False) target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False)
resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm')) resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm'))
def read_resistivity(self): def read_resistivity(self):
return self.query('HTR:RES') return self.query('DEV::HTR:RES')
def read_value(self): def read_value(self):
return round(self.query('HTR:SIG:POWR'), 3) return round(self.query('DEV::HTR:SIG:POWR'), 3)
def read_target(self): def read_target(self):
if self.controlled_by != 0: if self.controlled_by != 0:
@ -291,17 +290,17 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
if self.resistivity: if self.resistivity:
# round to the next voltage step # round to the next voltage step
value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity
return round(self.change('HTR:SIG:POWR', value), 3) return round(self.change('DEV::HTR:SIG:POWR', value), 3)
class HeaterOutputWithRange(HeaterOutput): class HeaterOutputWithRange(HeaterOutput):
"""heater output with heater range""" """heater output with heater range"""
channel_type = 'HTR,TEMP' kind = 'HTR,TEMP'
limit = Parameter('max. heater power', FloatRange(unit='uW'), readonly=False) limit = Parameter('max. heater power', FloatRange(unit='uW'), readonly=False)
def read_limit(self): def read_limit(self):
maxcur = self.query('TEMP:LOOP:RANGE') # mA maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA
return self.read_resistivity() * maxcur ** 2 # uW return self.read_resistivity() * maxcur ** 2 # uW
def write_limit(self, value): def write_limit(self, value):
@ -315,6 +314,5 @@ class HeaterOutputWithRange(HeaterOutput):
break break
else: else:
maxcur = cur maxcur = cur
self.change('TEMP:LOOP:RANGE', maxcur) self.change('DEV::TEMP:LOOP:RANGE', maxcur)
return self.read_limit() return self.read_limit()