diff --git a/frappy/client/__init__.py b/frappy/client/__init__.py index b3bcc6f..59fb20f 100644 --- a/frappy/client/__init__.py +++ b/frappy/client/__init__.py @@ -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) diff --git a/frappy/config.py b/frappy/config.py index b8b645e..c19cc6a 100644 --- a/frappy/config.py +++ b/frappy/config.py @@ -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: diff --git a/frappy/datatypes.py b/frappy/datatypes.py index 49a91c7..28dad55 100644 --- a/frappy/datatypes.py +++ b/frappy/datatypes.py @@ -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): diff --git a/frappy/lib/__init__.py b/frappy/lib/__init__.py index 81fc108..827ac03 100644 --- a/frappy/lib/__init__.py +++ b/frappy/lib/__init__.py @@ -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]]) diff --git a/frappy/mixins.py b/frappy/mixins.py index c353ad4..8bfad2c 100644 --- a/frappy/mixins.py +++ b/frappy/mixins.py @@ -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}') diff --git a/frappy/modules.py b/frappy/modules.py index fafd3b4..0105c51 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -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 diff --git a/frappy_psi/convergence.py b/frappy_psi/convergence.py index 0a34a95..6a079c4 100644 --- a/frappy_psi/convergence.py +++ b/frappy_psi/convergence.py @@ -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 + + . ''', 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 - 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 diff --git a/frappy_psi/ips_mercury.py b/frappy_psi/ips_mercury.py index 978bc58..467f9eb 100644 --- a/frappy_psi/ips_mercury.py +++ b/frappy_psi/ips_mercury.py @@ -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 diff --git a/frappy_psi/magfield.py b/frappy_psi/magfield.py index ae58ffc..71f128b 100644 --- a/frappy_psi/magfield.py +++ b/frappy_psi/magfield.py @@ -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 diff --git a/frappy_psi/mercury.py b/frappy_psi/mercury.py index 2ffade5..bdb5227 100644 --- a/frappy_psi/mercury.py +++ b/frappy_psi/mercury.py @@ -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): diff --git a/frappy_psi/sea.py b/frappy_psi/sea.py index c73b1be..c78b6f0 100644 --- a/frappy_psi/sea.py +++ b/frappy_psi/sea.py @@ -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') diff --git a/frappy_psi/triton.py b/frappy_psi/triton.py index 1feffd8..99f7f40 100644 --- a/frappy_psi/triton.py +++ b/frappy_psi/triton.py @@ -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() -