From c3f55435da1f7ac959d5669780da4468d82ef086 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Wed, 14 Sep 2022 10:59:55 +0200 Subject: [PATCH 01/22] add reading of slave currents and voltages with fast polling --- secop_psi/ips_mercury.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index 6020359..ebd07e4 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -22,7 +22,7 @@ from secop.core import Parameter, EnumType, FloatRange, BoolType from secop.lib.enum import Enum -from secop.errors import BadValueError +from secop.errors import BadValueError, HardwareError from secop_psi.magfield import Magfield from secop_psi.mercury import MercuryChannel, off_on, Mapped @@ -37,6 +37,12 @@ class Field(MercuryChannel, Magfield): setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0) voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0) atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0) + I1 = Parameter('master current', FloatRange(unit='A'), default=0) + I2 = Parameter('slave 2 current', FloatRange(unit='A'), default=0) + I3 = Parameter('slave 3 current', FloatRange(unit='A'), default=0) + V1 = Parameter('master voltage', FloatRange(unit='V'), default=0) + V2 = Parameter('slave 2 voltage', FloatRange(unit='V'), default=0) + V3 = Parameter('slave 3 voltage', FloatRange(unit='V'), default=0) forced_persistent_field = Parameter( 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) @@ -46,6 +52,10 @@ class Field(MercuryChannel, Magfield): slave_currents = None __init = True + def doPoll(self): + super().doPoll() + self.read_current() + def read_value(self): self.current = self.query('PSU:SIG:FLD') pf = self.query('PSU:SIG:PFLD') @@ -104,7 +114,11 @@ class Field(MercuryChannel, Magfield): current = self.query('PSU:SIG:CURR') for i in range(self.nslaves + 1): if i: - self.slave_currents[i].append(self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i)) + 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) + self.slave_currents[i].append(curri) else: self.slave_currents[i].append(current) min_i = min(self.slave_currents[i]) From b0315e133b07ae779cb4261edd9e9f36824979e3 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Sep 2022 13:58:12 +0200 Subject: [PATCH 02/22] rework switch timing - specific things in ips_mercury.py - general things in magfield.py Change-Id: I7c2bae815b9a80a17803b44b8941ef3dea3adb60 --- secop_psi/ips_mercury.py | 17 +++++++++++++++-- secop_psi/magfield.py | 41 ++++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index ebd07e4..f74972a 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -20,6 +20,7 @@ # ***************************************************************************** """oxford instruments mercury IPS power supply""" +import time from secop.core import Parameter, EnumType, FloatRange, BoolType from secop.lib.enum import Enum from secop.errors import BadValueError, HardwareError @@ -51,6 +52,7 @@ class Field(MercuryChannel, Magfield): nslaves = 3 slave_currents = None __init = True + __reset_switch_time = False def doPoll(self): super().doPoll() @@ -62,7 +64,7 @@ class Field(MercuryChannel, Magfield): if self.__init: self.__init = False self.persistent_field = pf - if self.switch_heater != 0 or self._field_mismatch is None: + if self.switch_heater == self.switch_heater.on or self._field_mismatch is None: self.forced_persistent_field = False self._field_mismatch = False return self.current @@ -94,7 +96,18 @@ class Field(MercuryChannel, Magfield): return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp) def read_switch_heater(self): - return self.query('PSU:SIG:SWHT', off_on) + value = self.query('PSU:SIG:SWHT', off_on) + now = time.time() + switch_time = self.switch_time[self.switch_heater] + if value != self.switch_heater: + self.__reset_switch_time = True + if now < (switch_time or 0) + 10: + # probably switch heater was changed, but IPS reply is not yet updated + return self.switch_heater + elif self.__reset_switch_time: + self.__reset_switch_time = False + self.switch_time = [None, None] + return value def write_switch_heater(self, value): return self.change('PSU:SIG:SWHT', value, off_on) diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index 1d800b2..ef90bc2 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -44,6 +44,9 @@ Status = Enum( FINALIZING=390, ) +OFF = 0 +ON = 1 + class Magfield(HasLimits, Drivable): value = Parameter('magnetic field', datatype=FloatRange(unit='T')) @@ -52,7 +55,7 @@ class Magfield(HasLimits, Drivable): 'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT) tolerance = Parameter( 'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002) - switch_heater = Parameter('switch heater', EnumType(off=0, on=1), + switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON), readonly=False, default=0) persistent_field = Parameter( 'persistent field', FloatRange(unit='$'), readonly=False) @@ -87,8 +90,7 @@ class Magfield(HasLimits, Drivable): _state = None __init = True _last_target = None - switch_on_time = None - switch_off_time = None + switch_time = None, None def doPoll(self): if self.__init: @@ -185,14 +187,11 @@ class Magfield(HasLimits, Drivable): def update_switch_heater(self, value): """is called whenever switch heater was changed""" - if value != 0: - self.switch_off_time = None - if self.switch_on_time is None: - self.switch_on_time = time.time() - else: - self.switch_on_time = None - if self.switch_off_time is None: - self.switch_off_time = time.time() + switch_time = self.switch_time[value] + if switch_time is None: + switch_time = time.time() + self.switch_time = [None, None] + self.switch_time[value] = switch_time def start_switch_on(self, state): """switch heater on""" @@ -213,12 +212,10 @@ class Magfield(HasLimits, Drivable): abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off self.read_switch_heater() - if self.switch_on_time is None: - if state.now - self.switch_off_time > 10: - self.log.warning('switch turned off manually?') - return self.start_switch_on - return Retry() - if state.now - self.switch_on_time < self.wait_switch_on: + if self.switch_time[ON] is None: + self.log.warning('switch turned off manually?') + return self.start_switch_on + if state.now - self.switch_time[ON] < self.wait_switch_on: return Retry() self._last_target = self.target return self.start_ramp_to_target @@ -279,12 +276,10 @@ class Magfield(HasLimits, Drivable): return self.start_switch_on self.persistent_field = self.value self.read_switch_heater() - if self.switch_off_time is None: - if state.now - self.switch_on_time > 10: - self.log.warning('switch turned on manually?') - return self.start_switch_off - return Retry() - if state.now - self.switch_off_time < self.wait_switch_off: + if self.switch_time[OFF] is None: + self.log.warning('switch turned on manually?') + return self.start_switch_off + if state.now - self.switch_time[OFF] < self.wait_switch_off: return Retry() if abs(self.value) > self.persistent_limit: self.status = Status.IDLE, 'leads current at field, switch off' From aad1c33742bb6a3ad050927c456a3f100711c344 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Sep 2022 13:59:51 +0200 Subject: [PATCH 03/22] improvements on interactive client - add selective logging - fix handling of exceptions Change-Id: I7e2c2d4ed12302874c3bb2cc7bd707aa8e487341 --- secop/client/__init__.py | 4 ++-- secop/client/interactive.py | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/secop/client/__init__.py b/secop/client/__init__.py index 346179e..e01522a 100644 --- a/secop/client/__init__.py +++ b/secop/client/__init__.py @@ -356,7 +356,7 @@ class SecopClient(ProxyClient): except ConnectionClosed: pass except Exception as e: - self.log.error('rxthread ended with %s' % e) + self.log.error('rxthread ended with %r', e) self._rxthread = None self.disconnect(False) if self._shutdown: @@ -490,7 +490,7 @@ class SecopClient(ProxyClient): def _unhandled_message(self, action, ident, data): if not self.callback(None, 'unhandledMessage', action, ident, data): - self.log.warning('unhandled message: %s %s %r' % (action, ident, data)) + self.log.warning('unhandled message: %s %s %r', action, ident, data) def _set_state(self, online, state=None): # remark: reconnecting is treated as online diff --git a/secop/client/interactive.py b/secop/client/interactive.py index 85d6428..2eb6365 100644 --- a/secop/client/interactive.py +++ b/secop/client/interactive.py @@ -24,6 +24,7 @@ import sys import time import json +import re from queue import Queue from secop.client import SecopClient from secop.errors import SECoPError @@ -58,10 +59,15 @@ class Logger: if lev == loglevel: func = self.emit setattr(self, lev, func) + self._minute = 0 - @staticmethod - def emit(fmt, *args, **kwds): - print(str(fmt) % args) + def emit(self, fmt, *args, **kwds): + now = time.time() + minute = now // 60 + if minute != self._minute: + self._minute = minute + print(time.strftime('--- %H:%M:%S ---', time.localtime(now))) + print('%6.3f' % (now % 60.0), str(fmt) % args) @staticmethod def noop(fmt, *args, **kwds): @@ -77,6 +83,8 @@ class PrettyFloat(float): class Module: + _log_pattern = re.compile('.*') + def __init__(self, name, secnode): self._name = name self._secnode = secnode @@ -174,6 +182,14 @@ class Module: '\n'.join(self._one_line(k, wid) for k in self._parameters), ', '.join(k + '()' for k in self._commands)) + def logging(self, level='comlog', pattern='.*'): + self._log_pattern = re.compile(pattern) + self._secnode.request('logging', self._name, level) + + def handle_log_message_(self, data): + if self._log_pattern.match(data): + self._secnode.log.info('%s: %r', self._name, data) + class Param: def __init__(self, name, unit=None): @@ -258,6 +274,17 @@ class Client(SecopClient): if 'status' in mobj._parameters: self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update) self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update) - setattr(main, modname, mobj) + self.register_callback(None, self.unhandledMessage) self.log.info('%s', USAGE) + + def unhandledMessage(self, action, ident, data): + """handle logging messages""" + if action == 'log': + modname = ident.split(':')[0] + modobj = getattr(main, modname, None) + if modobj: + modobj.handle_log_message_(data) + return + self.log.info('module %s not found', modname) + self.log.info('unhandled: %s %s %r', action, ident, data) From 4287ec6477c720aa929d2877014fcb8a6ab6e232 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Sep 2022 14:50:39 +0200 Subject: [PATCH 04/22] apply main unit also in structured types Change-Id: I5a3efb167f2b460b847d8e7ac75a21848976b5f8 --- secop/datatypes.py | 28 ++++++++++++++++++++++++---- secop/modules.py | 8 ++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/secop/datatypes.py b/secop/datatypes.py index 5da9a99..8296c2f 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -124,6 +124,9 @@ class DataType(HasProperties): """ raise NotImplementedError + def set_main_unit(self, unit): + """replace $ in unit by argument""" + class Stub(DataType): """incomplete datatype, to be replaced with a proper one later during module load @@ -155,9 +158,17 @@ class Stub(DataType): prop.datatype = globals()[stub.name](*stub.args, **stub.kwds) +class HasUnit: + unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='') + + def set_main_unit(self, unit): + if '$' in self.unit: + self.setProperty('unit', self.unit.replace('$', unit)) + + # SECoP types: -class FloatRange(DataType): +class FloatRange(HasUnit, DataType): """(restricted) float type :param minval: (property **min**) @@ -166,7 +177,6 @@ class FloatRange(DataType): """ min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max) max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max) - unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='') fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0), extname='absolute_resolution', default=0.0) @@ -331,7 +341,7 @@ class IntRange(DataType): raise BadValueError('incompatible datatypes') -class ScaledInteger(DataType): +class ScaledInteger(HasUnit, DataType): """scaled integer (= fixed resolution float) type :param minval: (property **min**) @@ -344,7 +354,6 @@ class ScaledInteger(DataType): scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True) min = Property('low limit', FloatRange(), extname='min', mandatory=True) max = Property('high limit', FloatRange(), extname='max', mandatory=True) - unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='') fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') absolute_resolution = Property('absolute resolution', FloatRange(0), extname='absolute_resolution', default=0.0) @@ -806,6 +815,9 @@ class ArrayOf(DataType): except AttributeError: raise BadValueError('incompatible datatypes') from None + def set_main_unit(self, unit): + self.members.set_main_unit(unit) + class TupleOf(DataType): """data structure with fields of inhomogeneous type @@ -872,6 +884,10 @@ class TupleOf(DataType): for a, b in zip(self.members, other.members): a.compatible(b) + def set_main_unit(self, unit): + for member in self.members: + member.set_main_unit(unit) + class ImmutableDict(dict): def _no(self, *args, **kwds): @@ -961,6 +977,10 @@ class StructOf(DataType): except (AttributeError, TypeError, KeyError): raise BadValueError('incompatible datatypes') from None + def set_main_unit(self, unit): + for member in self.members.values(): + member.set_main_unit(unit) + class CommandType(DataType): """command diff --git a/secop/modules.py b/secop/modules.py index 3806302..6c48cb1 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -476,10 +476,10 @@ class Module(HasAccessibles): aobj.finish() # Modify units AFTER applying the cfgdict - for pname, pobj in self.parameters.items(): - dt = pobj.datatype - if '$' in dt.unit: - dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit)) + mainunit = self.parameters['value'].datatype.unit + if mainunit: + for pname, pobj in self.parameters.items(): + pobj.datatype.set_main_unit(mainunit) # 6) check complete configuration of * properties if not errors: From 554f265eb88329e27f130b2a4e53fe5d39ab5608 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Sep 2022 14:53:59 +0200 Subject: [PATCH 05/22] fix main value unit Change-Id: I30c6107fae31b5087bac75d66db6be8dee78a757 --- secop/modules.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/secop/modules.py b/secop/modules.py index 6c48cb1..d6321ec 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -476,10 +476,12 @@ class Module(HasAccessibles): aobj.finish() # Modify units AFTER applying the cfgdict - mainunit = self.parameters['value'].datatype.unit - if mainunit: - for pname, pobj in self.parameters.items(): - pobj.datatype.set_main_unit(mainunit) + mainvalue = self.parameters.get('value') + if mainvalue: + mainunit = ['value'].datatype.unit + if mainunit: + for pname, pobj in self.parameters.items(): + pobj.datatype.set_main_unit(mainunit) # 6) check complete configuration of * properties if not errors: From befba09acc9c8df19559e542fdfbdedfd21869d0 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Sep 2022 14:54:48 +0200 Subject: [PATCH 06/22] fix main value unit Change-Id: I27186e66bf065e015680a6925c5428714444c25e --- secop/modules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secop/modules.py b/secop/modules.py index d6321ec..827740b 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -478,7 +478,7 @@ class Module(HasAccessibles): # Modify units AFTER applying the cfgdict mainvalue = self.parameters.get('value') if mainvalue: - mainunit = ['value'].datatype.unit + mainunit = mainvalue.datatype.unit if mainunit: for pname, pobj in self.parameters.items(): pobj.datatype.set_main_unit(mainunit) From 0b9e227669cdf50d86e042bb3501254bc05ba159 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 16 Sep 2022 08:14:14 +0200 Subject: [PATCH 07/22] state after discussion with users - cirterium for "no substantial forece change" must be improved --- secop_psi/uniax.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/secop_psi/uniax.py b/secop_psi/uniax.py index 22443be..51a2182 100644 --- a/secop_psi/uniax.py +++ b/secop_psi/uniax.py @@ -52,6 +52,8 @@ class Uniax(PersistentMixin, Drivable): default=0.2, persistent='auto') low_pos = Parameter('max. position for positive forces', FloatRange(unit='deg'), readonly=False, needscfg=False) high_pos = Parameter('min. position for negative forces', FloatRange(unit='deg'), readonly=False, needscfg=False) + motor_play = Parameter('summed steps without substantial change', FloatRange(), default=0) + max_play = Parameter('max. summed steps without substantial change', FloatRange(), readonly=False, default=70) pollinterval = 0.1 fast_pollfactor = 1 @@ -64,7 +66,6 @@ class Uniax(PersistentMixin, Drivable): _action = None _last_force = 0 _expected_step = 1 - _fail_cnt = 0 _in_cnt = 0 _init_action = False _zero_pos_tol = None @@ -248,12 +249,13 @@ class Uniax(PersistentMixin, Drivable): if abs(target - force) < self.tolerance: self._in_cnt += 1 if self._in_cnt >= 3: + self.motor_play = 0 self.next_action(self.within_tolerance) return else: self._in_cnt = 0 if self.init_action(): - self._fail_cnt = 0 + self.motor_play = 0 self.write_adjusting(True) self.status = 'BUSY', 'adjusting force' elif not self._filtered: @@ -263,16 +265,20 @@ class Uniax(PersistentMixin, Drivable): if self._expected_step: # compare detected / expected step q = force_step / self._expected_step + mstep = self._expected_step * self.slope if q < 0.1: - self._fail_cnt += 1 + self.motor_play += mstep elif q > 0.5: - self._fail_cnt = max(0, self._fail_cnt - 1) - if self._fail_cnt >= 10: + if abs(self.motor_play) <= abs(mstep): + self.motor_play = 0 + else: + self.motor_play = self.motor_play * (1 - abs(mstep / self.motor_play)) + if abs(self.motor_play) >= 10: if force < self.hysteresis: self.log.warning('adjusting failed - try to find zero pos') self.set_zero_pos(target, None) self.next_action(self.find) - elif self._fail_cnt > 20: + elif abs(self.motor_play) > self.max_play: self.stop() self.status = 'ERROR', 'force seems not to change substantially' self.log.error(self.status[1]) From 3ab9821860c31718c940b45c96b6910cd32dec23 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 16 Sep 2022 08:06:24 +0200 Subject: [PATCH 08/22] improve formatting of values Change-Id: I4a9290e85ee2071a3f2cfe0d00bc7dc4dcb4caed --- secop/client/interactive.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/secop/client/interactive.py b/secop/client/interactive.py index 2eb6365..28d8b61 100644 --- a/secop/client/interactive.py +++ b/secop/client/interactive.py @@ -23,11 +23,11 @@ import sys import time -import json import re from queue import Queue from secop.client import SecopClient from secop.errors import SECoPError +from secop.datatypes import get_datatype USAGE = """ Usage: @@ -97,15 +97,12 @@ class Module: def _one_line(self, pname, minwid=0): """return . = truncated to one line""" + param = getattr(type(self), pname) try: value = getattr(self, pname) - # make floats appear with 7 digits only - r = repr(json.loads(json.dumps(value), parse_float=PrettyFloat)) + r = param.format(value) except Exception as e: r = repr(e) - unit = getattr(type(self), pname).unit - if unit: - r += ' %s' % unit pname = pname.ljust(minwid) vallen = 113 - len(self._name) - len(pname) if len(r) > vallen: @@ -192,11 +189,11 @@ class Module: class Param: - def __init__(self, name, unit=None): + def __init__(self, name, datainfo): self.name = name self.prev = None self.prev_time = 0 - self.unit = unit + self.datatype = get_datatype(datainfo) def __get__(self, obj, owner): if obj is None: @@ -214,6 +211,9 @@ class Param: except SECoPError as e: obj._secnode.log.error(repr(e)) + def format(self, value): + return self.datatype.format_value(value) + class Command: def __init__(self, name, modname, secnode): @@ -266,8 +266,7 @@ class Client(SecopClient): self.log.info('overwrite module %s', modname) attrs = {} for pname, pinfo in moddesc['parameters'].items(): - unit = pinfo['datainfo'].get('unit') - attrs[pname] = Param(pname, unit) + attrs[pname] = Param(pname, pinfo['datainfo']) for cname in moddesc['commands']: attrs[cname] = Command(cname, modname, self) mobj = type('M_%s' % modname, (Module,), attrs)(modname, self) From a8f1495bc8db0579e5c64cfea0015557dd9fa49e Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 16 Sep 2022 14:53:42 +0200 Subject: [PATCH 09/22] [WIP] uniax after changing to StateMachine Change-Id: I0173f8c8eaaeb2526477d05803a615673297667d --- secop_psi/uniax.py | 281 +++++++++++++++++++++------------------------ 1 file changed, 132 insertions(+), 149 deletions(-) diff --git a/secop_psi/uniax.py b/secop_psi/uniax.py index 51a2182..fa819f2 100644 --- a/secop_psi/uniax.py +++ b/secop_psi/uniax.py @@ -26,6 +26,11 @@ import math from secop.core import Drivable, Parameter, FloatRange, Done, \ Attached, Command, PersistentMixin, PersistentParam, BoolType from secop.errors import BadValueError +from secop.lib.statemachine import Retry, StateMachine, Restart + + +class Error(Exception): + pass class Uniax(PersistentMixin, Drivable): @@ -52,29 +57,33 @@ class Uniax(PersistentMixin, Drivable): default=0.2, persistent='auto') low_pos = Parameter('max. position for positive forces', FloatRange(unit='deg'), readonly=False, needscfg=False) high_pos = Parameter('min. position for negative forces', FloatRange(unit='deg'), readonly=False, needscfg=False) - motor_play = Parameter('summed steps without substantial change', FloatRange(), default=0) - max_play = Parameter('max. summed steps without substantial change', FloatRange(), readonly=False, default=70) + substantial_force = Parameter('min. force change expected within motor play', FloatRange(), default=0) + motor_play = Parameter('acceptable motor play within hysteresis', FloatRange(), readonly=False, default=10) + motor_max_play = Parameter('acceptable motor play outside hysteresis', FloatRange(), readonly=False, default=70) pollinterval = 0.1 - fast_pollfactor = 1 _mot_target = None # for detecting manual motor manipulations _filter_start = 0 + _filtered = False _cnt = 0 _sum = 0 _cnt_rderr = 0 _cnt_wrerr = 0 - _action = None - _last_force = 0 - _expected_step = 1 _in_cnt = 0 - _init_action = False _zero_pos_tol = None - _find_target = 0 + _state = None def earlyInit(self): super().earlyInit() self._zero_pos_tol = {} - self._action = self.idle + + def initModule(self): + super().initModule() + self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup) + + def doPoll(self): + self.read_value() + self._state.cycle() def drive_relative(self, step, ntry=3): """drive relative, try 3 times""" @@ -105,31 +114,14 @@ class Uniax(PersistentMixin, Drivable): mot = self.motor if mot.isBusy(): if mot.target != self._mot_target: - self.action = self.idle + raise Error('control stopped - motor moved directly') return True return False - def next_action(self, action): - """call next action - - :param action: function to be called next time - :param do_now: do next action in the same cycle - """ - self._action = action - self._init_action = True - self.log.info('action %r', action.__name__) - - def init_action(self): - """return true when called the first time after next_action""" - if self._init_action: - self._init_action = False - return True - return False - - def zero_pos(self, value,): + def zero_pos(self, value): """get high_pos or low_pos, depending on sign of value - :param force: when not 0, return an estimate for a good starting position + :param value: return an estimate for a good starting position """ name = 'high_pos' if value > 0 else 'low_pos' @@ -158,145 +150,141 @@ class Uniax(PersistentMixin, Drivable): setattr(self, name, pos) return pos - def find(self, force, target): + def do_find(self, state): """find active (engaged) range""" - sign = math.copysign(1, target) - if force * sign > self.hysteresis or force * sign > target * sign: + if state.init: + state.prev_direction = 0 # find not yet started + direction = math.copysign(1, self.target) + if self.value * direction > self.hysteresis or self.value * direction > self.target * direction: if self.motor_busy(): - self.log.info('motor stopped - substantial force detected: %g', force) + self.log.info('motor stopped - substantial force detected: %g', self.value) self.motor.stop() - elif self.init_action(): - self.next_action(self.adjust) - return - if abs(force) > self.hysteresis: - self.set_zero_pos(force, self.motor.read_value()) - self.next_action(self.adjust) - return - if force * sign < -self.hysteresis: - self._previous_force = force - self.next_action(self.free) - return + elif state.prev_direction == 0: + return self.do_adjust + if abs(self.value) > self.hysteresis: + self.set_zero_pos(self.value, self.motor.read_value()) + return self.do_adjust + if self.value * direction < -self.hysteresis: + state.force_before_free = self.value + return self.do_free if self.motor_busy(): - if sign * self._find_target < 0: # target sign changed + if direction == -state.prev_direction: # target direction changed self.motor.stop() - self.next_action(self.find) # restart find - return + state.init_find = True # restart find + return Retry() + zero_pos = self.zero_pos(self.target) + if state.prev_direction: # find already started + if abs(self.motor.target - self.motor.value) > self.motor.tolerance: + # no success on last find try, try short and strong step + self.write_adjusting(True) + self.log.info('one step to %g', self.motor.value + self.safe_step) + self.drive_relative(direction * self.safe_step) + return Retry() else: - self._find_target = target - zero_pos = self.zero_pos(target) - side_name = 'positive' if target > 0 else 'negative' - if not self.init_action(): - if abs(self.motor.target - self.motor.value) > self.motor.tolerance: - # no success on last find try, try short and strong step - self.write_adjusting(True) - self.log.info('one step to %g', self.motor.value + self.safe_step) - self.drive_relative(sign * self.safe_step) - return - if zero_pos is not None: - self.status = 'BUSY', 'change to %s side' % side_name - zero_pos += sign * (self.hysteresis * self.slope - self.motor.tolerance) - if (self.motor.value - zero_pos) * sign < -self.motor.tolerance: - self.write_adjusting(False) - self.log.info('change side to %g', zero_pos) - self.drive_relative(zero_pos - self.motor.value) - return - # we are already at or beyond zero_pos - self.next_action(self.adjust) - return - self.write_adjusting(False) - self.status = 'BUSY', 'find %s side' % side_name - self.log.info('one turn to %g', self.motor.value + sign * 360) - self.drive_relative(sign * 360) + state.prev_direction = math.copysign(1, self.target) + side_name = 'negative' if direction == -1 else 'positive' + if zero_pos is not None: + self.status = 'BUSY', 'change to %s side' % side_name + zero_pos += direction * (self.hysteresis * self.slope - self.motor.tolerance) + if (self.motor.value - zero_pos) * direction < -self.motor.tolerance: + self.write_adjusting(False) + self.log.info('change side to %g', zero_pos) + self.drive_relative(zero_pos - self.motor.value) + return Retry() + # we are already at or beyond zero_pos + return self.do_adjust + self.write_adjusting(False) + self.status = 'BUSY', 'find %s side' % side_name + self.log.info('one turn to %g', self.motor.value + direction * 360) + self.drive_relative(direction * 360) + return Retry() - def free(self, force, target): + def cleanup(self, state): + """in case of error, set error status""" + if state.stopped: # stop or restart + if state.stopped is Restart: + return + self.status = 'IDLE', 'stopped' + self.log.warning('stopped') + else: + self.status = 'ERROR', str(state.last_error) + if isinstance(state.last_error, Error): + self.log.error('%s', state.last_error) + else: + self.log.error('%r raised in state %r', str(state.last_error), state.status_string) + self.motor.stop() + self.write_adjusting(False) + + def do_free(self, state): """free from high force at other end""" + if state.init: + state.free_way = None if self.motor_busy(): - return - if abs(force) > abs(self._previous_force) + self.tolerance: + return Retry() + if abs(self.value) > abs(state.force_before_free) + self.tolerance: self.stop() self.status = 'ERROR', 'force increase while freeing' self.log.error(self.status[1]) - return - if abs(force) < self.hysteresis: - self.next_action(self.find) - return - if self.init_action(): - self._free_way = 0 - self.log.info('free from high force %g', force) + return None + if abs(self.value) < self.hysteresis: + return self.do_find + if state.free_way is None: + state.free_way = 0 + self.log.info('free from high force %g', self.value) self.write_adjusting(True) - sign = math.copysign(1, target) - if self._free_way > (abs(self._previous_force) + self.hysteresis) * self.slope: + direction = math.copysign(1, self.target) + if state.free_way > (abs(state.force_before_free) + self.hysteresis) * self.slope: self.stop() self.status = 'ERROR', 'freeing failed' self.log.error(self.status[1]) - return - self._free_way += self.safe_step - self.drive_relative(sign * self.safe_step) + return None + state.free_way += self.safe_step + self.drive_relative(direction * self.safe_step) + return Retry() - def within_tolerance(self, force, target): + def do_within_tolerance(self, state): """within tolerance""" if self.motor_busy(): - return - if abs(target - force) > self.tolerance: - self.next_action(self.adjust) - elif self.init_action(): - self.status = 'IDLE', 'within tolerance' + return Retry() + if abs(self.target - self.value) > self.tolerance: + return self.do_adjust + self.status = 'IDLE', 'within tolerance' - def adjust(self, force, target): + def do_adjust(self, state): """adjust force""" + if state.init: + state.prev_force = None if self.motor_busy(): return - if abs(target - force) < self.tolerance: + if abs(self.target - self.value) < self.tolerance: self._in_cnt += 1 if self._in_cnt >= 3: - self.motor_play = 0 - self.next_action(self.within_tolerance) - return + return self.do_within_tolerance else: self._in_cnt = 0 - if self.init_action(): - self.motor_play = 0 + if state.prev_force is None: + state.prev_force = self.value + state.prev_pos = self.motor.pos self.write_adjusting(True) self.status = 'BUSY', 'adjusting force' elif not self._filtered: return else: - force_step = force - self._last_force - if self._expected_step: - # compare detected / expected step - q = force_step / self._expected_step - mstep = self._expected_step * self.slope - if q < 0.1: - self.motor_play += mstep - elif q > 0.5: - if abs(self.motor_play) <= abs(mstep): - self.motor_play = 0 - else: - self.motor_play = self.motor_play * (1 - abs(mstep / self.motor_play)) - if abs(self.motor_play) >= 10: - if force < self.hysteresis: + if abs(self.value - state.prev_force) > self.substantial_force: + state.prev_force = self.value + state.prev_pos = self.motor.value + else: + motor_dif = abs(self.value - state.prev_pos) + if abs(self.value) < self.hysteresis: + if motor_dif > self.motor_play: self.log.warning('adjusting failed - try to find zero pos') - self.set_zero_pos(target, None) - self.next_action(self.find) - elif abs(self.motor_play) > self.max_play: - self.stop() - self.status = 'ERROR', 'force seems not to change substantially' - self.log.error(self.status[1]) - return - self._last_force = force - force_step = (target - force) * self.pid_i - if abs(target - force) < self.tolerance * 0.5: - self._expected_step = 0 - return - self._expected_step = force_step - step = force_step * self.slope - self.drive_relative(step) - - def idle(self, *args): - if self.init_action(): - self.write_adjusting(False) - if self.status[0] == 'BUSY': - self.status = 'IDLE', 'stopped' + self.set_zero_pos(self.target, None) + return self.do_find + elif motor_dif > self.motor_max_play: + raise Error('force seems not to change substantially') + force_step = (self.target - self.value) * self.pid_i + self.drive_relative(force_step * self.slope) + return Retry() def read_value(self): try: @@ -313,7 +301,6 @@ class Uniax(PersistentMixin, Drivable): now = time.time() if self.motor_busy(): # do not filter while driving - self.value = force self.reset_filter() self._filtered = False else: @@ -322,46 +309,42 @@ class Uniax(PersistentMixin, Drivable): if now < self._filter_start + self.filter_interval: return Done force = self._sum / self._cnt - self.value = force self.reset_filter(now) self._filtered = True if abs(force) > self.limit + self.hysteresis: + self.motor.stop() self.status = 'ERROR', 'above max limit' self.log.error(self.status[1]) return Done if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered: self.set_zero_pos(force, self.motor.read_value()) - self._action(self.value, self.target) - return Done + return force def write_target(self, target): if abs(target) > self.limit: raise BadValueError('force above limit') if abs(target - self.value) <= self.tolerance: - if self.isBusy(): - self.stop() - self.next_action(self.within_tolerance) - else: + if not self.isBusy(): self.status = 'IDLE', 'already at target' - self.next_action(self.within_tolerance) - return target + self._state.start(self.do_within_tolerance) + return target self.log.info('new target %g', target) self._cnt_rderr = 0 self._cnt_wrerr = 0 self.status = 'BUSY', 'changed target' if self.value * math.copysign(1, target) > self.hysteresis: - self.next_action(self.adjust) + self._state.start(self.do_adjust) else: - self.next_action(self.find) + self._state.start(self.do_find) return target @Command() def stop(self): - self._action = self.idle if self.motor.isBusy(): self.log.info('stop motor') self.motor.stop() - self.next_action(self.idle) + self.status = 'IDLE', 'stopped' + self._state.stop() def write_force_offset(self, value): self.force_offset = value From 486be9604e9640ca64ca0f27ff786cce1811f7b4 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 16 Sep 2022 14:57:58 +0200 Subject: [PATCH 10/22] [WIP] uniax: removed do_ prefix Change-Id: I98d56a8ece681515de8f05767c67686715212c09 --- secop_psi/uniax.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/secop_psi/uniax.py b/secop_psi/uniax.py index fa819f2..095c1d2 100644 --- a/secop_psi/uniax.py +++ b/secop_psi/uniax.py @@ -150,7 +150,7 @@ class Uniax(PersistentMixin, Drivable): setattr(self, name, pos) return pos - def do_find(self, state): + def find(self, state): """find active (engaged) range""" if state.init: state.prev_direction = 0 # find not yet started @@ -160,13 +160,13 @@ class Uniax(PersistentMixin, Drivable): self.log.info('motor stopped - substantial force detected: %g', self.value) self.motor.stop() elif state.prev_direction == 0: - return self.do_adjust + return self.adjust if abs(self.value) > self.hysteresis: self.set_zero_pos(self.value, self.motor.read_value()) - return self.do_adjust + return self.adjust if self.value * direction < -self.hysteresis: state.force_before_free = self.value - return self.do_free + return self.free if self.motor_busy(): if direction == -state.prev_direction: # target direction changed self.motor.stop() @@ -192,7 +192,7 @@ class Uniax(PersistentMixin, Drivable): self.drive_relative(zero_pos - self.motor.value) return Retry() # we are already at or beyond zero_pos - return self.do_adjust + return self.adjust self.write_adjusting(False) self.status = 'BUSY', 'find %s side' % side_name self.log.info('one turn to %g', self.motor.value + direction * 360) @@ -215,7 +215,7 @@ class Uniax(PersistentMixin, Drivable): self.motor.stop() self.write_adjusting(False) - def do_free(self, state): + def free(self, state): """free from high force at other end""" if state.init: state.free_way = None @@ -227,7 +227,7 @@ class Uniax(PersistentMixin, Drivable): self.log.error(self.status[1]) return None if abs(self.value) < self.hysteresis: - return self.do_find + return self.find if state.free_way is None: state.free_way = 0 self.log.info('free from high force %g', self.value) @@ -242,15 +242,15 @@ class Uniax(PersistentMixin, Drivable): self.drive_relative(direction * self.safe_step) return Retry() - def do_within_tolerance(self, state): + def within_tolerance(self, state): """within tolerance""" if self.motor_busy(): return Retry() if abs(self.target - self.value) > self.tolerance: - return self.do_adjust + return self.adjust self.status = 'IDLE', 'within tolerance' - def do_adjust(self, state): + def adjust(self, state): """adjust force""" if state.init: state.prev_force = None @@ -259,7 +259,7 @@ class Uniax(PersistentMixin, Drivable): if abs(self.target - self.value) < self.tolerance: self._in_cnt += 1 if self._in_cnt >= 3: - return self.do_within_tolerance + return self.within_tolerance else: self._in_cnt = 0 if state.prev_force is None: @@ -279,7 +279,7 @@ class Uniax(PersistentMixin, Drivable): if motor_dif > self.motor_play: self.log.warning('adjusting failed - try to find zero pos') self.set_zero_pos(self.target, None) - return self.do_find + return self.find elif motor_dif > self.motor_max_play: raise Error('force seems not to change substantially') force_step = (self.target - self.value) * self.pid_i @@ -326,16 +326,16 @@ class Uniax(PersistentMixin, Drivable): if abs(target - self.value) <= self.tolerance: if not self.isBusy(): self.status = 'IDLE', 'already at target' - self._state.start(self.do_within_tolerance) + self._state.start(self.within_tolerance) return target self.log.info('new target %g', target) self._cnt_rderr = 0 self._cnt_wrerr = 0 self.status = 'BUSY', 'changed target' if self.value * math.copysign(1, target) > self.hysteresis: - self._state.start(self.do_adjust) + self._state.start(self.adjust) else: - self._state.start(self.do_find) + self._state.start(self.find) return target @Command() From 14a71427fa3686c5945076d06a8a78210edff904 Mon Sep 17 00:00:00 2001 From: zebra Date: Mon, 19 Sep 2022 11:37:56 +0200 Subject: [PATCH 11/22] jtccr: fix rel_paths --- cfg/main/jtccr.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/cfg/main/jtccr.cfg b/cfg/main/jtccr.cfg index baf8a1e..ddf2631 100644 --- a/cfg/main/jtccr.cfg +++ b/cfg/main/jtccr.cfg @@ -12,6 +12,7 @@ service = main class = secop_psi.sea.SeaDrivable io = sea_main sea_object = tt +rel_paths = . tt [T_ccr] class = secop_psi.sea.SeaReadable From 54f091d0fe38dc52bdaf385028709f6db193e483 Mon Sep 17 00:00:00 2001 From: zebra Date: Mon, 19 Sep 2022 11:38:33 +0200 Subject: [PATCH 12/22] disable encoder for ma6 stick motor --- cfg/main/ma6.cfg | 2 +- cfg/sea/ma6.config.json | 115 +++++++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 51 deletions(-) diff --git a/cfg/main/ma6.cfg b/cfg/main/ma6.cfg index 7d61d60..9cd10b9 100644 --- a/cfg/main/ma6.cfg +++ b/cfg/main/ma6.cfg @@ -63,5 +63,5 @@ uri = ma6-ts.psi.ch:3003 description = stick rotation, typically used for omega class = secop_psi.phytron.Motor io = om_io -encoder_mode = CHECK +encoder_mode = NO diff --git a/cfg/sea/ma6.config.json b/cfg/sea/ma6.config.json index 0ebe15e..d2ca2a3 100644 --- a/cfg/sea/ma6.config.json +++ b/cfg/sea/ma6.config.json @@ -1,50 +1,50 @@ {"tt": {"base": "/tt", "params": [ {"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 18}, {"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3}, -{"path": "status", "type": "text", "visibility": 3}, +{"path": "status", "type": "text", "readonly": false, "cmd": "run tt", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3}, {"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3}, -{"path": "target", "type": "float"}, -{"path": "running", "type": "int"}, +{"path": "target", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "running", "type": "int", "readonly": false, "cmd": "run tt"}, {"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"}, {"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"}, {"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"}, {"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4}, -{"path": "log/mean", "type": "float", "visibility": 3}, -{"path": "log/m2", "type": "float", "visibility": 3}, -{"path": "log/stddev", "type": "float", "visibility": 3}, -{"path": "log/n", "type": "float", "visibility": 3}, +{"path": "log/mean", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "log/m2", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "log/stddev", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "log/n", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, {"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "kids": 9}, {"path": "dblctrl/tshift", "type": "float", "readonly": false, "cmd": "tt dblctrl/tshift"}, {"path": "dblctrl/mode", "type": "enum", "enum": {"disabled": -1, "inactive": 0, "stable": 1, "up": 2, "down": 3}, "readonly": false, "cmd": "tt dblctrl/mode"}, -{"path": "dblctrl/shift_up", "type": "float"}, -{"path": "dblctrl/shift_lo", "type": "float"}, -{"path": "dblctrl/t_min", "type": "float"}, -{"path": "dblctrl/t_max", "type": "float"}, +{"path": "dblctrl/shift_up", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "dblctrl/shift_lo", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "dblctrl/t_min", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "dblctrl/t_max", "type": "float", "readonly": false, "cmd": "run tt"}, {"path": "dblctrl/int2", "type": "float", "readonly": false, "cmd": "tt dblctrl/int2"}, {"path": "dblctrl/prop_up", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_up"}, {"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo"}, -{"path": "tm", "type": "float", "kids": 4}, +{"path": "tm", "type": "float", "readonly": false, "cmd": "run tt", "kids": 4}, {"path": "tm/curve", "type": "text", "readonly": false, "cmd": "tt tm/curve", "kids": 1}, {"path": "tm/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt tm/curve/points", "visibility": 3}, {"path": "tm/alarm", "type": "float", "readonly": false, "cmd": "tt tm/alarm"}, -{"path": "tm/stddev", "type": "float"}, -{"path": "tm/raw", "type": "float"}, -{"path": "ts", "type": "float", "kids": 4}, +{"path": "tm/stddev", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "tm/raw", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "ts", "type": "float", "readonly": false, "cmd": "run tt", "kids": 4}, {"path": "ts/curve", "type": "text", "readonly": false, "cmd": "tt ts/curve", "kids": 1}, {"path": "ts/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts/curve/points", "visibility": 3}, {"path": "ts/alarm", "type": "float", "readonly": false, "cmd": "tt ts/alarm"}, -{"path": "ts/stddev", "type": "float"}, -{"path": "ts/raw", "type": "float"}, -{"path": "ts_2", "type": "float", "kids": 4}, +{"path": "ts/stddev", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "ts/raw", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "ts_2", "type": "float", "readonly": false, "cmd": "run tt", "kids": 4}, {"path": "ts_2/curve", "type": "text", "readonly": false, "cmd": "tt ts_2/curve", "kids": 1}, {"path": "ts_2/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts_2/curve/points", "visibility": 3}, {"path": "ts_2/alarm", "type": "float", "readonly": false, "cmd": "tt ts_2/alarm"}, -{"path": "ts_2/stddev", "type": "float"}, -{"path": "ts_2/raw", "type": "float"}, +{"path": "ts_2/stddev", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "ts_2/raw", "type": "float", "readonly": false, "cmd": "run tt"}, {"path": "set", "type": "float", "readonly": false, "cmd": "tt set", "kids": 18}, {"path": "set/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt set/mode"}, -{"path": "set/reg", "type": "float"}, +{"path": "set/reg", "type": "float", "readonly": false, "cmd": "run tt"}, {"path": "set/ramp", "type": "float", "readonly": false, "cmd": "tt set/ramp", "description": "maximum ramp in K/min (0: ramp off)"}, {"path": "set/wramp", "type": "float", "readonly": false, "cmd": "tt set/wramp"}, {"path": "set/smooth", "type": "float", "readonly": false, "cmd": "tt set/smooth", "description": "smooth time (minutes)"}, @@ -53,17 +53,17 @@ {"path": "set/resist", "type": "float", "readonly": false, "cmd": "tt set/resist"}, {"path": "set/maxheater", "type": "text", "readonly": false, "cmd": "tt set/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"}, {"path": "set/linearpower", "type": "float", "readonly": false, "cmd": "tt set/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"}, -{"path": "set/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"}, +{"path": "set/maxpowerlim", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum power limit (before any booster or converter)"}, {"path": "set/maxpower", "type": "float", "readonly": false, "cmd": "tt set/maxpower", "description": "maximum power [W]"}, -{"path": "set/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"}, +{"path": "set/maxcurrent", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum current before any booster or converter"}, {"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"}, -{"path": "set/power", "type": "float"}, +{"path": "set/power", "type": "float", "readonly": false, "cmd": "run tt"}, {"path": "set/prop", "type": "float", "readonly": false, "cmd": "tt set/prop", "description": "bigger means more gain"}, {"path": "set/integ", "type": "float", "readonly": false, "cmd": "tt set/integ", "description": "bigger means faster"}, {"path": "set/deriv", "type": "float", "readonly": false, "cmd": "tt set/deriv"}, {"path": "setsamp", "type": "float", "readonly": false, "cmd": "tt setsamp", "kids": 18}, {"path": "setsamp/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt setsamp/mode"}, -{"path": "setsamp/reg", "type": "float"}, +{"path": "setsamp/reg", "type": "float", "readonly": false, "cmd": "run tt"}, {"path": "setsamp/ramp", "type": "float", "readonly": false, "cmd": "tt setsamp/ramp", "description": "maximum ramp in K/min (0: ramp off)"}, {"path": "setsamp/wramp", "type": "float", "readonly": false, "cmd": "tt setsamp/wramp"}, {"path": "setsamp/smooth", "type": "float", "readonly": false, "cmd": "tt setsamp/smooth", "description": "smooth time (minutes)"}, @@ -72,16 +72,16 @@ {"path": "setsamp/resist", "type": "float", "readonly": false, "cmd": "tt setsamp/resist"}, {"path": "setsamp/maxheater", "type": "text", "readonly": false, "cmd": "tt setsamp/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"}, {"path": "setsamp/linearpower", "type": "float", "readonly": false, "cmd": "tt setsamp/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"}, -{"path": "setsamp/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"}, +{"path": "setsamp/maxpowerlim", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum power limit (before any booster or converter)"}, {"path": "setsamp/maxpower", "type": "float", "readonly": false, "cmd": "tt setsamp/maxpower", "description": "maximum power [W]"}, -{"path": "setsamp/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"}, +{"path": "setsamp/maxcurrent", "type": "float", "readonly": false, "cmd": "run tt", "description": "the maximum current before any booster or converter"}, {"path": "setsamp/manualpower", "type": "float", "readonly": false, "cmd": "tt setsamp/manualpower"}, -{"path": "setsamp/power", "type": "float"}, +{"path": "setsamp/power", "type": "float", "readonly": false, "cmd": "run tt"}, {"path": "setsamp/prop", "type": "float", "readonly": false, "cmd": "tt setsamp/prop", "description": "bigger means more gain"}, {"path": "setsamp/integ", "type": "float", "readonly": false, "cmd": "tt setsamp/integ", "description": "bigger means faster"}, {"path": "setsamp/deriv", "type": "float", "readonly": false, "cmd": "tt setsamp/deriv"}, {"path": "display", "type": "text", "readonly": false, "cmd": "tt display"}, -{"path": "remote", "type": "bool"}]}, +{"path": "remote", "type": "bool", "readonly": false, "cmd": "run tt"}]}, "cc": {"base": "/cc", "params": [ {"path": "", "type": "bool", "kids": 96}, @@ -239,16 +239,16 @@ {"path": "", "type": "enum", "enum": {"xds35_auto": 0, "xds35_manual": 1, "sv65": 2, "other": 3, "no": -1}, "readonly": false, "cmd": "hepump", "description": "xds35: scroll pump, sv65: leybold", "kids": 10}, {"path": "send", "type": "text", "readonly": false, "cmd": "hepump send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, -{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running", "visibility": 3}, -{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco", "visibility": 3}, -{"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto", "visibility": 3}, -{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve", "visibility": 3}, -{"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2", "visibility": 3}, +{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running"}, +{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco"}, +{"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto"}, +{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve"}, +{"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2"}, {"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3}, {"path": "health", "type": "float"}]}, "hemot": {"base": "/hepump/hemot", "params": [ -{"path": "", "type": "float", "readonly": false, "cmd": "run hemot", "visibility": 3, "kids": 30}, +{"path": "", "type": "float", "readonly": false, "cmd": "run hemot", "kids": 30}, {"path": "send", "type": "text", "readonly": false, "cmd": "hemot send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "hemot is_running", "visibility": 3}, @@ -280,6 +280,16 @@ {"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"}, {"path": "custompar", "type": "float", "readonly": false, "cmd": "hemot custompar"}]}, +"nvflow": {"base": "/nvflow", "params": [ +{"path": "", "type": "float", "kids": 7}, +{"path": "send", "type": "text", "readonly": false, "cmd": "nvflow send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "stddev", "type": "float"}, +{"path": "nsamples", "type": "int", "readonly": false, "cmd": "nvflow nsamples"}, +{"path": "offset", "type": "float", "readonly": false, "cmd": "nvflow offset"}, +{"path": "scale", "type": "float", "readonly": false, "cmd": "nvflow scale"}, +{"path": "save", "type": "bool", "readonly": false, "cmd": "nvflow save", "description": "unchecked: current calib is not saved. set checked: save calib"}]}, + "ln2fill": {"base": "/ln2fill", "params": [ {"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2}, "readonly": false, "cmd": "ln2fill", "kids": 14}, {"path": "send", "type": "text", "readonly": false, "cmd": "ln2fill send", "visibility": 3}, @@ -317,32 +327,32 @@ {"path": "vext", "type": "float"}]}, "mf": {"base": "/mf", "params": [ -{"path": "", "type": "float", "kids": 26}, +{"path": "", "type": "float", "readonly": false, "cmd": "run mf", "kids": 26}, {"path": "persmode", "type": "int", "readonly": false, "cmd": "mf persmode"}, -{"path": "perswitch", "type": "int"}, +{"path": "perswitch", "type": "int", "readonly": false, "cmd": "run mf"}, {"path": "nowait", "type": "int", "readonly": false, "cmd": "mf nowait"}, -{"path": "maxlimit", "type": "float", "visibility": 3}, +{"path": "maxlimit", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3}, {"path": "limit", "type": "float", "readonly": false, "cmd": "mf limit"}, {"path": "ramp", "type": "float", "readonly": false, "cmd": "mf ramp"}, {"path": "perscurrent", "type": "float", "readonly": false, "cmd": "mf perscurrent"}, {"path": "perslimit", "type": "float", "readonly": false, "cmd": "mf perslimit"}, {"path": "perswait", "type": "int", "readonly": false, "cmd": "mf perswait"}, {"path": "persdelay", "type": "int", "readonly": false, "cmd": "mf persdelay"}, -{"path": "current", "type": "float"}, -{"path": "measured", "type": "float"}, -{"path": "voltage", "type": "float"}, -{"path": "lastfield", "type": "float", "visibility": 3}, -{"path": "ampRamp", "type": "float", "visibility": 3}, -{"path": "inductance", "type": "float", "visibility": 3}, +{"path": "current", "type": "float", "readonly": false, "cmd": "run mf"}, +{"path": "measured", "type": "float", "readonly": false, "cmd": "run mf"}, +{"path": "voltage", "type": "float", "readonly": false, "cmd": "run mf"}, +{"path": "lastfield", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3}, +{"path": "ampRamp", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3}, +{"path": "inductance", "type": "float", "readonly": false, "cmd": "run mf", "visibility": 3}, {"path": "trainedTo", "type": "float", "readonly": false, "cmd": "mf trainedTo"}, -{"path": "trainMode", "type": "int"}, +{"path": "trainMode", "type": "int", "readonly": false, "cmd": "run mf"}, {"path": "external", "type": "int", "readonly": false, "cmd": "mf external"}, {"path": "startScript", "type": "text", "readonly": false, "cmd": "mf startScript", "visibility": 3}, -{"path": "is_running", "type": "int", "visibility": 3}, +{"path": "is_running", "type": "int", "readonly": false, "cmd": "run mf", "visibility": 3}, {"path": "verbose", "type": "int", "readonly": false, "cmd": "mf verbose", "visibility": 3}, -{"path": "driver", "type": "text", "visibility": 3}, -{"path": "creationCmd", "type": "text", "visibility": 3}, -{"path": "targetValue", "type": "float"}, +{"path": "driver", "type": "text", "readonly": false, "cmd": "run mf", "visibility": 3}, +{"path": "creationCmd", "type": "text", "readonly": false, "cmd": "run mf", "visibility": 3}, +{"path": "targetValue", "type": "float", "readonly": false, "cmd": "run mf"}, {"path": "status", "type": "text", "readonly": false, "cmd": "mf status", "visibility": 3}]}, "lev": {"base": "/lev", "params": [ @@ -350,4 +360,9 @@ {"path": "send", "type": "text", "readonly": false, "cmd": "lev send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, {"path": "mode", "type": "enum", "enum": {"slow": 0, "fast (switches to slow automatically after filling)": 1}, "readonly": false, "cmd": "lev mode"}, -{"path": "n2", "type": "float"}]}} +{"path": "n2", "type": "float"}]}, + +"prep0": {"base": "/prep0", "params": [ +{"path": "", "type": "text", "readonly": false, "cmd": "prep0", "kids": 2}, +{"path": "send", "type": "text", "readonly": false, "cmd": "prep0 send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}]}} From 07995e22353923ec47cc40747dfb957f28f11fa2 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 20 Sep 2022 10:31:43 +0200 Subject: [PATCH 13/22] fix bug when restarting statemachine fixed bad if clause + better debug message on restart/stop --- secop/lib/statemachine.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/secop/lib/statemachine.py b/secop/lib/statemachine.py index 309cabf..9fe6271 100644 --- a/secop/lib/statemachine.py +++ b/secop/lib/statemachine.py @@ -132,7 +132,8 @@ class StateMachine: :return: None (for custom cleanup functions this might be a new state) """ if state.stopped: # stop or restart - state.log.debug('%sed in state %r', repr(state.stopped).lower(), state.status_string) + verb = 'stopped' if state.stopped is Stop else 'restarted' + state.log.debug('%s in state %r', verb, state.status_string) else: state.log.warning('%r raised in state %r', state.last_error, state.status_string) @@ -196,7 +197,7 @@ class StateMachine: self.log.debug('called %r %sexc=%r', self.cleanup, 'ret=%r ' % ret if ret else '', e) if ret is None: - self.log.debug('state: None') + self.log.debug('state: None after cleanup') self.state = None self._idle_event.set() return None @@ -270,8 +271,8 @@ class StateMachine: if self.stopped: # cleanup is not yet done self.last_error = self.stopped self.cleanup(self) # ignore return state on restart - self.stopped = False - self._start(state, **kwds) + self.stopped = False + self._start(state, **kwds) else: self._start(state, **kwds) From 5e3bf96df18e631237eded97314bf053e9ec7558 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 20 Sep 2022 11:07:44 +0200 Subject: [PATCH 14/22] rework of uniax - try to deal with anomaly at 17 N - reduce pid on overshoot - use new statemachine --- cfg/uniax.cfg | 7 +- secop_psi/uniax.py | 309 +++++++++++++++++++++++++++------------------ 2 files changed, 193 insertions(+), 123 deletions(-) diff --git a/cfg/uniax.cfg b/cfg/uniax.cfg index c1cbf18..1d8d755 100644 --- a/cfg/uniax.cfg +++ b/cfg/uniax.cfg @@ -42,10 +42,15 @@ digits = 2 scale_factor = 0.0156 offset = 15 +[res_io] +description = io to lakeshore +class = secop_psi.ls340res.LscIO +uri = tcp://192.168.127.254:3003 + [res] description = temperature on uniax stick class = secop_psi.ls340res.ResChannel -uri = tcp://192.168.127.254:3003 +io = res_io channel = A [T] diff --git a/secop_psi/uniax.py b/secop_psi/uniax.py index 095c1d2..e4e0813 100644 --- a/secop_psi/uniax.py +++ b/secop_psi/uniax.py @@ -25,11 +25,11 @@ import time import math from secop.core import Drivable, Parameter, FloatRange, Done, \ Attached, Command, PersistentMixin, PersistentParam, BoolType -from secop.errors import BadValueError +from secop.errors import BadValueError, SECoPError from secop.lib.statemachine import Retry, StateMachine, Restart -class Error(Exception): +class Error(SECoPError): pass @@ -38,11 +38,11 @@ class Uniax(PersistentMixin, Drivable): motor = Attached() transducer = Attached() limit = Parameter('abs limit of force', FloatRange(0, 190, unit='N'), readonly=False, default=150) - tolerance = Parameter('force tolerance', FloatRange(0, 10, unit='N'), readonly=False, default=0.1) + tolerance = Parameter('force tolerance', FloatRange(0, 10, unit='N'), readonly=False, default=0.2) slope = PersistentParam('spring constant', FloatRange(unit='deg/N'), readonly=False, default=0.5, persistent='auto') pid_i = PersistentParam('integral', FloatRange(), readonly=False, default=0.5, persistent='auto') - filter_interval = Parameter('filter time', FloatRange(0, 60, unit='s'), readonly=False, default=1) + filter_interval = Parameter('filter time', FloatRange(0, 60, unit='s'), readonly=False, default=5) current_step = Parameter('', FloatRange(unit='deg'), default=0) force_offset = PersistentParam('transducer offset', FloatRange(unit='N'), readonly=False, default=0, initwrite=True, persistent='auto') @@ -57,21 +57,21 @@ class Uniax(PersistentMixin, Drivable): default=0.2, persistent='auto') low_pos = Parameter('max. position for positive forces', FloatRange(unit='deg'), readonly=False, needscfg=False) high_pos = Parameter('min. position for negative forces', FloatRange(unit='deg'), readonly=False, needscfg=False) - substantial_force = Parameter('min. force change expected within motor play', FloatRange(), default=0) + substantial_force = Parameter('min. force change expected within motor play', FloatRange(), default=1) motor_play = Parameter('acceptable motor play within hysteresis', FloatRange(), readonly=False, default=10) - motor_max_play = Parameter('acceptable motor play outside hysteresis', FloatRange(), readonly=False, default=70) + motor_max_play = Parameter('acceptable motor play outside hysteresis', FloatRange(), readonly=False, default=90) + timeout = Parameter('driving finishes when no progress within this delay', FloatRange(), readonly=False, default=300) pollinterval = 0.1 _mot_target = None # for detecting manual motor manipulations _filter_start = 0 - _filtered = False _cnt = 0 _sum = 0 _cnt_rderr = 0 _cnt_wrerr = 0 - _in_cnt = 0 _zero_pos_tol = None _state = None + _force = None # raw force def earlyInit(self): super().earlyInit() @@ -94,7 +94,12 @@ class Uniax(PersistentMixin, Drivable): self.current_step = step for _ in range(ntry): try: - self._mot_target = self.motor.write_target(mot.value + step) + if abs(mot.value - mot.target) < mot.tolerance: + # make sure rounding erros do not suppress small steps + newpos = mot.target + step + else: + newpos = mot.value + step + self._mot_target = self.motor.write_target(newpos) self._cnt_wrerr = max(0, self._cnt_wrerr - 1) return True except Exception as e: @@ -106,10 +111,6 @@ class Uniax(PersistentMixin, Drivable): self.motor.reset() return False - def reset_filter(self, now=0.0): - self._sum = self._cnt = 0 - self._filter_start = now or time.time() - def motor_busy(self): mot = self.motor if mot.isBusy(): @@ -118,6 +119,40 @@ class Uniax(PersistentMixin, Drivable): return True return False + def read_value(self): + try: + self._force = force = self.transducer.read_value() + self._cnt_rderr = max(0, self._cnt_rderr - 1) + except Exception as e: + self._cnt_rderr += 1 + if self._cnt_rderr > 10: + self.stop() + self.status = 'ERROR', 'too many read errors: %s' % e + self.log.error(self.status[1]) + self.read_target() + return Done + + now = time.time() + self._sum += force + self._cnt += 1 + if now < self._filter_start + self.filter_interval: + return Done + force = self._sum / self._cnt + self.reset_filter(now) + if abs(force) > self.limit + self.hysteresis: + self.motor.stop() + self.status = 'ERROR', 'above max limit' + self.log.error(self.status[1]) + self.read_target() + return Done + if self.zero_pos(force) is None and abs(force) > self.hysteresis: + self.set_zero_pos(force, self.motor.read_value()) + return force + + def reset_filter(self, now=0.0): + self._sum = self._cnt = 0 + self._filter_start = now or time.time() + def zero_pos(self, value): """get high_pos or low_pos, depending on sign of value @@ -148,23 +183,143 @@ class Uniax(PersistentMixin, Drivable): self._zero_pos_tol[name] = tol self.log.info('set %s = %.1f +- %.1f (@%g N)' % (name, pos, tol, force)) setattr(self, name, pos) - return pos + + def cleanup(self, state): + """in case of error, set error status""" + if state.stopped: # stop or restart + if state.stopped is Restart: + return + self.status = 'IDLE', 'stopped' + self.log.warning('stopped') + else: + self.status = 'ERROR', str(state.last_error) + if isinstance(state.last_error, Error): + self.log.error('%s', state.last_error) + else: + self.log.error('%r raised in state %r', str(state.last_error), state.status_string) + self.read_target() # make target invalid + self.motor.stop() + self.write_adjusting(False) + + def reset_progress(self, state): + state.prev_force = self.value + state.prev_pos = self.motor.value + state.prev_time = time.time() + + def check_progress(self, state): + force_step = self.target - self.value + direction = math.copysign(1, force_step) + try: + force_progress = direction * (self.value - state.prev_force) + except AttributeError: # prev_force undefined? + self.reset_progress(state) + return True + if force_progress >= self.substantial_force: + self.reset_progress(state) + else: + motor_dif = abs(self.motor.value - state.prev_pos) + if motor_dif > self.motor_play: + if motor_dif > self.motor_max_play: + raise Error('force seems not to change substantially %g %g (%g %g)' % (self.value, self.motor.value, state.prev_force, state.prev_pos)) + return False + return True + + def adjust(self, state): + """adjust force""" + if state.init: + state.phase = 0 # just initialized + state.in_since = 0 + state.direction = math.copysign(1, self.target - self.value) + state.pid_fact = 1 + if self.motor_busy(): + return Retry() + self.value = self._force + force_step = self.target - self.value + if abs(force_step) < self.tolerance: + if state.in_since == 0: + state.in_since = state.now + if state.now > state.in_since + 10: + return self.within_tolerance + else: + if force_step * state.direction < 0: + if state.pid_fact == 1: + self.log.info('overshoot -> adjust with reduced pid_i') + state.pid_fact = 0.1 + state.in_since = 0 + if state.phase == 0: + state.phase = 1 + self.reset_progress(state) + self.write_adjusting(True) + self.status = 'BUSY', 'adjusting force' + elif not self.check_progress(state): + if abs(self.value) < self.hysteresis: + if motor_dif > self.motor_play: + self.log.warning('adjusting failed - try to find zero pos') + self.set_zero_pos(self.target, None) + return self.find + elif time.time() > state.prev_time + self.timeout: + if state.phase == 1: + state.phase = 2 + self.log.warning('no substantial progress since %d sec', self.timeout) + self.status = 'IDLE', 'adjusting timeout' + self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * state.pid_fact) + return Retry() + + def within_tolerance(self, state): + """within tolerance""" + if state.init: + self.status = 'IDLE', 'within tolerance' + return Retry() + if self.motor_busy(): + return Retry() + force_step = self.target - self.value + if abs(force_step) < self.tolerance * 0.5: + self.current_step = 0 + else: + self.check_progress(state) + self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * 0.1) + if abs(force_step) > self.tolerance: + return self.out_of_tolerance + return Retry() + + def out_of_tolerance(self, state): + """out of tolerance""" + if state.init: + self.status = 'WARN', 'out of tolerance' + state.in_since = 0 + return Retry() + if self.motor_busy(): + return Retry() + force_step = self.target - self._force + if abs(force_step) < self.tolerance: + if state.in_since == 0: + state.in_since = state.now + if state.now > state.in_since + 10: + return self.within_tolerance + if abs(force_step) < self.tolerance * 0.5: + return Retry() + self.check_progress(state) + self.drive_relative(force_step * self.slope * self.pid_i * min(1, state.delta()) * 0.1) + return Retry() def find(self, state): """find active (engaged) range""" if state.init: state.prev_direction = 0 # find not yet started + self.reset_progress(state) direction = math.copysign(1, self.target) - if self.value * direction > self.hysteresis or self.value * direction > self.target * direction: + self.value = self._force + abs_force = self.value * direction + if abs_force > self.hysteresis or abs_force > self.target * direction: if self.motor_busy(): self.log.info('motor stopped - substantial force detected: %g', self.value) self.motor.stop() elif state.prev_direction == 0: return self.adjust - if abs(self.value) > self.hysteresis: + if abs_force > self.hysteresis: self.set_zero_pos(self.value, self.motor.read_value()) return self.adjust - if self.value * direction < -self.hysteresis: + if abs_force < -self.hysteresis: state.force_before_free = self.value return self.free if self.motor_busy(): @@ -199,33 +354,16 @@ class Uniax(PersistentMixin, Drivable): self.drive_relative(direction * 360) return Retry() - def cleanup(self, state): - """in case of error, set error status""" - if state.stopped: # stop or restart - if state.stopped is Restart: - return - self.status = 'IDLE', 'stopped' - self.log.warning('stopped') - else: - self.status = 'ERROR', str(state.last_error) - if isinstance(state.last_error, Error): - self.log.error('%s', state.last_error) - else: - self.log.error('%r raised in state %r', str(state.last_error), state.status_string) - self.motor.stop() - self.write_adjusting(False) - def free(self, state): """free from high force at other end""" if state.init: state.free_way = None + self.reset_progress(state) if self.motor_busy(): return Retry() - if abs(self.value) > abs(state.force_before_free) + self.tolerance: - self.stop() - self.status = 'ERROR', 'force increase while freeing' - self.log.error(self.status[1]) - return None + self.value = self._force + if abs(self.value) > abs(state.force_before_free) + self.hysteresis: + raise Error('force increase while freeing') if abs(self.value) < self.hysteresis: return self.find if state.free_way is None: @@ -233,93 +371,12 @@ class Uniax(PersistentMixin, Drivable): self.log.info('free from high force %g', self.value) self.write_adjusting(True) direction = math.copysign(1, self.target) - if state.free_way > (abs(state.force_before_free) + self.hysteresis) * self.slope: - self.stop() - self.status = 'ERROR', 'freeing failed' - self.log.error(self.status[1]) - return None + if state.free_way > abs(state.force_before_free + self.hysteresis) * self.slope + self.motor_max_play: + raise Error('freeing failed') state.free_way += self.safe_step self.drive_relative(direction * self.safe_step) return Retry() - def within_tolerance(self, state): - """within tolerance""" - if self.motor_busy(): - return Retry() - if abs(self.target - self.value) > self.tolerance: - return self.adjust - self.status = 'IDLE', 'within tolerance' - - def adjust(self, state): - """adjust force""" - if state.init: - state.prev_force = None - if self.motor_busy(): - return - if abs(self.target - self.value) < self.tolerance: - self._in_cnt += 1 - if self._in_cnt >= 3: - return self.within_tolerance - else: - self._in_cnt = 0 - if state.prev_force is None: - state.prev_force = self.value - state.prev_pos = self.motor.pos - self.write_adjusting(True) - self.status = 'BUSY', 'adjusting force' - elif not self._filtered: - return - else: - if abs(self.value - state.prev_force) > self.substantial_force: - state.prev_force = self.value - state.prev_pos = self.motor.value - else: - motor_dif = abs(self.value - state.prev_pos) - if abs(self.value) < self.hysteresis: - if motor_dif > self.motor_play: - self.log.warning('adjusting failed - try to find zero pos') - self.set_zero_pos(self.target, None) - return self.find - elif motor_dif > self.motor_max_play: - raise Error('force seems not to change substantially') - force_step = (self.target - self.value) * self.pid_i - self.drive_relative(force_step * self.slope) - return Retry() - - def read_value(self): - try: - force = self.transducer.read_value() - self._cnt_rderr = max(0, self._cnt_rderr - 1) - except Exception as e: - self._cnt_rderr += 1 - if self._cnt_rderr > 10: - self.stop() - self.status = 'ERROR', 'too many read errors: %s' % e - self.log.error(self.status[1]) - return Done - - now = time.time() - if self.motor_busy(): - # do not filter while driving - self.reset_filter() - self._filtered = False - else: - self._sum += force - self._cnt += 1 - if now < self._filter_start + self.filter_interval: - return Done - force = self._sum / self._cnt - self.reset_filter(now) - self._filtered = True - if abs(force) > self.limit + self.hysteresis: - self.motor.stop() - self.status = 'ERROR', 'above max limit' - self.log.error(self.status[1]) - return Done - if self.zero_pos(force) is None and abs(force) > self.hysteresis and self._filtered: - self.set_zero_pos(force, self.motor.read_value()) - return force - def write_target(self, target): if abs(target) > self.limit: raise BadValueError('force above limit') @@ -332,11 +389,19 @@ class Uniax(PersistentMixin, Drivable): self._cnt_rderr = 0 self._cnt_wrerr = 0 self.status = 'BUSY', 'changed target' + self.target = target if self.value * math.copysign(1, target) > self.hysteresis: self._state.start(self.adjust) else: self._state.start(self.find) - return target + return Done + + def read_target(self): + if self._state.state is None: + if self.status[1]: + raise Error(self.status[1]) + raise Error('inactive') + return self.target @Command() def stop(self): From 5affb9f31bb3862e8b22ae406f32de43c8b55cc9 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 20 Sep 2022 11:26:33 +0200 Subject: [PATCH 15/22] fix bug in persistent.py - use dirname instead of basename --- secop/persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secop/persistent.py b/secop/persistent.py index 0e7aebd..2d0f12f 100644 --- a/secop/persistent.py +++ b/secop/persistent.py @@ -129,7 +129,7 @@ class PersistentMixin(HasAccessibles): if getattr(v, 'persistent', False)} if data != self.persistentData: self.persistentData = data - persistentdir = os.path.basename(self.persistentFile) + persistentdir = os.path.dirname(self.persistentFile) tmpfile = self.persistentFile + '.tmp' if not os.path.isdir(persistentdir): os.makedirs(persistentdir, exist_ok=True) From 9f656546dff94dbb026a79fa785d9959e313dffb Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 20 Sep 2022 12:06:46 +0200 Subject: [PATCH 16/22] fix undefined status in softcal --- secop_psi/softcal.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/secop_psi/softcal.py b/secop_psi/softcal.py index 530c7be..45b385c 100644 --- a/secop_psi/softcal.py +++ b/secop_psi/softcal.py @@ -27,7 +27,8 @@ from os.path import basename, dirname, exists, join import numpy as np from scipy.interpolate import splev, splrep # pylint: disable=import-error -from secop.core import Attached, BoolType, Parameter, Readable, StringType, FloatRange +from secop.core import Attached, BoolType, Parameter, Readable, StringType, \ + FloatRange, Done def linear(x): @@ -182,7 +183,6 @@ class Sensor(Readable): description = 'a calibrated sensor value' _value_error = None - enablePoll = False def checkProperties(self): if 'description' not in self.propertyValues: @@ -196,6 +196,9 @@ class Sensor(Readable): if self.description == '_': self.description = '%r calibrated with curve %r' % (self.rawsensor, self.calib) + def doPoll(self): + self.read_status() + def write_calib(self, value): self._calib = CalCurve(value) return value @@ -221,3 +224,8 @@ class Sensor(Readable): def read_value(self): return self._calib(self.rawsensor.read_value()) + + def read_status(self): + self.update_status(self.rawsensor.status) + return Done + From e4aa2149f7e8cdca6ef7269c13d88a7376331819 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 21 Sep 2022 11:35:42 +0200 Subject: [PATCH 17/22] phytron: fix warning on repeated comm. error log only a warning when several retries ware successful Change-Id: I2f1dfba920b0841914da82229820bcdd4b97c6e9 --- secop_psi/phytron.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/secop_psi/phytron.py b/secop_psi/phytron.py index 614814d..66899e5 100644 --- a/secop_psi/phytron.py +++ b/secop_psi/phytron.py @@ -35,7 +35,9 @@ class PhytronIO(StringIO): identification = [('0IVR', 'MCC Minilog .*')] def communicate(self, command): - for ntry in range(5, 0, -1): + ntry = 5 + warn = None + for itry in range(ntry): try: _, _, reply = super().communicate('\x02' + command).partition('\x02') if reply[0] == '\x06': # ACK @@ -43,9 +45,12 @@ class PhytronIO(StringIO): raise CommunicationFailedError('missing ACK %r (cmd: %r)' % (reply, command)) except Exception as e: - if ntry == 1: + if itry < ntry - 1: + warn = e + else: raise - self.log.warning('%s - retry', e) + if warn: + self.log.warning('needed %d retries after %r', itry, warn) return reply[1:] From de5f17695cea15278ec12691ae4b5305a732528e Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 2 Jun 2022 10:03:38 +0200 Subject: [PATCH 18/22] make startup faster in case of errors When the io of one SECoP module fails, it takes ages to startup because each parameter poll takes the time to wait for a timeout. After the first communication error on an io, no more startup polls are tried on the modules using this io. Change-Id: I0d250953dfe91a7d68d2d2b108395cc25d471afe Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28588 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- secop/io.py | 32 ++++++++++++++------------------ secop/modules.py | 19 +++++++++++++++---- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/secop/io.py b/secop/io.py index 0ff9359..a5431c3 100644 --- a/secop/io.py +++ b/secop/io.py @@ -71,14 +71,6 @@ class HasIO(Module): elif not io: raise ConfigError("Module %s needs a value for either 'uri' or 'io'" % name) - def initModule(self): - try: - self.io.read_is_connected() - except (CommunicationFailedError, AttributeError): - # AttributeError: read_is_connected is not required for an io object - pass - super().initModule() - def communicate(self, *args): return self.io.communicate(*args) @@ -118,6 +110,7 @@ class IOBase(Communicator): _conn = None _last_error = None _lock = None + _last_connect_attempt = 0 def earlyInit(self): super().earlyInit() @@ -169,6 +162,17 @@ class IOBase(Communicator): return False return self.read_is_connected() + def check_connection(self): + """called before communicate""" + if not self.is_connected: + now = time.time() + if now >= self._last_connect_attempt + self.pollinterval: + # we do not try to reconnect more often than pollinterval + _last_connect_attempt = now + if self.read_is_connected(): + return + raise SilentError('disconnected') from None + def registerReconnectCallback(self, name, func): """register reconnect callback @@ -250,11 +254,7 @@ class StringIO(IOBase): wait_before is respected for end_of_lines within a command. """ command = command.encode(self.encoding) - if not self.is_connected: - # do not try to reconnect here - # read_is_connected is doing this when called by its poller - self.read_is_connected() # try to reconnect - raise SilentError('disconnected') from None + self.check_connection() try: with self._lock: # read garbage and wait before send @@ -359,11 +359,7 @@ class BytesIO(IOBase): @Command((BLOBType(), IntRange(0)), result=BLOBType()) def communicate(self, request, replylen): # pylint: disable=arguments-differ """send a request and receive (at least) bytes as reply""" - if not self.is_connected: - # do not try to reconnect here - # read_is_connected is doing this when called by its poller - self.read_is_connected() # try to reconnect - raise SilentError('disconnected') from None + self.check_connection() try: with self._lock: # read garbage and wait before send diff --git a/secop/modules.py b/secop/modules.py index 827740b..db6d296 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -30,7 +30,7 @@ from functools import wraps from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \ IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion -from secop.errors import BadValueError, ConfigError, \ +from secop.errors import BadValueError, CommunicationFailedError, ConfigError, \ ProgrammingError, SECoPError, secop_error from secop.lib import formatException, mkthread, UniqueObject, generalConfig from secop.lib.enum import Enum @@ -641,7 +641,7 @@ class Module(HasAccessibles): self.pollInfo.interval = fast_interval if flag else self.pollinterval self.pollInfo.trigger() - def callPollFunc(self, rfunc): + def callPollFunc(self, rfunc, raise_com_failed=False): """call read method with proper error handling""" try: rfunc() @@ -658,6 +658,8 @@ class Module(HasAccessibles): else: # uncatched error: this is more serious self.log.error('%s: %s', name, formatException()) + if raise_com_failed and isinstance(e, CommunicationFailedError): + raise def __pollThread(self, modules, started_callback): """poll thread body @@ -682,7 +684,7 @@ class Module(HasAccessibles): trg.set() self.registerReconnectCallback('trigger_polls', trigger_all) - # collect and call all read functions a first time + # collect all read functions for mobj in modules: pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll) # trigger a poll interval change when self.pollinterval changes. @@ -693,7 +695,16 @@ class Module(HasAccessibles): rfunc = getattr(mobj, 'read_' + pname) if rfunc.poll: pinfo.polled_parameters.append((mobj, rfunc, pobj)) - mobj.callPollFunc(rfunc) + # call all read functions a first time + try: + for m in modules: + for mobj, rfunc, _ in m.pollInfo.polled_parameters: + mobj.callPollFunc(rfunc, raise_com_failed=True) + except CommunicationFailedError as e: + # when communication failed, probably all parameters and may be more modules are affected. + # as this would take a lot of time (summed up timeouts), we do not continue + # trying and let the server accept connections, further polls might success later + self.log.error('communication failure on startup: %s', e) started_callback() to_poll = () while True: From 0df50bb0f9f8259467a6c9f7329d09f3bf94e993 Mon Sep 17 00:00:00 2001 From: Enrico Faulhaber Date: Tue, 2 Aug 2022 11:54:53 +0200 Subject: [PATCH 19/22] secop_mlz: minor rework entangle client Change-Id: Ie406b4220c22cdbf302a1fd36f2d7407d81a47fa Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/28951 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker Reviewed-by: Enrico Faulhaber --- secop_mlz/entangle.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index ba97125..df20938 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -28,6 +28,9 @@ Here we support devices which fulfill the official MLZ TANGO interface for the respective device classes. """ +# pylint: disable=too-many-lines + + import re import threading from time import sleep @@ -173,7 +176,7 @@ class PyTangoDevice(Module): tango_status_mapping = { PyTango.DevState.ON: Drivable.Status.IDLE, PyTango.DevState.ALARM: Drivable.Status.WARN, - PyTango.DevState.OFF: Drivable.Status.ERROR, + PyTango.DevState.OFF: Drivable.Status.DISABLED, PyTango.DevState.FAULT: Drivable.Status.ERROR, PyTango.DevState.MOVING: Drivable.Status.BUSY, } @@ -504,6 +507,9 @@ class AnalogOutput(PyTangoDevice, Drivable): return stable and at_target def read_status(self): + _st, _sts = super().read_status() + if _st == Readable.Status.DISABLED: + return _st, _sts if self._isAtTarget(): self._timeout = None self._moving = False From 485e81bfb0df37bdfb6dc38221418a9acee34c08 Mon Sep 17 00:00:00 2001 From: Jenkins system Date: Tue, 2 Aug 2022 15:31:52 +0200 Subject: [PATCH 20/22] [deb] Release v0.13.1 Change-Id: Ib038475d75de7b2976bc463423c2493eb531c02a --- debian/changelog | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/debian/changelog b/debian/changelog index 2b822a0..a2d2e8c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,77 @@ +secop-core (0.13.1) focal; urgency=medium + + [ Markus Zolliker ] + * an enum with value 0 should be interpreted as False + * make startup faster in case of errors + + [ Enrico Faulhaber ] + * secop_mlz: minor rework entangle client + + -- Markus Zolliker Tue, 02 Aug 2022 15:31:52 +0200 + +secop-core (0.13.0) focal; urgency=medium + + [ Georg Brandl ] + * debian: fix email addresses in changelog + + [ Markus Zolliker ] + * various small changes + * automatic saving of persistent parameters + * add more tests and fixes for command inheritance + * entangle.AnalogOutput: fix window mechanism + * remote logging (issue 46) + * add timeouts to MultiEvents + * introduce general config file + * improve handling of module init methods + * check for bad read_* and write_* methods + * change name of read_hw_status method in sequencer mixin + * fix doc (stringio - > io) + * enhance logging + * UniqueObject + * ReadHandler and WriteHandler decorators + * do not convert string to float + * check for problematic value range + * unify name and module on Attached property + * ppms: replace IOHandler by Read/WriteHandler + * fix handling commands + * common read/write handlers + * implement a state machine + * proper return value in handler read_* methods + * new poll mechanism + * support for fast poll when busy + * various small fixes + * reset connection on identification + * improve softcal + * move markdown to requirements-dev.txt + * improve k2601b driver + * fix and improved Attached + * fix error in write wrapper and more + * support write_ method on readonly param and more + * init generalConfig.defaults only in secop-server + * HasConvergence mixin + * avoid race conditions in read_*/write_* methods + * reintroduced individual init of generalConfig.defaults + * fix statemachine + * use a common poller thread for modules sharing io + * motor valve using trinamic motor + * improved trinamic driver + * fix error in secop.logging + * avoid deadlock in proxy + * improve poller error handling + * support for OI mercury series + * add 'ts' to the ppms simulation + * allow a configfile path as single argument to secop-server + * fix keithley 2601b after tests + * channel switcher for Lakeshore 370 with scanner + * feature implementation + * allow to convert numpy arrays to ArrayOf + * remove IOHandler stuff + + [ Enrico Faulhaber ] + * default unit to UTF8 + + -- Georg Brandl Tue, 02 Aug 2022 09:47:06 +0200 + secop-core (0.12.4) focal; urgency=medium * fix command inheritance From 48076edd993ebf46494c3760916dc748a98acfdf Mon Sep 17 00:00:00 2001 From: l_samenv Date: Thu, 22 Sep 2022 17:12:30 +0200 Subject: [PATCH 21/22] improve magfield and ips_mercury --- secop_psi/ips_mercury.py | 21 +++++++++++---------- secop_psi/magfield.py | 31 +++++++++++++++++-------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index f74972a..4c14ccf 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -26,6 +26,7 @@ from secop.lib.enum import Enum from secop.errors import BadValueError, HardwareError from secop_psi.magfield import Magfield from secop_psi.mercury import MercuryChannel, off_on, Mapped +from secop.lib.statemachine import Retry Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3) hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, @@ -52,7 +53,6 @@ class Field(MercuryChannel, Magfield): nslaves = 3 slave_currents = None __init = True - __reset_switch_time = False def doPoll(self): super().doPoll() @@ -98,15 +98,10 @@ class Field(MercuryChannel, Magfield): def read_switch_heater(self): value = self.query('PSU:SIG:SWHT', off_on) now = time.time() - switch_time = self.switch_time[self.switch_heater] if value != self.switch_heater: - self.__reset_switch_time = True - if now < (switch_time or 0) + 10: + if now < (self.switch_time[self.switch_heater] or 0) + 10: # probably switch heater was changed, but IPS reply is not yet updated return self.switch_heater - elif self.__reset_switch_time: - self.__reset_switch_time = False - self.switch_time = [None, None] return value def write_switch_heater(self, value): @@ -155,16 +150,22 @@ class Field(MercuryChannel, Magfield): try: self.set_and_go(self.persistent_field) except (HardwareError, AssertionError): - state.switch_undef = self.switch_on_time or state.now + state.switch_undef = self.switch_time[self.switch_heater.on] or state.now return self.wait_for_switch return self.ramp_to_field + def ramp_to_field(self, state): + if self.action != 'run_to_set': + self.status = Status.PREPARING, 'restart ramp to field' + return self.start_ramp_to_field + return super().ramp_to_field(state) + def wait_for_switch(self, state): - if self.now - self.switch_undef < self.wait_switch_on: + if state.now - state.switch_undef < self.wait_switch_on: return Retry() self.set_and_go(self.persistent_field) return self.ramp_to_field - + def start_ramp_to_target(self, state): self.set_and_go(self.target) return self.ramp_to_target diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index ef90bc2..b7d42f6 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -76,33 +76,23 @@ class Magfield(HasLimits, Drivable): # ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False) # TODO: the following parameters should be changed into properties after tests wait_switch_on = Parameter( - 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=61) + 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=60) wait_switch_off = Parameter( - 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61) + 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=60) wait_stable_leads = Parameter( 'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6) wait_stable_field = Parameter( - 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31) + 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=30) persistent_limit = Parameter( 'above this limit, lead currents are not driven to 0', FloatRange(0, unit='$'), readonly=False, default=99) _state = None - __init = True _last_target = None switch_time = None, None def doPoll(self): - if self.__init: - self.__init = False - if self.read_switch_heater() and self.mode == Mode.PERSISTENT: - self.read_value() # check for persistent field mismatch - # switch off heater from previous live or manual intervention - self.write_target(self.persistent_field) - else: - self._last_target = self.persistent_field - else: - self.read_value() + self.read_value() self._state.cycle() def checkProperties(self): @@ -119,6 +109,19 @@ class Magfield(HasLimits, Drivable): self.registerCallbacks(self) # for update_switch_heater self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup_state) + def startModule(self, start_events): + start_events.queue(self.startupCheck) + super().startModule(start_events) + + def startupCheck(self): + if self.read_switch_heater() and self.mode == Mode.PERSISTENT: + self.read_value() # check for persistent field mismatch + # switch off heater from previous live or manual intervention + self.write_mode(self.mode) + self.write_target(self.persistent_field) + else: + self._last_target = self.persistent_field + def write_target(self, target): self.check_limits(target) self.target = target From 00b0a54f12597c14ca72205496362d5ca5fc335f Mon Sep 17 00:00:00 2001 From: l_samenv Date: Thu, 22 Sep 2022 17:12:54 +0200 Subject: [PATCH 22/22] ramp unit must be $/min --- secop_psi/mercury.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/secop_psi/mercury.py b/secop_psi/mercury.py index deb4a9e..0aebc82 100644 --- a/secop_psi/mercury.py +++ b/secop_psi/mercury.py @@ -345,7 +345,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): class TemperatureLoop(TemperatureSensor, Loop, Drivable): channel_type = 'TEMP' output_module = Attached(HasInput, mandatory=False) - ramp = Parameter('ramp rate', FloatRange(0, unit='K/min'), readonly=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='$')) tolerance = Parameter(default=0.1)