From fd917724d87fd64bfcadfd4e576d8a45cafe1fda Mon Sep 17 00:00:00 2001 From: Alexander Zaft Date: Tue, 6 Jun 2023 10:30:19 +0200 Subject: [PATCH 01/14] io: add option to retry first ident request Change-Id: I524c15387eaf2461e3dfe690250a55f058467b0b Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31291 Tested-by: Jenkins Automated Tests Reviewed-by: Bjoern Pedersen Reviewed-by: Alexander Zaft --- frappy/io.py | 75 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/frappy/io.py b/frappy/io.py index dc669cd..da2cc73 100644 --- a/frappy/io.py +++ b/frappy/io.py @@ -133,6 +133,14 @@ class IOBase(Communicator): self._lock = threading.RLock() def connectStart(self): + if not self.is_connected: + uri = self.uri + self._conn = AsynConn(uri, self._eol_read, + default_settings=self.default_settings) + self.is_connected = True + self.checkHWIdent() + + def checkHWIdent(self): raise NotImplementedError def closeConnection(self): @@ -218,12 +226,19 @@ class StringIO(IOBase): default='\n', settable=True) encoding = Property('used encoding', datatype=StringType(), default='ascii', settable=True) - identification = Property(''' - identification + identification = Property( + '''identification - a list of tuples with commands and expected responses as regexp, - to be sent on connect''', - datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) + a list of tuples with commands and expected responses as regexp, + to be sent on connect''', + datatype=ArrayOf(TupleOf(StringType(), StringType())), + default=[], export=False) + retry_first_idn = Property( + '''retry first identification message + + a flag to indicate whether the first message should be resent once to + avoid data that may still be in the buffer to garble the message''', + datatype=BoolType(), default=False) def _convert_eol(self, value): if isinstance(value, str): @@ -248,16 +263,22 @@ class StringIO(IOBase): raise ValueError('end_of_line for read must not be empty') self._eol_write = self._convert_eol(eol[-1]) - def connectStart(self): - if not self.is_connected: - uri = self.uri - self._conn = AsynConn(uri, self._eol_read, default_settings=self.default_settings) - self.is_connected = True - for command, regexp in self.identification: - reply = self.communicate(command) - if not re.match(regexp, reply): - self.closeConnection() - raise CommunicationFailedError(f'bad response: {reply} does not match {regexp}') + def checkHWIdent(self): + if not self.identification: + return + idents = iter(self.identification) + command, regexp = next(idents) + reply = self.communicate(command) + if self.retry_first_idn and not re.match(regexp, reply): + self.log.debug('first ident command not successful.' + ' retrying in case of garbage data.') + idents = iter(self.identification) + for command, regexp in idents: + reply = self.communicate(command) + if not re.match(regexp, reply): + self.closeConnection() + raise CommunicationFailedError(f'bad response: {reply}' + ' does not match {regexp}') @Command(StringType(), result=StringType()) def communicate(self, command): @@ -356,19 +377,19 @@ class BytesIO(IOBase): - a two digit hexadecimal number (byte value) - a character - ?? indicating ignored bytes in responses - """, datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) + """, datatype=ArrayOf(TupleOf(StringType(), StringType())), + default=[], export=False) - def connectStart(self): - if not self.is_connected: - uri = self.uri - self._conn = AsynConn(uri, b'', default_settings=self.default_settings) - self.is_connected = True - for request, expected in self.identification: - replylen, replypat = make_regexp(expected) - reply = self.communicate(make_bytes(request), replylen) - if not replypat.match(reply): - self.closeConnection() - raise CommunicationFailedError(f'bad response: {reply!r} does not match {expected!r}') + _eol_read = b'' + + def checkHWIdent(self): + for request, expected in self.identification: + replylen, replypat = make_regexp(expected) + reply = self.communicate(make_bytes(request), replylen) + if not replypat.match(reply): + self.closeConnection() + raise CommunicationFailedError(f'bad response: {reply!r}' + ' does not match {expected!r}') @Command((BLOBType(), IntRange(0)), result=BLOBType()) def communicate(self, request, replylen): # pylint: disable=arguments-differ From dff0c819de7aa83d822b3a040d004b8656eb1248 Mon Sep 17 00:00:00 2001 From: Alexander Zaft Date: Mon, 12 Jun 2023 08:09:24 +0200 Subject: [PATCH 02/14] io: followup fix for retry-first-ident followup fix: no error was raised ever for the first identification message. Change-Id: I80f0f431add6dfd7b37d750b9fc661174aa8f217 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31318 Tested-by: Jenkins Automated Tests Reviewed-by: Georg Brandl Reviewed-by: Enrico Faulhaber Reviewed-by: Alexander Zaft --- frappy/io.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/frappy/io.py b/frappy/io.py index da2cc73..3201963 100644 --- a/frappy/io.py +++ b/frappy/io.py @@ -269,16 +269,21 @@ class StringIO(IOBase): idents = iter(self.identification) command, regexp = next(idents) reply = self.communicate(command) - if self.retry_first_idn and not re.match(regexp, reply): - self.log.debug('first ident command not successful.' - ' retrying in case of garbage data.') - idents = iter(self.identification) + if not re.match(regexp, reply): + if self.retry_first_idn: + self.log.debug('first ident command not successful.' + ' retrying in case of garbage data.') + idents = iter(self.identification) + else: + self.closeConnection() + raise CommunicationFailedError(f'bad response: {reply!r}' + f' does not match {regexp!r}') for command, regexp in idents: reply = self.communicate(command) if not re.match(regexp, reply): self.closeConnection() - raise CommunicationFailedError(f'bad response: {reply}' - ' does not match {regexp}') + raise CommunicationFailedError(f'bad response: {reply!r}' + f' does not match {regexp!r}') @Command(StringType(), result=StringType()) def communicate(self, command): From 0932228596cd06d571f2d3e6cf6ed567d8e99e1f Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Jun 2023 11:28:59 +0200 Subject: [PATCH 03/14] frappy_psi.mercury/triton: add control_off command frappy_psi.triton.TemperatureLoop has not output module to deactivate control -> add control_off also to loops in frappy_psi.mercury Change-Id: I4dc4333134da34a8d3ae0f3c037a1e5b108c95a1 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31341 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- frappy_psi/mercury.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappy_psi/mercury.py b/frappy_psi/mercury.py index 8c88dfd..40202be 100644 --- a/frappy_psi/mercury.py +++ b/frappy_psi/mercury.py @@ -25,7 +25,7 @@ import math import re import time -from frappy.core import Drivable, HasIO, Writable, StatusType, \ +from frappy.core import Command, Drivable, HasIO, Writable, StatusType, \ Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf from frappy.errors import HardwareError, ProgrammingError, ConfigError, RangeError @@ -218,7 +218,6 @@ class HasInput(HasControlledBy, MercuryChannel): class Loop(HasOutputModule, MercuryChannel, Drivable): """common base class for loops""" output_module = Attached(HasInput, mandatory=False) - control_active = Parameter(readonly=False) ctrlpars = Parameter( 'pid (proportional band, integral time, differential time', StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')), @@ -254,6 +253,13 @@ class Loop(HasOutputModule, MercuryChannel, Drivable): def read_status(self): return IDLE, '' + @Command() + def control_off(self): + """switch control off""" + # remark: this is needed in frappy_psi.trition.TemperatureLoop, as the heater + # output is not available there. We define it here as a convenience for the user. + self.write_control_active(False) + class ConvLoop(HasConvergence, Loop): def deactivate_control(self, source): From 36dfe968e8c4a0e5bed570bf9591581c0e9bb421 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 14 Jun 2023 11:01:48 +0200 Subject: [PATCH 04/14] frappy_psi.phytron: rename reset_error to clear_errors use the command 'clear_errors' to return from an error state + make sure target is valid after clear_errors Change-Id: I3c180500a05836d52bbb9a8ecbdb397adea03d0d Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31337 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- frappy_psi/phytron.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frappy_psi/phytron.py b/frappy_psi/phytron.py index 36062db..66f5b9f 100644 --- a/frappy_psi/phytron.py +++ b/frappy_psi/phytron.py @@ -83,7 +83,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): ioClass = PhytronIO _step_size = None # degree / step - _blocking_error = None # None or a string indicating the reason of an error needing reset + _blocking_error = None # None or a string indicating the reason of an error needing clear_errors _running = False # status indicates motor is running STATUS_MAP = { @@ -121,10 +121,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): if not axisbit & active_axes: # power cycle detected and this axis not yet active self.set('P37S', axisbit | active_axes) # activate axis if now < self.alive_time + 7 * 24 * 3600: # the device was running within last week - # inform the user about the loss of position by the need of doing reset_error + # inform the user about the loss of position by the need of doing clear_errors self._blocking_error = 'lost position' - else: # do reset silently - self.reset_error() + else: # do silently + self.clear_errors() self.alive_time = now self.saveParameters() return now @@ -171,7 +171,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): def write_target(self, value): self.read_alive_time() if self._blocking_error: - self.status = ERROR, 'reset needed after ' + self._blocking_error + self.status = ERROR, 'clear_errors needed after ' + self._blocking_error raise HardwareError(self.status[1]) self.saveParameters() if self.backlash: @@ -261,7 +261,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): self.start_machine(self.stopping, status=(BUSY, 'stopping')) @Command - def reset_error(self): + def clear_errors(self): """Reset error, set position to encoder""" self.read_value() if self._blocking_error: @@ -286,3 +286,4 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): self.read_value() self.status = 'IDLE', 'after error reset' self._blocking_error = None + self.target = self.value # clear error in target From 644d005dada3d0dc55f3aec6853d2e616c611f81 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 19 Jun 2023 10:33:24 +0200 Subject: [PATCH 05/14] frappy.mixins.HasOutputModule add 'set_control_active' method for overriding by subclasses Change-Id: Ib344319862a4a0bf29efef16a63db09d1f314a82 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31375 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- frappy/mixins.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frappy/mixins.py b/frappy/mixins.py index 8bfad2c..d35f09b 100644 --- a/frappy/mixins.py +++ b/frappy/mixins.py @@ -53,7 +53,7 @@ class HasControlledBy: to be called from the write_target method """ if self.controlled_by: - self.controlled_by = 0 + self.controlled_by = 0 # self for deactivate_control in self.inputCallbacks.values(): deactivate_control(self.name) @@ -74,6 +74,10 @@ class HasOutputModule: if self.output_module: self.output_module.register_input(self.name, self.deactivate_control) + def set_control_active(self, active): + """to be overridden for switching hw control""" + self.control_active = active + def activate_control(self): """method to switch control_active on @@ -85,10 +89,10 @@ class HasOutputModule: if name != self.name: deactivate_control(self.name) out.controlled_by = self.name - self.control_active = True + self.set_control_active(True) - def deactivate_control(self, source): + def deactivate_control(self, source=None): """called when an other module takes over control""" if self.control_active: - self.control_active = False - self.log.warning(f'switched to manual mode by {source}') + self.set_control_active(False) + self.log.warning(f'switched to manual mode by {source or self.name}') From 997e8e26e9521720a13c0c063d8348e5a6e949e4 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 19 Jun 2023 10:55:40 +0200 Subject: [PATCH 06/14] frappy_psi.mercury: proper handling of control_active Change-Id: I31e846fa6fdf6d642184e3736a66ffd53033bccf Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31376 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- frappy_psi/mercury.py | 50 ++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/frappy_psi/mercury.py b/frappy_psi/mercury.py index 40202be..5a8294b 100644 --- a/frappy_psi/mercury.py +++ b/frappy_psi/mercury.py @@ -28,7 +28,7 @@ import time from frappy.core import Command, Drivable, HasIO, Writable, StatusType, \ Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf -from frappy.errors import HardwareError, ProgrammingError, ConfigError, RangeError +from frappy.errors import HardwareError, ProgrammingError, ConfigError from frappy_psi.convergence import HasConvergence from frappy.states import Retry, Finish from frappy.mixins import HasOutputModule, HasControlledBy @@ -225,14 +225,15 @@ class Loop(HasOutputModule, MercuryChannel, Drivable): ) enable_pid_table = Parameter('', BoolType(), readonly=False) - def set_output(self, active, source='HW'): + def set_output(self, active, source=None): if active: self.activate_control() else: self.deactivate_control(source) def set_target(self, target): - self.set_output(True) + if not self.control_active: + self.activate_control() self.target = target def read_enable_pid_table(self): @@ -262,7 +263,7 @@ class Loop(HasOutputModule, MercuryChannel, Drivable): class ConvLoop(HasConvergence, Loop): - def deactivate_control(self, source): + def deactivate_control(self, source=None): if self.control_active: super().deactivate_control(source) self.convergence_state.start(self.inactive_state) @@ -378,16 +379,14 @@ class TemperatureLoop(TemperatureSensor, ConvLoop): super().doPoll() self.read_setpoint() - def read_control_active(self): - active = self.query(f'DEV::{self.ENABLE}', off_on) - self.set_output(active) - return active + def set_control_active(self, active): + super().set_control_active(active) + self.change(f'DEV::{self.ENABLE}', active, off_on) - def write_control_active(self, value): - if value: - raise RangeError('write to target to switch control on') - self.set_output(value, 'user') - return self.change(f'DEV::{self.ENABLE}', value, off_on) + def initialReads(self): + # initialize control active from HW + active = self.query(f'DEV::{self.ENABLE}', off_on) + super().set_output(active, 'HW') @nopoll # polled by read_setpoint def read_target(self): @@ -419,7 +418,7 @@ class TemperatureLoop(TemperatureSensor, ConvLoop): self.change(f'DEV::{self.ENABLE}', True, off_on) super().set_target(target) - def deactivate_control(self, source): + def deactivate_control(self, source=None): if self.__ramping: self.__ramping = False # stop ramping setpoint @@ -514,14 +513,16 @@ class PressureLoop(PressureSensor, HasControlledBy, ConvLoop): output_module = Attached(ValvePos, mandatory=False) tolerance = Parameter(default=0.1) - def read_control_active(self): - active = self.query('DEV::PRES:LOOP:FAUT', off_on) - self.set_output(active) - return active + def set_control_active(self, active): + super().set_control_active(active) + if not active: + self.self_controlled() # switches off auto flow + return self.change('DEV::PRES:LOOP:FAUT', active, off_on) - def write_control_active(self, value): - self.set_output(value, 'user') - return self.change('DEV::PRES:LOOP:FAUT', value, off_on) + def initialReads(self): + # initialize control active from HW + active = self.query('DEV::PRES:LOOP:FAUT', off_on) + super().set_output(active, 'HW') def read_target(self): return self.query('DEV::PRES:LOOP:PRST') @@ -566,14 +567,15 @@ class HasAutoFlow: if value: self.needle_valve.controlled_by = self.name else: + if self.needle_valve.control_active: + self.needle_valve.set_target(self.flowpars[1][0]) # flow min if self.needle_valve.controlled_by != SELF: self.needle_valve.controlled_by = SELF - self.needle_valve.write_target(self.flowpars[1][0]) # flow min return value - def auto_flow_off(self): + def auto_flow_off(self, source=None): if self.auto_flow: - self.log.warning('switch auto flow off') + self.log.warning(f'switched auto flow off by {source or self.name}') self.write_auto_flow(False) From 138b84e84cb9e6de6db31c621dd4e0ef97a6a8ea Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 19 Jun 2023 08:59:31 +0200 Subject: [PATCH 07/14] add a hook for reads to be done initially inital reads from HW should be done in the thread started by startModule, not in startModule itself. - add a hook method 'initialReads' for this + add doc for init methods + fix some errors in doc Change-Id: I914e3b7ee05050eea1ee8aff3461030adf08a461 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31374 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- doc/source/magic.rst | 19 ++++++++++++- doc/source/programming.rst | 18 ++++++++++++ doc/source/reference.rst | 2 +- frappy/datatypes.py | 4 +-- frappy/errors.py | 13 +++++---- frappy/modules.py | 58 +++++++++++++++++++++++++------------- test/test_statemachine.py | 13 +++++++-- 7 files changed, 96 insertions(+), 31 deletions(-) diff --git a/doc/source/magic.rst b/doc/source/magic.rst index 4a5f347..0baf40b 100644 --- a/doc/source/magic.rst +++ b/doc/source/magic.rst @@ -8,7 +8,24 @@ what the framwork does for you. Startup ....... -TODO: describe startup: init methods, first polls +On startup several methods are called. First :meth:`earlyInit` is called on all modules. +Use this to initialize attributes independent of other modules, if you can not initialize +as a class attribute, for example for mutable attributes. + +Then :meth:`initModule` is called for all modules. +Use it to initialize things related to other modules, for example registering callbacks. + +After this, :meth:`startModule` is called with a callback function argument. +:func:`frappy.modules.Module.startModule` starts the poller thread, calling +:meth:`writeInitParams` for writing initial parameters to hardware, followed +by :meth:`initialReads`. The latter is meant for reading values from hardware, +which are not polled continuously. Then all parameters configured for poll are polled +by calling the corresponding read_*() method. The end of this last initialisation +step is indicated to the server by the callback function. +After this, the poller thread starts regular polling, see next section. + +When overriding one of above methods, do not forget to super call. + .. _polling: diff --git a/doc/source/programming.rst b/doc/source/programming.rst index 5c2e186..776e7dc 100644 --- a/doc/source/programming.rst +++ b/doc/source/programming.rst @@ -95,5 +95,23 @@ Example code: return self.read_target() # return the read back value +Parameter Initialisation +------------------------ + +Initial values of parameters might be given by several different sources: + +1) value argument of a Parameter declaration +2) read from HW +3) read from persistent data file +4) value given in config file + +For (2) the programmer might decide for any parameter to poll it regularely from the +hardware. In this case changes from an other input, for example a keyboard or other +interface of the connected devices would be updated continuously in Frappy. +If there is no such other input, or if the programmer decides that such other +data sources are not to be considered, the hardware parameter might be read in just +once on startup, :func:`frappy.modules.Module.initialReads` may be overriden. +This method is called once on startup, before the regular polls start. + .. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies diff --git a/doc/source/reference.rst b/doc/source/reference.rst index 19360c2..cd624af 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -12,7 +12,7 @@ Module Base Classes ................... .. autoclass:: frappy.modules.Module - :members: earlyInit, initModule, startModule + :members: earlyInit, initModule, startModule, initialReads .. autoclass:: frappy.modules.Readable :members: Status diff --git a/frappy/datatypes.py b/frappy/datatypes.py index 28dad55..c93de9f 100644 --- a/frappy/datatypes.py +++ b/frappy/datatypes.py @@ -1163,9 +1163,9 @@ class ValueType(DataType): The optional (callable) validator can be used to restrict values to a certain type. - For example using `ValueType(dict)` would ensure only values that can be + For example using ``ValueType(dict)`` would ensure only values that can be turned into a dictionary can be used in this instance, as the conversion - `dict(value)` is called for validation. + ``dict(value)`` is called for validation. Notes: The validator must either accept a value by returning it or the converted value, diff --git a/frappy/errors.py b/frappy/errors.py index 3b47068..68c9ebd 100644 --- a/frappy/errors.py +++ b/frappy/errors.py @@ -63,12 +63,15 @@ class SECoPError(RuntimeError): """format with info about raising methods :param stripped: strip last method. - Use stripped=True (or str()) for the following cases, as the last method can be derived from the context: - - stored in pobj.readerror: read_ - - error message from a change command: write_ - - error message from a read command: read_ - Use stripped=False for the log file, as the related parameter is not known :return: the formatted error message + + Use stripped=True (or str()) for the following cases, as the last method can be derived from the context: + + - stored in pobj.readerror: read_ + - error message from a change command: write_ + - error message from a read command: read_ + + Use stripped=False for the log file, as the related parameter is not known """ mlist = self.raising_methods if mlist and stripped: diff --git a/frappy/modules.py b/frappy/modules.py index 9844b2f..9f7a03e 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -629,6 +629,16 @@ class Module(HasAccessibles): mkthread(self.__pollThread, self.polledModules, start_events.get_trigger()) self.startModuleDone = True + def initialReads(self): + """initial reads to be done + + override to read initial values from HW, when it is not desired + to poll them afterwards + + called from the poll thread, after writeInitParams but before + all parameters are polled once + """ + def doPoll(self): """polls important parameters like value and status @@ -678,15 +688,10 @@ class Module(HasAccessibles): before polling, parameters which need hardware initialisation are written """ - for mobj in modules: - mobj.writeInitParams() - modules = [m for m in modules if m.enablePoll] - if not modules: # no polls needed - exit thread - started_callback() - return + polled_modules = [m for m in modules if m.enablePoll] if hasattr(self, 'registerReconnectCallback'): # self is a communicator supporting reconnections - def trigger_all(trg=self.triggerPoll, polled_modules=modules): + def trigger_all(trg=self.triggerPoll, polled_modules=polled_modules): for m in polled_modules: m.pollInfo.last_main = 0 m.pollInfo.last_slow = 0 @@ -694,7 +699,7 @@ class Module(HasAccessibles): self.registerReconnectCallback('trigger_polls', trigger_all) # collect all read functions - for mobj in modules: + for mobj in polled_modules: pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll) # trigger a poll interval change when self.pollinterval changes. if 'pollinterval' in mobj.valueCallbacks: @@ -704,17 +709,32 @@ class Module(HasAccessibles): rfunc = getattr(mobj, 'read_' + pname) if rfunc.poll: pinfo.polled_parameters.append((mobj, rfunc, pobj)) - # 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() + while True: + try: + for mobj in modules: + # TODO when needed: here we might add a call to a method :meth:`beforeWriteInit` + mobj.writeInitParams() + mobj.initialReads() + # call all read functions a first time + for m in polled_modules: + for mobj, rfunc, _ in m.pollInfo.polled_parameters: + mobj.callPollFunc(rfunc, raise_com_failed=True) + # TODO when needed: here we might add calls to a method :meth:`afterInitPolls` + break + 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 + if started_callback: + self.log.error('communication failure on startup: %s', e) + started_callback() + started_callback = None + self.triggerPoll.wait(0.1) # wait for reconnection or max 10 sec. + break + if started_callback: + started_callback() + if not polled_modules: # no polls needed - exit thread + return to_poll = () while True: now = time.time() diff --git a/test/test_statemachine.py b/test/test_statemachine.py index 2e2efa2..2a49021 100644 --- a/test/test_statemachine.py +++ b/test/test_statemachine.py @@ -249,15 +249,22 @@ class Mod(HasStates, Drivable): self._my_time += 1 +class Started(RuntimeError): + pass + + def create_module(): updates = [] obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates)) obj.initModule() obj.statelist = [] try: - obj._Module__pollThread(obj.polledModules, None) - except TypeError: - pass # None is not callable + def started(): + raise Started() + # run __pollThread until Started is raised (after initial phase) + obj._Module__pollThread(obj.polledModules, started) + except Started: + pass updates.clear() return obj, updates From 3786d2f209c3e75ba0f45c216fa179c2cb20a97a Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 19 Jun 2023 10:57:34 +0200 Subject: [PATCH 08/14] frappy_psi.triton: fix HeaterOutput.limit + fix handling of control_active Change-Id: Ic11933f6c1c4d9df07aa9d06ae4dca40b755e4ed Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31377 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- frappy_psi/triton.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/frappy_psi/triton.py b/frappy_psi/triton.py index 99f7f40..2ea892d 100644 --- a/frappy_psi/triton.py +++ b/frappy_psi/triton.py @@ -25,7 +25,7 @@ from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUS Done, Property from frappy.datatypes import EnumType, FloatRange, StringType from frappy.lib.enum import Enum -from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF +from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput from frappy_psi import mercury actions = Enum(none=0, condense=1, circulate=2, collect=3) @@ -256,15 +256,14 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop): ctrlpars = Parameter('pid (gain, integral (inv. time), differential time') system_channel = Property('system channel name', StringType(), 'MC') - def write_control_active(self, value): + def set_control_active(self, active): if self.system_channel: self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str) - if value: + if active: self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str) if self.output_module: - limit = self.output_module.read_limit() or None # None: max. limit + limit = self.output_module.read_limit() self.output_module.write_limit(limit) - return super().write_control_active(value) class HeaterOutput(HasInput, MercuryChannel, Writable): @@ -286,7 +285,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): return self.value def write_target(self, value): - self.write_controlled_by(SELF) + self.self_controlled() if self.resistivity: # round to the next voltage step value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity @@ -301,13 +300,12 @@ class HeaterOutputWithRange(HeaterOutput): def read_limit(self): maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA + if maxcur == 0: + maxcur = 100 # mA return self.read_resistivity() * maxcur ** 2 # uW def write_limit(self, value): - if value is None: - maxcur = 100 # max. allowed current 100mA - else: - maxcur = sqrt(value / self.read_resistivity()) + maxcur = sqrt(value / self.read_resistivity()) for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100: if cur > maxcur * 0.999: maxcur = cur From 7c95f1f8eef6be0499caf7824ec06b621b41505c Mon Sep 17 00:00:00 2001 From: Alexander Zaft Date: Mon, 12 Jun 2023 14:00:56 +0200 Subject: [PATCH 09/14] config: fix merge_modules Change-Id: I31d05afe300443e08fb08f9e6645401f52cfae39 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31323 Tested-by: Jenkins Automated Tests Reviewed-by: Alexander Zaft --- frappy/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappy/config.py b/frappy/config.py index 1b77f15..186e57a 100644 --- a/frappy/config.py +++ b/frappy/config.py @@ -125,7 +125,7 @@ class Config(dict): continue if name not in self.module_names: self.module_names.add(name) - self.modules.append(mod) + self[name] = mod def process_file(filename, log): From 8dc9c57e9d685d0b3acb6720e1b9de44b89cf1e0 Mon Sep 17 00:00:00 2001 From: Alexander Zaft Date: Mon, 12 Jun 2023 16:16:06 +0200 Subject: [PATCH 10/14] entangle: fix tango guards for pytango 9.3 Change-Id: I666969f9c798971d5cd8a0c2f6564067ac3cde72 Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31327 Tested-by: Jenkins Automated Tests Reviewed-by: Georg Brandl --- frappy_mlz/entangle.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frappy_mlz/entangle.py b/frappy_mlz/entangle.py index 16783a7..8a417b7 100644 --- a/frappy_mlz/entangle.py +++ b/frappy_mlz/entangle.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ***************************************************************************** # # This program is free software; you can redistribute it and/or modify it under @@ -257,12 +256,12 @@ class BasePyTangoDevice: Wraps command execution and attribute operations of the given device with logging and exception mapping. """ - dev.command_inout = self._applyGuardToFunc(dev.command_inout) - dev.write_attribute = self._applyGuardToFunc(dev.write_attribute, + dev.__dict__['command_inout'] = self._applyGuardToFunc(dev.command_inout) + dev.__dict__['write_attribute'] = self._applyGuardToFunc(dev.write_attribute, 'attr_write') - dev.read_attribute = self._applyGuardToFunc(dev.read_attribute, + dev.__dict__['read_attribute'] = self._applyGuardToFunc(dev.read_attribute, 'attr_read') - dev.attribute_query = self._applyGuardToFunc(dev.attribute_query, + dev.__dict__['attribute_query'] = self._applyGuardToFunc(dev.attribute_query, 'attr_query') return dev From b92095974b7d70b4aff1222b03a477d1090ebf02 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 20 Jun 2023 11:03:37 +0200 Subject: [PATCH 11/14] camea filter addon Change-Id: I1d80aa3bfc4e441ad8a69930b81d6cc25cee9511 --- cfg/addons/camea-be-filter_cfg.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 cfg/addons/camea-be-filter_cfg.py diff --git a/cfg/addons/camea-be-filter_cfg.py b/cfg/addons/camea-be-filter_cfg.py new file mode 100644 index 0000000..84f491c --- /dev/null +++ b/cfg/addons/camea-be-filter_cfg.py @@ -0,0 +1,18 @@ +Node('cfg/sea/camea-be-filter.cfg', + 'Camea Be-Filter', + interface='5000', + name='camea-be-filter', +) + +Mod('sea_addons', + 'secop_psi.sea.SeaClient', + 'addons sea connection for camea-be-filter.addon', + config='camea-be-filter.addon', + service='addons', +) + +Mod('t_be_filter', + 'secop_psi.sea.SeaReadable', + io='sea_addons', + sea_object='t_be_filter', +) From d7a1604bd55d7feee4324c870b36c66146dadd56 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Mon, 26 Jun 2023 14:45:53 +0200 Subject: [PATCH 12/14] frappy_psi.sea: auto connect on both .ssynio and /syncio try to reconnect after failure --- frappy_psi/sea.py | 61 +++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/frappy_psi/sea.py b/frappy_psi/sea.py index 5bc6276..c437a4e 100644 --- a/frappy_psi/sea.py +++ b/frappy_psi/sea.py @@ -111,6 +111,7 @@ class SeaClient(ProxyClient, Module): _connect_thread = None _service_manager = None _instance = None + _last_connect = 0 def __init__(self, name, log, opts, srv): nodename = srv.node_cfg.get('name') or srv.node_cfg.get('equipment_id') @@ -135,6 +136,10 @@ class SeaClient(ProxyClient, Module): ProxyClient.__init__(self) Module.__init__(self, name, log, opts, srv) + def doPoll(self): + if not self.asynio and time.time() > self._last_connect + 10: + self._connect_thread = mkthread(self._connect, None) + def register_obj(self, module, obj): self.objects.add(obj) for k, v in module.path2param.items(): @@ -146,6 +151,7 @@ class SeaClient(ProxyClient, Module): self._connect_thread = mkthread(self._connect, start_events.get_trigger()) def _connect(self, started_callback): + self._last_connect = time.time() if self._instance: if not self._service_manager: if self._service_manager is None: @@ -192,36 +198,36 @@ class SeaClient(ProxyClient, Module): self.syncio.writeline(b'seauser seaser') assert self.syncio.readline() == b'Login OK' self.log.info('connected to %s', self.uri) - self.syncio.flush_recv() - ft = 'fulltransAct' if quiet else 'fulltransact' - self.syncio.writeline(('%s %s' % (ft, command)).encode()) - result = None - deadline = time.time() + 10 - while time.time() < deadline: - try: + try: + self.syncio.flush_recv() + ft = 'fulltransAct' if quiet else 'fulltransact' + self.syncio.writeline(('%s %s' % (ft, command)).encode()) + result = None + deadline = time.time() + 10 + while time.time() < deadline: reply = self.syncio.readline() if reply is None: continue - except ConnectionClosed: - break - reply = reply.decode() - if reply.startswith('TRANSACTIONSTART'): - result = [] - continue - if reply == 'TRANSACTIONFINISHED': + reply = reply.decode() + if reply.startswith('TRANSACTIONSTART'): + result = [] + continue + if reply == 'TRANSACTIONFINISHED': + if result is None: + self.log.info('missing TRANSACTIONSTART on: %s', command) + return '' + if not result: + return '' + return '\n'.join(result) if result is None: - self.log.info('missing TRANSACTIONSTART on: %s', command) - return '' + self.log.info('swallow: %s', reply) + continue if not result: - return '' - return '\n'.join(result) - if result is None: - self.log.info('swallow: %s', reply) - continue - if not result: - result = [reply.split('=', 1)[-1]] - else: - result.append(reply) + result = [reply.split('=', 1)[-1]] + else: + result.append(reply) + except ConnectionClosed: + self.syncio = None raise TimeoutError('no response within 10s') def _rxthread(self, started_callback): @@ -231,6 +237,10 @@ class SeaClient(ProxyClient, Module): if reply is None: continue except ConnectionClosed: + try: + self.asynio.close() + except Exception: + self.asynio = None break try: msg = json.loads(reply) @@ -463,7 +473,6 @@ class SeaModule(Module): descr['params'].pop(0) else: # filter by relative paths - # rel_paths = rel_paths.split() result = [] is_running = None for rpath in rel_paths: From a8e1d0e1e899852ef40479325c10a3f6a914607e Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 29 Jun 2023 11:27:28 +0200 Subject: [PATCH 13/14] frappy_psi.sea: try to reconnect on failure both .asynio and .syncio connection should be tried to reopen. (fix from mlz gerrit) Change-Id: I0da5bd9927865a1c55afb93a7a5b76c44fc8750e --- frappy_psi/sea.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappy_psi/sea.py b/frappy_psi/sea.py index c437a4e..bf294f7 100644 --- a/frappy_psi/sea.py +++ b/frappy_psi/sea.py @@ -227,6 +227,10 @@ class SeaClient(ProxyClient, Module): else: result.append(reply) except ConnectionClosed: + try: + self.syncio.disconnect() + except Exception: + pass self.syncio = None raise TimeoutError('no response within 10s') @@ -238,9 +242,10 @@ class SeaClient(ProxyClient, Module): continue except ConnectionClosed: try: - self.asynio.close() + self.asynio.disconnect() except Exception: - self.asynio = None + pass + self.asynio = None break try: msg = json.loads(reply) From 714c820115f8a99ed42e039b02fe5ac9d3aec2a2 Mon Sep 17 00:00:00 2001 From: camea Date: Mon, 3 Jul 2023 17:49:06 +0200 Subject: [PATCH 14/14] fixes in ori3 and dil5 config --- cfg/main/ori3_cfg.py | 6 ++++++ cfg/sea/ori3.config.json | 14 ++++++++++++-- cfg/stick/dil5_cfg.py | 3 ++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cfg/main/ori3_cfg.py b/cfg/main/ori3_cfg.py index 82486de..7713010 100644 --- a/cfg/main/ori3_cfg.py +++ b/cfg/main/ori3_cfg.py @@ -58,6 +58,12 @@ Mod('hemot', sea_object='hemot', ) +Mod('nvflow', + 'frappy_psi.sea.SeaReadable', '', + io='sea_main', + sea_object='nvflow', +) + Mod('table', 'frappy_psi.sea.SeaReadable', '', io='sea_main', diff --git a/cfg/sea/ori3.config.json b/cfg/sea/ori3.config.json index c960299..230cff2 100644 --- a/cfg/sea/ori3.config.json +++ b/cfg/sea/ori3.config.json @@ -237,10 +237,10 @@ {"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": "running", "type": "bool", "readonly": false, "cmd": "hepump running"}, {"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": "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", "visibility": 3}, {"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3}, {"path": "health", "type": "float"}]}, @@ -278,6 +278,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"}]}, + "table": {"base": "/table", "params": [ {"path": "", "type": "none", "kids": 17}, {"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3}, diff --git a/cfg/stick/dil5_cfg.py b/cfg/stick/dil5_cfg.py index 1f5d38a..a31867b 100644 --- a/cfg/stick/dil5_cfg.py +++ b/cfg/stick/dil5_cfg.py @@ -7,7 +7,7 @@ Mod('triton', 'frappy_psi.mercury.IO', 'connection to triton software', uri='tcp://linse-dil5:33576', - timeout=5.0, + timeout=25.0, ) Mod('ts', @@ -23,6 +23,7 @@ Mod('htr_mix', 'mix. chamber heater', slot='H1,T5', io='triton', + resistivity = 100 ) Mod('htr_sorb',