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 <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2023-06-19 08:59:31 +02:00
parent 997e8e26e9
commit 138b84e84c
7 changed files with 96 additions and 31 deletions

View File

@ -8,7 +8,24 @@ what the framwork does for you.
Startup 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: .. _polling:

View File

@ -95,5 +95,23 @@ Example code:
return self.read_target() # return the read back value 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 .. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies

View File

@ -12,7 +12,7 @@ Module Base Classes
................... ...................
.. autoclass:: frappy.modules.Module .. autoclass:: frappy.modules.Module
:members: earlyInit, initModule, startModule :members: earlyInit, initModule, startModule, initialReads
.. autoclass:: frappy.modules.Readable .. autoclass:: frappy.modules.Readable
:members: Status :members: Status

View File

@ -1163,9 +1163,9 @@ class ValueType(DataType):
The optional (callable) validator can be used to restrict values to a The optional (callable) validator can be used to restrict values to a
certain type. 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 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: Notes:
The validator must either accept a value by returning it or the converted value, The validator must either accept a value by returning it or the converted value,

View File

@ -63,12 +63,15 @@ class SECoPError(RuntimeError):
"""format with info about raising methods """format with info about raising methods
:param stripped: strip last method. :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_<pobj.name>
- error message from a change command: write_<pname>
- error message from a read command: read_<pname>
Use stripped=False for the log file, as the related parameter is not known
:return: the formatted error message :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_<pobj.name>
- error message from a change command: write_<pname>
- error message from a read command: read_<pname>
Use stripped=False for the log file, as the related parameter is not known
""" """
mlist = self.raising_methods mlist = self.raising_methods
if mlist and stripped: if mlist and stripped:

View File

@ -629,6 +629,16 @@ class Module(HasAccessibles):
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger()) mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
self.startModuleDone = True 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): def doPoll(self):
"""polls important parameters like value and status """polls important parameters like value and status
@ -678,15 +688,10 @@ class Module(HasAccessibles):
before polling, parameters which need hardware initialisation are written before polling, parameters which need hardware initialisation are written
""" """
for mobj in modules: polled_modules = [m for m in modules if m.enablePoll]
mobj.writeInitParams()
modules = [m for m in modules if m.enablePoll]
if not modules: # no polls needed - exit thread
started_callback()
return
if hasattr(self, 'registerReconnectCallback'): if hasattr(self, 'registerReconnectCallback'):
# self is a communicator supporting reconnections # 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: for m in polled_modules:
m.pollInfo.last_main = 0 m.pollInfo.last_main = 0
m.pollInfo.last_slow = 0 m.pollInfo.last_slow = 0
@ -694,7 +699,7 @@ class Module(HasAccessibles):
self.registerReconnectCallback('trigger_polls', trigger_all) self.registerReconnectCallback('trigger_polls', trigger_all)
# collect all read functions # collect all read functions
for mobj in modules: for mobj in polled_modules:
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll) pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
# trigger a poll interval change when self.pollinterval changes. # trigger a poll interval change when self.pollinterval changes.
if 'pollinterval' in mobj.valueCallbacks: if 'pollinterval' in mobj.valueCallbacks:
@ -704,17 +709,32 @@ class Module(HasAccessibles):
rfunc = getattr(mobj, 'read_' + pname) rfunc = getattr(mobj, 'read_' + pname)
if rfunc.poll: if rfunc.poll:
pinfo.polled_parameters.append((mobj, rfunc, pobj)) pinfo.polled_parameters.append((mobj, rfunc, pobj))
# call all read functions a first time while True:
try: try:
for m in modules: for mobj in modules:
for mobj, rfunc, _ in m.pollInfo.polled_parameters: # TODO when needed: here we might add a call to a method :meth:`beforeWriteInit`
mobj.callPollFunc(rfunc, raise_com_failed=True) mobj.writeInitParams()
except CommunicationFailedError as e: mobj.initialReads()
# when communication failed, probably all parameters and may be more modules are affected. # call all read functions a first time
# as this would take a lot of time (summed up timeouts), we do not continue for m in polled_modules:
# trying and let the server accept connections, further polls might success later for mobj, rfunc, _ in m.pollInfo.polled_parameters:
self.log.error('communication failure on startup: %s', e) mobj.callPollFunc(rfunc, raise_com_failed=True)
started_callback() # 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 = () to_poll = ()
while True: while True:
now = time.time() now = time.time()

View File

@ -249,15 +249,22 @@ class Mod(HasStates, Drivable):
self._my_time += 1 self._my_time += 1
class Started(RuntimeError):
pass
def create_module(): def create_module():
updates = [] updates = []
obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates)) obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates))
obj.initModule() obj.initModule()
obj.statelist = [] obj.statelist = []
try: try:
obj._Module__pollThread(obj.polledModules, None) def started():
except TypeError: raise Started()
pass # None is not callable # run __pollThread until Started is raised (after initial phase)
obj._Module__pollThread(obj.polledModules, started)
except Started:
pass
updates.clear() updates.clear()
return obj, updates return obj, updates