mercury, ips, sea, triton, convergence
after gerrit Change-Id: Iff14047ecc476589aef10c96fae9970133b8bd14
This commit is contained in:
parent
750b5a7794
commit
8039351395
@ -250,16 +250,9 @@ class ProxyClient:
|
||||
return bool(cblist)
|
||||
|
||||
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
|
||||
self.callback(None, 'updateEvent', module, param, *entry)
|
||||
self.callback(module, 'updateEvent', module, param, *entry)
|
||||
self.callback((module, param), 'updateEvent', module, param, *entry)
|
||||
self.callback(None, 'updateEvent', module, param, value, timestamp, readerror)
|
||||
self.callback(module, 'updateEvent', module, param, value, timestamp, readerror)
|
||||
self.callback((module, param), 'updateEvent', module, param,value, timestamp, readerror)
|
||||
|
||||
|
||||
class SecopClient(ProxyClient):
|
||||
@ -651,6 +644,16 @@ class SecopClient(ProxyClient):
|
||||
data = datatype.import_value(data)
|
||||
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
|
||||
|
||||
PREDEFINED_NAMES = set(frappy.params.PREDEFINED_ACCESSIBLES)
|
||||
|
@ -126,13 +126,15 @@ class Config(dict):
|
||||
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()
|
||||
mods = Collector(Mod)
|
||||
ns = {'Node': node.add, 'Mod': mods.add, 'Param': Param, 'Command': Param, 'Group': Group}
|
||||
|
||||
# pylint: disable=exec-used
|
||||
exec(config_text, ns)
|
||||
exec(compile(config_text, filename, 'exec'), ns)
|
||||
return Config(node, mods)
|
||||
|
||||
|
||||
@ -175,9 +177,7 @@ def load_config(cfgfiles, log):
|
||||
for cfgfile in cfgfiles.split(','):
|
||||
filename = to_config_path(cfgfile, log)
|
||||
log.debug('Parsing config file %s...', filename)
|
||||
with open(filename, 'rb') as f:
|
||||
config_text = f.read()
|
||||
cfg = process_file(config_text)
|
||||
cfg = process_file(filename)
|
||||
if config:
|
||||
config.merge_modules(cfg)
|
||||
else:
|
||||
|
@ -274,9 +274,8 @@ class FloatRange(HasUnit, DataType):
|
||||
def compatible(self, other):
|
||||
if not isinstance(other, (FloatRange, ScaledInteger)):
|
||||
raise WrongTypeError('incompatible datatypes')
|
||||
# avoid infinity
|
||||
other.validate(max(sys.float_info.min, self.min))
|
||||
other.validate(min(sys.float_info.max, self.max))
|
||||
other.validate(self.min)
|
||||
other.validate(self.max)
|
||||
|
||||
|
||||
class IntRange(DataType):
|
||||
|
@ -396,3 +396,13 @@ class UniqueObject:
|
||||
|
||||
def __repr__(self):
|
||||
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]])
|
||||
|
@ -21,16 +21,17 @@
|
||||
# *****************************************************************************
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
"""
|
||||
controlled_by = Parameter('source of target value', EnumType(members={'self': 0}), default=0)
|
||||
target = Parameter() # make sure target is a parameter
|
||||
inputCallbacks = ()
|
||||
|
||||
def register_input(self, name, deactivate_control):
|
||||
@ -57,7 +58,7 @@ class HasControlledBy(Writable):
|
||||
deactivate_control(self.name)
|
||||
|
||||
|
||||
class HasOutputModule(Writable):
|
||||
class HasOutputModule:
|
||||
"""mixin for modules having an output module
|
||||
|
||||
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
|
||||
output_module = Attached(HasControlledBy, mandatory=False)
|
||||
control_active = Parameter('control mode', BoolType(), default=False)
|
||||
target = Parameter() # make sure target is a parameter
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
@ -85,8 +87,8 @@ class HasOutputModule(Writable):
|
||||
out.controlled_by = self.name
|
||||
self.control_active = True
|
||||
|
||||
def deactivate_control(self, switched_by):
|
||||
def deactivate_control(self, source):
|
||||
"""called when an other module takes over control"""
|
||||
if self.control_active:
|
||||
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}')
|
||||
|
@ -245,7 +245,6 @@ class Feature(HasAccessibles):
|
||||
a mixin with Feature as a direct base class is recognized as a SECoP feature
|
||||
and reported in the module property 'features'
|
||||
"""
|
||||
featureName = None
|
||||
|
||||
|
||||
class PollInfo:
|
||||
@ -377,10 +376,10 @@ class Module(HasAccessibles):
|
||||
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('frappy.modules')]
|
||||
# list of only the 'highest' secop module class
|
||||
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
|
||||
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
|
||||
# 1) make local copies of parameter objects
|
||||
@ -898,6 +897,7 @@ class Communicator(HasComlog, Module):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
SECoP_BASE_CLASSES = {Readable, Writable, Drivable, Communicator}
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached module
|
||||
|
@ -21,11 +21,11 @@
|
||||
# *****************************************************************************
|
||||
|
||||
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 import merge_status
|
||||
|
||||
|
||||
class HasConvergence(HasStates):
|
||||
class HasConvergence:
|
||||
"""mixin for convergence checks
|
||||
|
||||
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
|
||||
that the spent time inside tolerance stored already is not altered when
|
||||
changing tolerance.
|
||||
|
||||
does not inherit from HasStates (own state machine!)
|
||||
"""
|
||||
tolerance = Parameter('absolute tolerance', FloatRange(0, unit='$'), readonly=False, default=0)
|
||||
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:
|
||||
then the timeout event happens after this time + <settling_time> + <timeout>.
|
||||
''', FloatRange(0, unit='sec'), readonly=False, default=3600)
|
||||
status = Parameter('status determined from convergence checks', default=(IDLE, ''))
|
||||
convergence_state = None
|
||||
status = Parameter() # make sure status is a parameter
|
||||
convergence_state = None # the state machine
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self.convergence_state = StateMachine(threaded=False, logger=self.log,
|
||||
cleanup=self.cleanup, spent_inside=0)
|
||||
self.convergence_state = StateMachine(
|
||||
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)
|
||||
if state.stopped:
|
||||
if state.stopped is Stop: # and not Restart
|
||||
self.status = WARN, 'stopped'
|
||||
self.__set_status(WARN, 'stopped')
|
||||
else:
|
||||
self.status = WARN, repr(state.last_error)
|
||||
self.__set_status(WARN, repr(state.last_error))
|
||||
|
||||
def doPoll(self):
|
||||
super().doPoll()
|
||||
state = self.convergence_state
|
||||
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)
|
||||
if slope or not self.timeout:
|
||||
return slope
|
||||
return dif / self.timeout # assume exponential decay of dif, with time constant <tolerance>
|
||||
|
||||
def get_dif_tol(self):
|
||||
value = self.read_value()
|
||||
def convergence_dif(self):
|
||||
"""get difference target - value and tolerance"""
|
||||
tol = self.tolerance
|
||||
if not tol:
|
||||
tol = 0.01 * max(abs(self.target), abs(value))
|
||||
dif = abs(self.target - value)
|
||||
tol = 0.01 * max(abs(self.target), abs(self.value))
|
||||
dif = abs(self.target - self.value)
|
||||
return dif, tol
|
||||
|
||||
def start_state(self):
|
||||
def convergence_start(self):
|
||||
"""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)"""
|
||||
state.spent_inside = 0
|
||||
dif, tol = self.get_dif_tol()
|
||||
dif, tol = self.convergence_dif()
|
||||
if dif < tol:
|
||||
state.timeout_base = state.now
|
||||
return self.state_inside
|
||||
return self.convergence_inside
|
||||
if not self.timeout:
|
||||
return Retry
|
||||
if state.init:
|
||||
state.timeout_base = state.now
|
||||
state.dif_crit = dif # criterium for resetting timeout base
|
||||
self.status = BUSY, 'approaching'
|
||||
state.dif_crit -= self.get_min_slope(dif) * state.delta()
|
||||
self.__set_status(BUSY, 'approaching')
|
||||
state.dif_crit -= self.convergence_min_slope(dif) * state.delta()
|
||||
if dif < state.dif_crit: # progress is good: reset timeout base
|
||||
state.timeout_base = state.now
|
||||
elif state.now > state.timeout_base + self.timeout:
|
||||
self.status = WARN, 'convergence timeout'
|
||||
return self.state_instable
|
||||
self.__set_status(WARN, 'convergence timeout')
|
||||
return self.convergence_instable
|
||||
return Retry
|
||||
|
||||
def state_inside(self, state):
|
||||
def convergence_inside(self, state):
|
||||
"""inside tolerance, still busy"""
|
||||
dif, tol = self.get_dif_tol()
|
||||
dif, tol = self.convergence_dif()
|
||||
if dif > tol:
|
||||
return self.state_outside
|
||||
return self.convergence_outside
|
||||
state.spent_inside += state.delta()
|
||||
if state.spent_inside > self.settling_time:
|
||||
self.status = IDLE, 'reached target'
|
||||
return self.state_stable
|
||||
self.__set_status(IDLE, 'reached target')
|
||||
return self.convergence_stable
|
||||
if state.init:
|
||||
self.status = BUSY, 'inside tolerance'
|
||||
self.__set_status(BUSY, 'inside tolerance')
|
||||
return Retry
|
||||
|
||||
def state_outside(self, state):
|
||||
def convergence_outside(self, state):
|
||||
"""temporarely outside tolerance, busy"""
|
||||
dif, tol = self.get_dif_tol()
|
||||
dif, tol = self.convergence_dif()
|
||||
if dif < tol:
|
||||
return self.state_inside
|
||||
return self.convergence_inside
|
||||
if state.now > state.timeout_base + self.settling_time + self.timeout:
|
||||
self.status = WARN, 'settling timeout'
|
||||
return self.state_instable
|
||||
self.__set_status(WARN, 'settling timeout')
|
||||
return self.convergence_instable
|
||||
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
|
||||
state.spent_inside = max(0.0, state.spent_inside - state.delta())
|
||||
return Retry
|
||||
|
||||
def state_stable(self, state):
|
||||
def convergence_stable(self, state):
|
||||
"""stable, after settling_time spent within tolerance, idle"""
|
||||
dif, tol = self.get_dif_tol()
|
||||
dif, tol = self.convergence_dif()
|
||||
if dif <= tol:
|
||||
return Retry
|
||||
self.status = WARN, 'instable'
|
||||
self.__set_status(WARN, 'instable')
|
||||
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"""
|
||||
dif, tol = self.get_dif_tol()
|
||||
dif, tol = self.convergence_dif()
|
||||
if dif <= tol:
|
||||
state.spent_inside += state.delta()
|
||||
if state.spent_inside > self.settling_time:
|
||||
self.status = IDLE, 'stable' # = recovered from instable
|
||||
return self.state_stable
|
||||
self.__set_status(IDLE, 'stable') # = recovered from instable
|
||||
return self.convergence_stable
|
||||
else:
|
||||
state.spent_inside = max(0, state.spent_inside - state.delta())
|
||||
return Retry
|
||||
|
||||
def state_interrupt(self, state):
|
||||
def convergence_interrupt(self, state):
|
||||
"""stopping"""
|
||||
self.status = IDLE, 'stopped' # stop called
|
||||
return self.state_instable
|
||||
self.__set_status(state.stop_status) # stop called
|
||||
return self.convergence_instable
|
||||
|
||||
def stop(self):
|
||||
"""set to idle when busy
|
||||
@ -170,4 +185,14 @@ class HasConvergence(HasStates):
|
||||
does not stop control!
|
||||
"""
|
||||
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
|
||||
|
@ -21,7 +21,7 @@
|
||||
"""oxford instruments mercury IPS power supply"""
|
||||
|
||||
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.errors import BadValueError, HardwareError
|
||||
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)
|
||||
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
|
||||
working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
|
||||
channel_type = 'PSU'
|
||||
kind = 'PSU'
|
||||
slave_currents = None
|
||||
classdict = {}
|
||||
|
||||
def __new__(cls, name, logger, cfgdict, srv):
|
||||
base = cls.__bases__[1]
|
||||
def __new__(cls, name, logger, cfgdict, srv): # pylint: disable=arguments-differ
|
||||
nunits = cfgdict.get('nunits', 1)
|
||||
if isinstance(nunits, dict):
|
||||
nunits = nunits['value']
|
||||
if nunits == 1:
|
||||
obj = object.__new__(cls)
|
||||
return obj
|
||||
return Module.__new__(cls, name, logger, cfgdict, srv)
|
||||
classname = cls.__name__ + str(nunits)
|
||||
newclass = cls.classdict.get(classname)
|
||||
if not newclass:
|
||||
@ -62,8 +62,7 @@ class SimpleField(MercuryChannel, SimpleMagfield):
|
||||
|
||||
newclass = type(classname, (cls,), attrs)
|
||||
cls.classdict[classname] = newclass
|
||||
obj = object.__new__(newclass)
|
||||
return obj
|
||||
return Module.__new__(newclass, name, logger, cfgdict, srv)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
@ -73,34 +72,34 @@ class SimpleField(MercuryChannel, SimpleMagfield):
|
||||
self.log.error('can not set to hold %r', e)
|
||||
|
||||
def read_value(self):
|
||||
return self.query('PSU:SIG:FLD')
|
||||
return self.query('DEV::PSU:SIG:FLD')
|
||||
|
||||
def read_ramp(self):
|
||||
return self.query('PSU:SIG:RFST')
|
||||
return self.query('DEV::PSU:SIG:RFST')
|
||||
|
||||
def write_ramp(self, value):
|
||||
return self.change('PSU:SIG:RFST', value)
|
||||
return self.change('DEV::PSU:SIG:RFST', value)
|
||||
|
||||
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):
|
||||
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):
|
||||
return self.query('PSU:ATOB')
|
||||
return self.query('DEV::PSU:ATOB')
|
||||
|
||||
def read_voltage(self):
|
||||
return self.query('PSU:SIG:VOLT')
|
||||
return self.query('DEV::PSU:SIG:VOLT')
|
||||
|
||||
def read_working_ramp(self):
|
||||
return self.query('PSU:SIG:RFLD')
|
||||
return self.query('DEV::PSU:SIG:RFLD')
|
||||
|
||||
def read_setpoint(self):
|
||||
return self.query('PSU:SIG:FSET')
|
||||
return self.query('DEV::PSU:SIG:FSET')
|
||||
|
||||
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.run_to_set) == Action.run_to_set
|
||||
|
||||
@ -133,7 +132,7 @@ class SimpleField(MercuryChannel, SimpleMagfield):
|
||||
|
||||
class Field(SimpleField, Magfield):
|
||||
persistent_field = Parameter(
|
||||
'persistent field', FloatRange(unit='$'), readonly=False)
|
||||
'persistent field at last switch off', FloatRange(unit='$'), readonly=False)
|
||||
wait_switch_on = Parameter(
|
||||
'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60)
|
||||
wait_switch_off = Parameter(
|
||||
@ -142,7 +141,7 @@ class Field(SimpleField, Magfield):
|
||||
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
|
||||
|
||||
_field_mismatch = None
|
||||
__init = True
|
||||
__persistent_field = None # internal value of persistent field
|
||||
__switch_fixed_until = 0
|
||||
|
||||
def doPoll(self):
|
||||
@ -154,33 +153,41 @@ class Field(SimpleField, Magfield):
|
||||
# will complain and this will be handled in start_ramp_to_field
|
||||
self.switch_on_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)
|
||||
|
||||
def read_value(self):
|
||||
current = self.query('PSU:SIG:FLD')
|
||||
pf = self.query('PSU:SIG:PFLD')
|
||||
if self.__init:
|
||||
self.__init = False
|
||||
self.persistent_field = pf
|
||||
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
|
||||
current = self.query('DEV::PSU:SIG:FLD')
|
||||
if self.switch_heater == self.switch_heater.on:
|
||||
self.__persistent_field = current
|
||||
self.forced_persistent_field = False
|
||||
self._field_mismatch = False
|
||||
return current
|
||||
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
|
||||
return pf
|
||||
pf = self.query('DEV::PSU:SIG:PFLD')
|
||||
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):
|
||||
if self.slave_currents is None:
|
||||
self.slave_currents = [[] for _ in range(self.nunits + 1)]
|
||||
if self.nunits > 1:
|
||||
for i in range(1, self.nunits + 1):
|
||||
curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i)
|
||||
volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i)
|
||||
setattr(self, 'I%d' % i, curri)
|
||||
setattr(self, 'V%d' % i, volti)
|
||||
curri = self.query(f'DEV:PSU.M{i}:PSU:SIG:CURR')
|
||||
volti = self.query(f'DEV:PSU.M{i}:PSU:SIG:VOLT')
|
||||
setattr(self, f'I{i}', curri)
|
||||
setattr(self, f'V{i}', volti)
|
||||
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)
|
||||
min_ = min(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
|
||||
self.log.warning('individual currents mismatch %r', self.slave_currents)
|
||||
else:
|
||||
current = self.query('PSU:SIG:CURR')
|
||||
current = self.query('DEV::PSU:SIG:CURR')
|
||||
if self.atob:
|
||||
return current / self.atob
|
||||
return 0
|
||||
|
||||
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.__persistent_field = value
|
||||
return value
|
||||
raise BadValueError('changing persistent field needs forced_persistent_field=True')
|
||||
|
||||
@ -212,7 +220,7 @@ class Field(SimpleField, Magfield):
|
||||
return super().write_target(target)
|
||||
|
||||
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()
|
||||
if value != self.switch_heater:
|
||||
if now < self.__switch_fixed_until:
|
||||
@ -226,10 +234,10 @@ class Field(SimpleField, Magfield):
|
||||
return value
|
||||
|
||||
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):
|
||||
return self.query('PSU:SWOFT') * 0.001
|
||||
return self.query('DEV::PSU:SWOFT') * 0.001
|
||||
|
||||
def write_switch_heater(self, value):
|
||||
if value == self.read_switch_heater():
|
||||
@ -238,20 +246,20 @@ class Field(SimpleField, Magfield):
|
||||
return value
|
||||
self.__switch_fixed_until = time.time() + 10
|
||||
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
|
||||
|
||||
def start_ramp_to_field(self, sm):
|
||||
if abs(self.current - self.persistent_field) <= self.tolerance:
|
||||
self.log.info('leads %g are already at %g', self.current, self.persistent_field)
|
||||
if abs(self.current - self.__persistent_field) <= self.tolerance:
|
||||
self.log.info('leads %g are already at %g', self.current, self.__persistent_field)
|
||||
return self.ramp_to_field
|
||||
try:
|
||||
self.set_and_go(self.persistent_field)
|
||||
self.set_and_go(self.__persistent_field)
|
||||
except (HardwareError, AssertionError) as e:
|
||||
if self.switch_heater:
|
||||
self.log.warn('switch is already on!')
|
||||
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
|
||||
return self.wait_for_switch
|
||||
return self.ramp_to_field
|
||||
@ -274,7 +282,7 @@ class Field(SimpleField, Magfield):
|
||||
sm.try_cnt -= 1
|
||||
if sm.try_cnt < 0:
|
||||
raise
|
||||
self.set_and_go(sm.persistent_field)
|
||||
self.set_and_go(self.__persistent_field)
|
||||
return Retry
|
||||
|
||||
def wait_for_switch(self, sm):
|
||||
@ -283,14 +291,14 @@ class Field(SimpleField, Magfield):
|
||||
try:
|
||||
self.log.warn('try again')
|
||||
# try again
|
||||
self.set_and_go(self.persistent_field)
|
||||
except (HardwareError, AssertionError) as e:
|
||||
self.set_and_go(self.__persistent_field)
|
||||
except (HardwareError, AssertionError):
|
||||
return Retry
|
||||
return sm.after_wait
|
||||
|
||||
def wait_for_switch_on(self, sm):
|
||||
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
|
||||
return Retry
|
||||
self.log.warning('switch turned off manually?')
|
||||
@ -299,7 +307,7 @@ class Field(SimpleField, Magfield):
|
||||
|
||||
def wait_for_switch_off(self, sm):
|
||||
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
|
||||
return Retry
|
||||
self.log.warning('switch turned on manually?')
|
||||
@ -307,6 +315,9 @@ class Field(SimpleField, Magfield):
|
||||
return super().wait_for_switch_off(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:
|
||||
assert self.write_action(Action.hold) == Action.hold
|
||||
assert self.write_action(Action.run_to_zero) == Action.run_to_zero
|
||||
|
@ -20,12 +20,11 @@
|
||||
"""generic persistent magnet driver"""
|
||||
|
||||
import time
|
||||
from frappy.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR
|
||||
from frappy.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType
|
||||
from frappy.features import HasTargetLimits
|
||||
from frappy.errors import ConfigError, ProgrammingError, HardwareError, BadValueError
|
||||
from frappy.core import Drivable, Parameter, BUSY, Limit
|
||||
from frappy.datatypes import FloatRange, EnumType, TupleOf, StatusType
|
||||
from frappy.errors import ConfigError, HardwareError, DisabledError
|
||||
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()
|
||||
|
||||
@ -48,8 +47,10 @@ OFF = 0
|
||||
ON = 1
|
||||
|
||||
|
||||
class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
|
||||
class SimpleMagfield(HasStates, Drivable):
|
||||
value = Parameter('magnetic field', datatype=FloatRange(unit='T'))
|
||||
target_min = Limit()
|
||||
target_max = Limit()
|
||||
ramp = Parameter(
|
||||
'wanted ramp rate for field', FloatRange(unit='$/min'), readonly=False)
|
||||
# export only when different from ramp:
|
||||
@ -68,6 +69,22 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
|
||||
FloatRange(0, unit='s'), readonly=False, default=30)
|
||||
|
||||
_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):
|
||||
dt = self.parameters['target'].datatype
|
||||
@ -76,6 +93,9 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
|
||||
raise ConfigError('target.max not configured')
|
||||
if dt.min == UNLIMITED.min: # not given: assume bipolar symmetric
|
||||
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()
|
||||
|
||||
def stop(self):
|
||||
@ -101,7 +121,6 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
|
||||
return last
|
||||
|
||||
def write_target(self, target):
|
||||
self.check_limits(target)
|
||||
self.start_machine(self.start_field_change, target=target)
|
||||
return target
|
||||
|
||||
@ -147,8 +166,7 @@ class SimpleMagfield(HasStates, HasTargetLimits, Drivable):
|
||||
|
||||
class Magfield(SimpleMagfield):
|
||||
status = Parameter(datatype=StatusType(Status))
|
||||
mode = Parameter(
|
||||
'persistent mode', EnumType(Mode), readonly=False, initwrite=False, default=Mode.PERSISTENT)
|
||||
mode = Parameter('persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT)
|
||||
switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
|
||||
readonly=False, default=0)
|
||||
current = Parameter(
|
||||
@ -180,6 +198,9 @@ class Magfield(SimpleMagfield):
|
||||
def doPoll(self):
|
||||
if self.init_persistency:
|
||||
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.init_persistency = False
|
||||
super().doPoll()
|
||||
@ -207,9 +228,7 @@ class Magfield(SimpleMagfield):
|
||||
if self.mode == Mode.DISABLED:
|
||||
if target == 0:
|
||||
return 0
|
||||
self.log.info('raise error %r', target)
|
||||
raise BadValueError('disabled')
|
||||
self.check_limits(target)
|
||||
raise DisabledError('disabled')
|
||||
self.start_machine(self.start_field_change, target=target, mode=self.mode)
|
||||
return target
|
||||
|
||||
|
@ -25,12 +25,13 @@ import math
|
||||
import re
|
||||
import time
|
||||
|
||||
from frappy.core import Drivable, HasIO, Writable, \
|
||||
Parameter, Property, Readable, StringIO, Attached, IDLE, nopoll
|
||||
from frappy.core import Drivable, HasIO, Writable, StatusType, \
|
||||
Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll
|
||||
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.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/%]*)$')
|
||||
@ -126,7 +127,7 @@ class MercuryChannel(HasIO):
|
||||
msg = f'invalid reply {reply!r} to cmd {cmd!r}'
|
||||
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
|
||||
|
||||
: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 tolerance: tolerance for 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
|
||||
|
||||
Example (kind=TEMP, slot=DB6.T1:
|
||||
@ -145,6 +147,7 @@ class MercuryChannel(HasIO):
|
||||
adr = self._complete_adr(adr)
|
||||
params = [f'{k}:{convert(v)}' for k, v in values]
|
||||
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
|
||||
reply = self.communicate(cmd)
|
||||
head = f'STAT:SET:{adr}:'
|
||||
@ -153,29 +156,35 @@ class MercuryChannel(HasIO):
|
||||
replyiter = iter(reply[len(head):].split(':'))
|
||||
# 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))
|
||||
assert keys == tuple(k for k, _ in values)
|
||||
assert keys == givenkeys
|
||||
assert any(v == 'VALID' for v in valid)
|
||||
result = tuple(convert(r) for r in result)
|
||||
except (AssertionError, AttributeError, ValueError) as e:
|
||||
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
|
||||
if n_retry == 0:
|
||||
return [v[1] for v in values] # no readback check
|
||||
keys = [v[0] for v in values]
|
||||
debug = []
|
||||
readback = self.multiquery(adr, keys, convert, debug)
|
||||
for k, r, b in zip(keys, result, readback):
|
||||
return [v for _, v in values]
|
||||
if lazy:
|
||||
debug = [reply]
|
||||
readback = [v for _, v in values]
|
||||
else:
|
||||
debug = []
|
||||
readback = list(self.multiquery(adr, givenkeys, convert, debug))
|
||||
failed = False
|
||||
for i, ((k, v), r, b) in enumerate(zip(values, result, readback)):
|
||||
if convert is as_float:
|
||||
tol = max(abs(r) * 1e-3, abs(b) * 1e-3, tolerance)
|
||||
if abs(r - b) > tol:
|
||||
break
|
||||
elif r != b:
|
||||
break
|
||||
else:
|
||||
if abs(b - v) > tol or abs(r - v) > tol:
|
||||
readback[i] = None
|
||||
failed = True
|
||||
elif b != v or r != v:
|
||||
readback[i] = None
|
||||
failed = True
|
||||
if not failed:
|
||||
return readback
|
||||
self.log.warning('sent: %s', cmd)
|
||||
self.log.warning('got: %s', debug[0])
|
||||
return readback
|
||||
self.log.warning('sent: %s', cmd)
|
||||
self.log.warning('got: %s', debug[0])
|
||||
return tuple(v[1] if b is None else b for b, v in zip(readback, values))
|
||||
|
||||
def query(self, adr, convert=as_float):
|
||||
"""query a single parameter
|
||||
@ -185,9 +194,9 @@ class MercuryChannel(HasIO):
|
||||
adr, _, name = adr.rpartition(':')
|
||||
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(':')
|
||||
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):
|
||||
@ -202,38 +211,14 @@ class TemperatureSensor(MercuryChannel, Readable):
|
||||
return self.query('DEV::TEMP:SIG:RES')
|
||||
|
||||
|
||||
class HasInput(MercuryChannel):
|
||||
controlled_by = Parameter('source of target value', EnumType(members={'self': SELF}), default=0)
|
||||
# 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 HasInput(HasControlledBy, MercuryChannel):
|
||||
pass
|
||||
|
||||
|
||||
class Loop(HasConvergence, MercuryChannel, Drivable):
|
||||
class Loop(HasOutputModule, MercuryChannel, Drivable):
|
||||
"""common base class for loops"""
|
||||
control_active = Parameter('control mode', BoolType())
|
||||
output_module = Attached(HasInput, mandatory=False)
|
||||
control_active = Parameter(readonly=False)
|
||||
ctrlpars = Parameter(
|
||||
'pid (proportional band, integral time, differential time',
|
||||
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)
|
||||
|
||||
def initModule(self):
|
||||
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):
|
||||
def set_output(self, active, source='HW'):
|
||||
if active:
|
||||
if self.output_module and self.output_module.controlled_by != self.name:
|
||||
self.output_module.write_controlled_by(self.name)
|
||||
self.activate_control()
|
||||
else:
|
||||
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
|
||||
self.deactivate_control(source)
|
||||
|
||||
def set_target(self, target):
|
||||
if self.control_active:
|
||||
self.set_output(True)
|
||||
else:
|
||||
self.log.warning('switch loop control on')
|
||||
self.write_control_active(True)
|
||||
self.set_output(True)
|
||||
self.target = target
|
||||
self.start_state()
|
||||
|
||||
def read_enable_pid_table(self):
|
||||
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'])
|
||||
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
|
||||
|
||||
Remark:
|
||||
@ -324,7 +305,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
if self._last_target is not None:
|
||||
if not self.true_power:
|
||||
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
|
||||
|
||||
def read_status(self):
|
||||
@ -344,7 +325,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
self.write_resistivity(round(res, 1))
|
||||
if self.controlled_by == 0:
|
||||
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
|
||||
|
||||
def read_target(self):
|
||||
@ -362,23 +343,25 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
might be used by a software loop
|
||||
"""
|
||||
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
|
||||
return target
|
||||
|
||||
def write_target(self, value):
|
||||
self.write_controlled_by(SELF)
|
||||
self.self_controlled()
|
||||
return self.set_target(value)
|
||||
|
||||
|
||||
class TemperatureLoop(TemperatureSensor, Loop, Drivable):
|
||||
class TemperatureLoop(TemperatureSensor, ConvLoop):
|
||||
kind = 'TEMP'
|
||||
output_module = Attached(HasInput, mandatory=False)
|
||||
ramp = Parameter('ramp rate', FloatRange(0, unit='$/min'), readonly=False)
|
||||
enable_ramp = Parameter('enable ramp rate', BoolType(), readonly=False)
|
||||
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)
|
||||
_last_setpoint_change = None
|
||||
__status = IDLE, ''
|
||||
# overridden in subclass frappy_psi.triton.TemperatureLoop
|
||||
ENABLE = 'TEMP:LOOP:ENAB'
|
||||
ENABLE_RAMP = 'TEMP:LOOP:RENA'
|
||||
@ -394,7 +377,9 @@ class TemperatureLoop(TemperatureSensor, Loop, Drivable):
|
||||
return active
|
||||
|
||||
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)
|
||||
|
||||
@nopoll # polled by read_setpoint
|
||||
@ -407,43 +392,65 @@ class TemperatureLoop(TemperatureSensor, Loop, Drivable):
|
||||
def read_setpoint(self):
|
||||
setpoint = self.query('DEV::TEMP:LOOP:TSET')
|
||||
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
|
||||
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
|
||||
if unchanged_since > max(12.0, 0.06 / max(1e-4, self.ramp)):
|
||||
self.__ramping = False
|
||||
self.target = self.setpoint
|
||||
return setpoint
|
||||
self._last_setpoint_change = time.time()
|
||||
else:
|
||||
self.__ramping = False
|
||||
self.target = 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):
|
||||
target = self.change('DEV::TEMP:LOOP:TSET', value)
|
||||
target = self.change('DEV::TEMP:LOOP:TSET', value, lazy=True)
|
||||
if self.enable_ramp:
|
||||
self._last_setpoint_change = None
|
||||
self.__ramping = True
|
||||
self.set_target(value)
|
||||
self.convergence_state.status = RAMPING, 'ramping'
|
||||
self.read_status()
|
||||
self.convergence_state.start(self.ramping_state)
|
||||
else:
|
||||
self.set_target(target)
|
||||
self.convergence_start()
|
||||
self.read_status()
|
||||
return self.target
|
||||
|
||||
def read_enable_ramp(self):
|
||||
return self.query(f'DEV::{self.ENABLE_RAMP}', off_on)
|
||||
|
||||
def write_enable_ramp(self, value):
|
||||
return self.change(f'DEV::{self.ENABLE_RAMP}', value, off_on)
|
||||
|
||||
def set_output(self, active):
|
||||
if active:
|
||||
if self.output_module and self.output_module.controlled_by != self.name:
|
||||
self.output_module.write_controlled_by(self.name)
|
||||
else:
|
||||
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
|
||||
if self.enable_ramp < value: # ramp_enable was off: start from current value
|
||||
self.change('DEV::TEMP:LOOP:TSET', self.value, lazy=True)
|
||||
result = self.change(f'DEV::{self.ENABLE_RAMP}', value, off_on)
|
||||
if self.isDriving() and value != self.enable_ramp:
|
||||
self.enable_ramp = value
|
||||
self.write_target(self.target)
|
||||
return result
|
||||
|
||||
def read_ramp(self):
|
||||
result = self.query(f'DEV::{self.RAMP_RATE}')
|
||||
@ -470,7 +477,7 @@ class PressureSensor(MercuryChannel, Readable):
|
||||
return self.query('DEV::PRES:SIG:PRES')
|
||||
|
||||
|
||||
class ValvePos(HasInput, MercuryChannel, Drivable):
|
||||
class ValvePos(HasInput, Drivable):
|
||||
kind = 'PRES,AUX'
|
||||
value = Parameter('value pos', FloatRange(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')
|
||||
|
||||
def write_target(self, value):
|
||||
self.write_controlled_by(SELF)
|
||||
self.self_controlled()
|
||||
return self.change('DEV::PRES:LOOP:FSET', value)
|
||||
|
||||
|
||||
class PressureLoop(HasInput, PressureSensor, Loop, Drivable):
|
||||
class PressureLoop(PressureSensor, HasControlledBy, ConvLoop):
|
||||
kind = 'PRES'
|
||||
output_module = Attached(ValvePos, mandatory=False)
|
||||
tolerance = Parameter(default=0.1)
|
||||
@ -506,7 +513,7 @@ class PressureLoop(HasInput, PressureSensor, Loop, Drivable):
|
||||
return active
|
||||
|
||||
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)
|
||||
|
||||
def read_target(self):
|
||||
@ -521,7 +528,7 @@ class PressureLoop(HasInput, PressureSensor, Loop, Drivable):
|
||||
super().set_target(target)
|
||||
|
||||
def write_target(self, value):
|
||||
self.write_controlled_by(SELF)
|
||||
self.self_controlled()
|
||||
self.set_target(value)
|
||||
return value
|
||||
|
||||
@ -548,14 +555,13 @@ class HasAutoFlow:
|
||||
self.needle_valve.register_input(self.name, self.auto_flow_off)
|
||||
|
||||
def write_auto_flow(self, value):
|
||||
if value:
|
||||
if self.needle_valve and self.needle_valve.controlled_by != self.name:
|
||||
self.needle_valve.write_controlled_by(self.name)
|
||||
else:
|
||||
if self.needle_valve and self.needle_valve.controlled_by != SELF:
|
||||
self.needle_valve.write_controlled_by(SELF)
|
||||
_, (fmin, _) = self.flowpars
|
||||
self.needle_valve.write_target(fmin)
|
||||
if self.needle_valve:
|
||||
if value:
|
||||
self.needle_valve.controlled_by = self.name
|
||||
else:
|
||||
if self.needle_valve.controlled_by != SELF:
|
||||
self.needle_valve.controlled_by = SELF
|
||||
self.needle_valve.write_target(self.flowpars[1][0]) # flow min
|
||||
return value
|
||||
|
||||
def auto_flow_off(self):
|
||||
|
@ -40,9 +40,8 @@ from os.path import expanduser, join, exists
|
||||
from frappy.client import ProxyClient
|
||||
from frappy.datatypes import ArrayOf, BoolType, \
|
||||
EnumType, FloatRange, IntRange, StringType
|
||||
from frappy.errors import ConfigError, HardwareError, secop_error, NoSuchModuleError, \
|
||||
CommunicationFailedError
|
||||
from frappy.lib import generalConfig, mkthread, formatExtendedStack
|
||||
from frappy.errors import ConfigError, HardwareError, secop_error, CommunicationFailedError
|
||||
from frappy.lib import generalConfig, mkthread
|
||||
from frappy.lib.asynconn import AsynConn, ConnectionClosed
|
||||
from frappy.modules import Attached, Command, Done, Drivable, \
|
||||
Module, Parameter, Property, Readable, Writable
|
||||
@ -62,7 +61,7 @@ Mod(%(seaconn)r,
|
||||
"""
|
||||
|
||||
CFG_MODULE = """Mod(%(module)r,
|
||||
'frappy_psi.sea.%(modcls)s',
|
||||
'frappy_psi.sea.%(modcls)s', '',
|
||||
io = %(seaconn)r,
|
||||
sea_object = %(module)r,
|
||||
)
|
||||
@ -75,18 +74,18 @@ SERVICE_NAMES = {
|
||||
}
|
||||
|
||||
SEA_DIR = expanduser('~/sea')
|
||||
for confdir in generalConfig.confdir.split(os.pathsep):
|
||||
seaconfdir = join(confdir, 'sea')
|
||||
if exists(seaconfdir):
|
||||
break
|
||||
else:
|
||||
seaconfdir = os.environ.get('FRAPPY_SEA_DIR')
|
||||
seaconfdir = os.environ.get('FRAPPY_SEA_DIR')
|
||||
if not exists(seaconfdir):
|
||||
for confdir in generalConfig.confdir.split(os.pathsep):
|
||||
seaconfdir = join(confdir, 'sea')
|
||||
if exists(seaconfdir):
|
||||
break
|
||||
|
||||
|
||||
def get_sea_port(instance):
|
||||
for filename in ('sea_%s.tcl' % instance, 'sea.tcl'):
|
||||
try:
|
||||
with open(join(SEA_DIR, filename)) as f:
|
||||
with open(join(SEA_DIR, filename), encoding='utf-8') as f:
|
||||
for line in f:
|
||||
linesplit = line.split()
|
||||
if len(linesplit) == 3:
|
||||
@ -115,18 +114,21 @@ class SeaClient(ProxyClient, Module):
|
||||
_instance = None
|
||||
|
||||
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:
|
||||
self._instance = instance
|
||||
port = get_sea_port(instance)
|
||||
if port is None:
|
||||
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.shutdown = False
|
||||
self.path2param = {}
|
||||
self._write_lock = threading.Lock()
|
||||
config = opts.get('config')
|
||||
if isinstance(config, dict):
|
||||
config = config['value']
|
||||
if config:
|
||||
self.default_json_file[name] = config.split()[0] + '.json'
|
||||
self.syncio = None
|
||||
@ -147,14 +149,17 @@ class SeaClient(ProxyClient, Module):
|
||||
def _connect(self, started_callback):
|
||||
if self._instance:
|
||||
if not self._service_manager:
|
||||
from servicemanager import SeaManager
|
||||
self._service_manager = SeaManager()
|
||||
self._service_manager.do_start(self._instance)
|
||||
if self._service_manager is None:
|
||||
try:
|
||||
from servicemanager import SeaManager # pylint: disable=import-outside-toplevel
|
||||
self._service_manager = SeaManager()
|
||||
except ImportError:
|
||||
self._service_manager = False
|
||||
if self._service_manager:
|
||||
self._service_manager.do_start(self._instance)
|
||||
if '//' not in self.uri:
|
||||
self.uri = 'tcp://' + self.uri
|
||||
self.asynio = AsynConn(self.uri)
|
||||
# print('CONNECT', self.uri, self.asynio)
|
||||
# print(formatExtendedStack())
|
||||
reply = self.asynio.readline()
|
||||
if reply != b'OK':
|
||||
raise CommunicationFailedError('reply %r should be "OK"' % reply)
|
||||
@ -184,13 +189,11 @@ class SeaClient(ProxyClient, Module):
|
||||
pass
|
||||
self._connect(None)
|
||||
self.syncio = AsynConn(self.uri)
|
||||
# print('SYNCIO', self.uri)
|
||||
assert self.syncio.readline() == b'OK'
|
||||
self.syncio.writeline(b'seauser seaser')
|
||||
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()
|
||||
# print('> %s' % command)
|
||||
ft = 'fulltransAct' if quiet else 'fulltransact'
|
||||
self.syncio.writeline(('%s %s' % (ft, command)).encode())
|
||||
result = None
|
||||
@ -203,19 +206,18 @@ class SeaClient(ProxyClient, Module):
|
||||
except ConnectionClosed:
|
||||
break
|
||||
reply = reply.decode()
|
||||
# print('< %s' % reply)
|
||||
if reply.startswith('TRANSACTIONSTART'):
|
||||
result = []
|
||||
continue
|
||||
if reply == 'TRANSACTIONFINISHED':
|
||||
if result is None:
|
||||
print('missing TRANSACTIONSTART on: %s' % command)
|
||||
self.log.info('missing TRANSACTIONSTART on: %s', command)
|
||||
return ''
|
||||
if not result:
|
||||
return ''
|
||||
return '\n'.join(result)
|
||||
if result is None:
|
||||
print('swallow: %s' % reply)
|
||||
self.log.info('swallow: %s', reply)
|
||||
continue
|
||||
if not result:
|
||||
result = [reply.split('=', 1)[-1]]
|
||||
@ -234,7 +236,7 @@ class SeaClient(ProxyClient, Module):
|
||||
try:
|
||||
msg = json.loads(reply)
|
||||
except Exception as e:
|
||||
print(repr(e), reply)
|
||||
self.log.warn('bad reply %r %r', e, reply)
|
||||
continue
|
||||
if isinstance(msg, str):
|
||||
if msg.startswith('_E '):
|
||||
@ -259,12 +261,12 @@ class SeaClient(ProxyClient, Module):
|
||||
continue
|
||||
if flag != 'hdbevent':
|
||||
if obj not in ('frappy_async_client', 'get_all_param'):
|
||||
print('SKIP', msg)
|
||||
self.log.debug('skip %r', msg)
|
||||
continue
|
||||
if not data:
|
||||
continue
|
||||
if not isinstance(data, dict):
|
||||
print('what means %r' % msg)
|
||||
self.log.debug('what means %r', msg)
|
||||
continue
|
||||
now = time.time()
|
||||
for path, value in data.items():
|
||||
@ -318,7 +320,7 @@ class SeaClient(ProxyClient, Module):
|
||||
|
||||
|
||||
class SeaConfigCreator(SeaClient):
|
||||
def startModule(self, started_callback):
|
||||
def startModule(self, start_events):
|
||||
"""save objects (and sub-objects) description and exit"""
|
||||
self._connect(None)
|
||||
reply = self.request('describe_all')
|
||||
@ -343,20 +345,20 @@ class SeaConfigCreator(SeaClient):
|
||||
service = SERVICE_NAMES[ext]
|
||||
seaconn = 'sea_' + service
|
||||
cfgfile = join(seaconfdir, stripped + '_cfg.py')
|
||||
with open(cfgfile, 'w') as fp:
|
||||
fp.write(CFG_HEADER % dict(config=filename, seaconn=seaconn, service=service,
|
||||
nodedescr=description.get(filename, filename)))
|
||||
with open(cfgfile, 'w', encoding='utf-8') as fp:
|
||||
fp.write(CFG_HEADER % {'config': filename, 'seaconn': seaconn, 'service': service,
|
||||
'nodedescr': description.get(filename, filename)})
|
||||
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')
|
||||
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')
|
||||
result.append('%s: %s' % (filename, ','.join(n for n in descr)))
|
||||
raise SystemExit('; '.join(result))
|
||||
|
||||
@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"""
|
||||
errors = []
|
||||
reply = None
|
||||
@ -400,12 +402,18 @@ class SeaModule(Module):
|
||||
sea_object = None
|
||||
hdbpath = None # hdbpath for main writable
|
||||
|
||||
# pylint: disable=too-many-statements,arguments-differ,too-many-branches
|
||||
def __new__(cls, name, logger, cfgdict, srv):
|
||||
if hasattr(srv, 'extra_sea_modules'):
|
||||
extra_modules = srv.extra_sea_modules
|
||||
else:
|
||||
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']]
|
||||
visibility_level = cfgdict.pop('visibility_level', 2)
|
||||
|
||||
@ -416,25 +424,22 @@ class SeaModule(Module):
|
||||
paramdesc['key'] = 'value'
|
||||
if issubclass(cls, SeaWritable):
|
||||
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
|
||||
paramdesc['key'] = 'target'
|
||||
paramdesc['readonly'] = False
|
||||
extra_module_set = ()
|
||||
if 'description' not in cfgdict:
|
||||
cfgdict['description'] = '%s@%s' % (single_module, json_file)
|
||||
cfgdict['description'] = f'{single_module}@{json_file}'
|
||||
else:
|
||||
sea_object = cfgdict.pop('sea_object')
|
||||
rel_paths = cfgdict.pop('rel_paths', '.')
|
||||
if 'description' not in cfgdict:
|
||||
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:
|
||||
# sea_object, descr = json.load(fp)
|
||||
with open(join(seaconfdir, json_file)) as fp:
|
||||
with open(join(seaconfdir, json_file), encoding='utf-8') as fp:
|
||||
content = json.load(fp)
|
||||
# print(json_file, content.keys())
|
||||
descr = content[sea_object]
|
||||
if rel_paths == '*' or not rel_paths:
|
||||
# take all
|
||||
@ -478,7 +483,6 @@ class SeaModule(Module):
|
||||
result.pop(0)
|
||||
else:
|
||||
logger.error('%s: no value found', name)
|
||||
# logger.info('PARAMS %s %r', name, result)
|
||||
base = descr['base']
|
||||
params = descr['params']
|
||||
if issubclass(cls, SeaWritable):
|
||||
@ -486,14 +490,14 @@ class SeaModule(Module):
|
||||
assert paramdesc['key'] == 'value'
|
||||
params.append(paramdesc.copy()) # copy value?
|
||||
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['readonly'] = False
|
||||
extra_module_set = cfgdict.pop('extra_modules', ())
|
||||
if extra_module_set:
|
||||
extra_module_set = set(extra_module_set.replace(',', ' ').split())
|
||||
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):
|
||||
if sea_object in ('table', 'cc'):
|
||||
@ -504,12 +508,10 @@ class SeaModule(Module):
|
||||
path = paramdesc['path']
|
||||
readonly = paramdesc.get('readonly', True)
|
||||
dt = get_datatype(paramdesc)
|
||||
#print('----', sea_object)
|
||||
#print(dt, paramdesc)
|
||||
kwds = dict(description=paramdesc.get('description', path),
|
||||
datatype=dt,
|
||||
visibility=paramdesc.get('visibility', 1),
|
||||
needscfg=False, readonly=readonly)
|
||||
kwds = {'description': paramdesc.get('description', path),
|
||||
'datatype': dt,
|
||||
'visibility': paramdesc.get('visibility', 1),
|
||||
'needscfg': False, 'readonly': readonly}
|
||||
if kwds['datatype'] is None:
|
||||
kwds.update(visibility=3, default='', datatype=StringType())
|
||||
pathlist = path.split('/') if path else []
|
||||
@ -555,10 +557,8 @@ class SeaModule(Module):
|
||||
continue # skip this parameter
|
||||
path2param.setdefault(hdbpath, []).append((name, key))
|
||||
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)
|
||||
try:
|
||||
reply = float(reply)
|
||||
@ -571,15 +571,13 @@ class SeaModule(Module):
|
||||
attributes['read_' + key] = rfunc
|
||||
|
||||
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']):
|
||||
value = datatype.export_value(value)
|
||||
if isinstance(value, bool):
|
||||
value = int(value)
|
||||
# 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)
|
||||
return Done
|
||||
|
||||
@ -596,7 +594,7 @@ class SeaModule(Module):
|
||||
attributes[pname] = pobj
|
||||
pobj.__set_name__(cls, pname)
|
||||
|
||||
classname = '%s_%s' % (cls.__name__, name)
|
||||
classname = f'{cls.__name__}_{name}'
|
||||
newcls = type(classname, (cls,), attributes)
|
||||
result = Module.__new__(newcls)
|
||||
return result
|
||||
@ -609,11 +607,9 @@ class SeaModule(Module):
|
||||
try:
|
||||
pobj = self.parameters[parameter]
|
||||
except KeyError:
|
||||
print(self.name, parameter)
|
||||
self.log.error('do not know %s:%s', self.name, parameter)
|
||||
raise
|
||||
pobj.timestamp = timestamp
|
||||
#if not pobj.readonly and pobj.value != value:
|
||||
# print('UPDATE', module, parameter, value)
|
||||
# should be done here: deal with clock differences
|
||||
if not readerror:
|
||||
try:
|
||||
@ -668,7 +664,7 @@ class SeaDrivable(SeaModule, Drivable):
|
||||
# return self.target
|
||||
|
||||
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']
|
||||
return value
|
||||
|
||||
@ -701,4 +697,4 @@ class SeaDrivable(SeaModule, Drivable):
|
||||
- on stdsct drivables this will call the halt script
|
||||
- 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')
|
||||
|
@ -20,14 +20,13 @@
|
||||
# *****************************************************************************
|
||||
"""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, \
|
||||
Done, Property
|
||||
from frappy.datatypes import EnumType, FloatRange, StringType
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF
|
||||
from frappy.lib import clamp
|
||||
import frappy_psi.mercury as mercury
|
||||
from frappy_psi import mercury
|
||||
|
||||
actions = Enum(none=0, condense=1, circulate=2, collect=3)
|
||||
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):
|
||||
channel_type = 'ACTN'
|
||||
kind = 'ACTN'
|
||||
cooldown_channel = Property('cool down channel', StringType(), 'T5')
|
||||
mix_channel = Property('mix channel', StringType(), 'T5')
|
||||
value = Parameter('running action', EnumType(actions))
|
||||
@ -72,7 +71,7 @@ class Action(MercuryChannel, Writable):
|
||||
|
||||
|
||||
class Valve(MercuryChannel, Drivable):
|
||||
channel_type = 'VALV'
|
||||
kind = 'VALV'
|
||||
value = Parameter('valve state', EnumType(closed=0, opened=1))
|
||||
target = Parameter('valve target', EnumType(close=0, open=1))
|
||||
|
||||
@ -82,7 +81,7 @@ class Valve(MercuryChannel, Drivable):
|
||||
self.read_status()
|
||||
|
||||
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):
|
||||
pos = self.read_value()
|
||||
@ -92,41 +91,41 @@ class Valve(MercuryChannel, Drivable):
|
||||
# success
|
||||
if self._try_count:
|
||||
# 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.setFastPoll(False)
|
||||
return IDLE, ''
|
||||
self._try_count += 1
|
||||
if self._try_count % 4 == 0:
|
||||
# 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'
|
||||
if self._try_count > 9:
|
||||
# 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
|
||||
self.change('VALV:SIG:STATE', self.target, open_close)
|
||||
self.change('DEV::VALV:SIG:STATE', self.target, open_close)
|
||||
return BUSY, 'waiting'
|
||||
|
||||
def write_target(self, value):
|
||||
if value != self.read_value():
|
||||
self._try_count = 0
|
||||
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
|
||||
return value
|
||||
|
||||
|
||||
class Pump(MercuryChannel, Writable):
|
||||
channel_type = 'PUMP'
|
||||
kind = 'PUMP'
|
||||
value = Parameter('pump state', EnumType(off=0, on=1))
|
||||
target = Parameter('pump target', EnumType(off=0, on=1))
|
||||
|
||||
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):
|
||||
return self.change('PUMP:SIG:STATE', value, off_on)
|
||||
return self.change('DEV::PUMP:SIG:STATE', value, off_on)
|
||||
|
||||
def read_status(self):
|
||||
return IDLE, ''
|
||||
@ -142,35 +141,35 @@ class TurboPump(Pump):
|
||||
electronics_temp = Parameter('temperature of electronics', FloatRange(unit='K'))
|
||||
|
||||
def read_status(self):
|
||||
status = self.query('PUMP:STATUS', str)
|
||||
status = self.query('DEV::PUMP:STATUS', str)
|
||||
if status == 'OK':
|
||||
return IDLE, ''
|
||||
return WARN, status
|
||||
|
||||
def read_power(self):
|
||||
return self.query('PUMP:SIG:POWR')
|
||||
return self.query('DEV::PUMP:SIG:POWR')
|
||||
|
||||
def read_freq(self):
|
||||
return self.query('PUMP:SIG:SPD')
|
||||
return self.query('DEV::PUMP:SIG:SPD')
|
||||
|
||||
def read_powerstage_temp(self):
|
||||
return self.query('PUMP:SIG:PST')
|
||||
return self.query('DEV::PUMP:SIG:PST')
|
||||
|
||||
def read_motor_temp(self):
|
||||
return self.query('PUMP:SIG:MT')
|
||||
return self.query('DEV::PUMP:SIG:MT')
|
||||
|
||||
def read_bearing_temp(self):
|
||||
return self.query('PUMP:SIG:BT')
|
||||
return self.query('DEV::PUMP:SIG:BT')
|
||||
|
||||
def read_pumpbase_temp(self):
|
||||
return self.query('PUMP:SIG:PBT')
|
||||
return self.query('DEV::PUMP:SIG:PBT')
|
||||
|
||||
def read_electronics_temp(self):
|
||||
return self.query('PUMP:SIG:ET')
|
||||
return self.query('DEV::PUMP:SIG:ET')
|
||||
|
||||
|
||||
# class PulseTubeCompressor(MercuryChannel, Writable):
|
||||
# channel_type = 'PTC'
|
||||
# kind = 'PTC'
|
||||
# value = Parameter('compressor state', EnumType(closed=0, opened=1))
|
||||
# target = Parameter('compressor target', EnumType(close=0, open=1))
|
||||
# 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'))
|
||||
#
|
||||
# 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):
|
||||
# return self.change('PTC:SIG:STATE', value, off_on)
|
||||
# return self.change('DEV::PTC:SIG:STATE', value, off_on)
|
||||
#
|
||||
# def read_status(self):
|
||||
# # 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):
|
||||
# return self.query('PTC:SIG:WIT')
|
||||
# return self.query('DEV::PTC:SIG:WIT')
|
||||
#
|
||||
# def read_water_out_temp(self):
|
||||
# return self.query('PTC:SIG:WOT')
|
||||
# return self.query('DEV::PTC:SIG:WOT')
|
||||
#
|
||||
# def read_helium_temp(self):
|
||||
# return self.query('PTC:SIG:HT')
|
||||
# return self.query('DEV::PTC:SIG:HT')
|
||||
#
|
||||
# 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):
|
||||
# return self.query('PTC:SIG:HHP')
|
||||
# return self.query('DEV::PTC:SIG:HHP')
|
||||
#
|
||||
# def read_motor_current(self):
|
||||
# return self.query('PTC:SIG:MCUR')
|
||||
# return self.query('DEV::PTC:SIG:MCUR')
|
||||
|
||||
|
||||
class FlowMeter(MercuryChannel, Readable):
|
||||
channel_type = 'FLOW'
|
||||
kind = 'FLOW'
|
||||
|
||||
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: switch on/off filter, check
|
||||
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)
|
||||
|
||||
def read_filter_time(self):
|
||||
return self.query('TEMP:FILT:TIME')
|
||||
return self.query('DEV::TEMP:FILT:TIME')
|
||||
|
||||
def write_filter_time(self, value):
|
||||
self.change('TEMP:FILT:WIN', 80)
|
||||
return self.change('TEMP:FILT:TIME', value)
|
||||
self.change('DEV::TEMP:FILT:WIN', 80)
|
||||
return self.change('DEV::TEMP:FILT:TIME', value)
|
||||
|
||||
def read_dwell_time(self):
|
||||
return self.query('TEMP:MEAS:DWEL')
|
||||
return self.query('DEV::TEMP:MEAS:DWEL')
|
||||
|
||||
def write_dwell_time(self, value):
|
||||
self.change('TEMP:FILT:WIN', 80)
|
||||
return self.change('TEMP:MEAS:DWEL', value)
|
||||
self.change('DEV::TEMP:FILT:WIN', 80)
|
||||
return self.change('DEV::TEMP:MEAS:DWEL', value)
|
||||
|
||||
def read_pause_time(self):
|
||||
return self.query('TEMP:MEAS:PAUS')
|
||||
return self.query('DEV::TEMP:MEAS:PAUS')
|
||||
|
||||
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):
|
||||
@ -261,7 +260,7 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
|
||||
if self.system_channel:
|
||||
self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str)
|
||||
if value:
|
||||
self.change('TEMP:LOOP:FILT:ENAB', 'ON', str)
|
||||
self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str)
|
||||
if self.output_module:
|
||||
limit = self.output_module.read_limit() or None # None: max. limit
|
||||
self.output_module.write_limit(limit)
|
||||
@ -270,16 +269,16 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
|
||||
|
||||
class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
"""heater output"""
|
||||
channel_type = 'HTR'
|
||||
kind = 'HTR'
|
||||
value = Parameter('heater output', FloatRange(unit='uW'))
|
||||
target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False)
|
||||
resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm'))
|
||||
|
||||
def read_resistivity(self):
|
||||
return self.query('HTR:RES')
|
||||
return self.query('DEV::HTR:RES')
|
||||
|
||||
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):
|
||||
if self.controlled_by != 0:
|
||||
@ -291,17 +290,17 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
|
||||
if self.resistivity:
|
||||
# round to the next voltage step
|
||||
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):
|
||||
"""heater output with heater range"""
|
||||
channel_type = 'HTR,TEMP'
|
||||
kind = 'HTR,TEMP'
|
||||
|
||||
limit = Parameter('max. heater power', FloatRange(unit='uW'), readonly=False)
|
||||
|
||||
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
|
||||
|
||||
def write_limit(self, value):
|
||||
@ -315,6 +314,5 @@ class HeaterOutputWithRange(HeaterOutput):
|
||||
break
|
||||
else:
|
||||
maxcur = cur
|
||||
self.change('TEMP:LOOP:RANGE', maxcur)
|
||||
self.change('DEV::TEMP:LOOP:RANGE', maxcur)
|
||||
return self.read_limit()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user