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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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