From 138b84e84cb9e6de6db31c621dd4e0ef97a6a8ea Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 19 Jun 2023 08:59:31 +0200 Subject: [PATCH] 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