diff --git a/bin/secop-server b/bin/secop-server index f983bb5..4776545 100755 --- a/bin/secop-server +++ b/bin/secop-server @@ -94,6 +94,7 @@ def main(argv=None): generalConfig.defaults['lazy_number_validation'] = True generalConfig.defaults['disable_value_range_check'] = True generalConfig.defaults['legacy_hasiodev'] = True + generalConfig.defaults['tolerate_poll_property'] = True generalConfig.init(args.gencfg) logger.init(loglevel) diff --git a/secop/core.py b/secop/core.py index 73ee8bb..be3d338 100644 --- a/secop/core.py +++ b/secop/core.py @@ -33,8 +33,9 @@ from secop.lib.enum import Enum from secop.modules import Attached, Communicator, \ Done, Drivable, Module, Readable, Writable from secop.params import Command, Parameter -from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW from secop.properties import Property from secop.proxy import Proxy, SecNode, proxy_class from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff) from secop.persistent import PersistentMixin, PersistentParam +from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \ + CommonWriteHandler, nopoll diff --git a/secop/io.py b/secop/io.py index 27c8adc..48edf0c 100644 --- a/secop/io.py +++ b/secop/io.py @@ -35,7 +35,6 @@ from secop.errors import CommunicationFailedError, CommunicationSilentError, \ ConfigError, ProgrammingError from secop.modules import Attached, Command, \ Communicator, Done, Module, Parameter, Property -from secop.poller import REGULAR from secop.lib import generalConfig @@ -109,7 +108,7 @@ class IOBase(Communicator): uri = Property('hostname:portnumber', datatype=StringType()) timeout = Parameter('timeout', datatype=FloatRange(0), default=2) wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0) - is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False, poll=REGULAR) + is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False) pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10) _reconnectCallbacks = None @@ -133,6 +132,9 @@ class IOBase(Communicator): self._conn = None self.is_connected = False + def doPoll(self): + self.read_is_connected() + def read_is_connected(self): """try to reconnect, when not connected diff --git a/secop/lib/sequence.py b/secop/lib/sequence.py index 0fce746..5d416ee 100644 --- a/secop/lib/sequence.py +++ b/secop/lib/sequence.py @@ -153,7 +153,7 @@ class SequencerMixin: self._seq_error = str(e) finally: self._seq_thread = None - self.pollParams(0) + self.doPoll() def _seq_thread_inner(self, seq, store_init): store = Namespace() diff --git a/secop/modules.py b/secop/modules.py index eeacc21..122a215 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -23,8 +23,8 @@ """Define base classes for real Modules implemented in the server""" -import sys import time +import threading from collections import OrderedDict from functools import wraps @@ -35,7 +35,6 @@ from secop.errors import BadValueError, ConfigError, \ from secop.lib import formatException, mkthread, UniqueObject, generalConfig from secop.lib.enum import Enum from secop.params import Accessible, Command, Parameter -from secop.poller import BasicPoller, Poller from secop.properties import HasProperties, Property from secop.logging import RemoteLogHandler, HasComlog @@ -90,15 +89,18 @@ class HasAccessibles(HasProperties): else: aobj.merge(merged_properties[aname]) accessibles[aname] = aobj + # rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles # move (2) to the end - for aname in list(cls.__dict__.get('paramOrder', ())): + paramOrder = cls.__dict__.get('paramOrder', ()) + for aname in paramOrder: if aname in accessibles: accessibles.move_to_end(aname) # ignore unknown names # move (3) to the end for aname in new_names: - accessibles.move_to_end(aname) + if aname not in paramOrder: + accessibles.move_to_end(aname) # note: for python < 3.6 the order of inherited items is not ensured between # declarations within the same class cls.accessibles = accessibles @@ -147,7 +149,7 @@ class HasAccessibles(HasProperties): setattr(self, pname, value) # important! trigger the setter return value - new_rfunc.poll = getattr(rfunc, 'poll', True) and pobj.poll + new_rfunc.poll = getattr(rfunc, 'poll', True) else: def new_rfunc(self, pname=pname): @@ -268,6 +270,9 @@ class Module(HasAccessibles): extname='implementation') interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()), extname='interface_classes') + pollinterval = Property('poll interval for parameters handled by doPoll', FloatRange(0.1, 120), default=5) + slowinterval = Property('poll interval for other parameters', FloatRange(0.1, 120), default=15) + enablePoll = True # properties, parameters and commands are auto-merged upon subclassing parameters = {} @@ -275,7 +280,6 @@ class Module(HasAccessibles): # reference to the dispatcher (used for sending async updates) DISPATCHER = None - pollerClass = Poller #: default poller used def __init__(self, name, logger, cfgdict, srv): # remember the dispatcher object (for the async callbacks) @@ -289,6 +293,7 @@ class Module(HasAccessibles): self.initModuleDone = False self.startModuleDone = False self.remoteLogHandler = None + self.nextPollEvent = threading.Event() errors = [] # handle module properties @@ -333,13 +338,6 @@ class Module(HasAccessibles): for aname, aobj in self.accessibles.items(): # make a copy of the Parameter/Command object aobj = aobj.copy() - if isinstance(aobj, Parameter): - # fix default properties poll and needscfg - if aobj.poll is None: - aobj.poll = bool(aobj.handler) - if aobj.needscfg is None: - aobj.needscfg = not aobj.poll - if not self.export: # do not export parameters of a module not exported aobj.export = False if aobj.export: @@ -578,16 +576,25 @@ class Module(HasAccessibles): registers it in the server for waiting defaults to 30 seconds """ - if self.writeDict: - mkthread(self.writeInitParams, start_events.get_trigger()) + if self.enablePoll or self.writeDict: + # enablePoll == False: start poll thread for writing values from writeDict only + mkthread(self.__pollThread, start_events.get_trigger()) self.startModuleDone = True - def pollOneParam(self, pname): - """poll parameter with proper error handling""" + def doPoll(self): + """polls important parameters like value and status + + all other parameters are polled automatically + """ + + def triggerPollEvent(self, *args): # args needed for valueCallback + """interrupts waiting between polls""" + self.nextPollEvent.set() # trigger poll loop + + def callPollFunc(self, rfunc): + """call read method with proper error handling""" try: - rfunc = getattr(self, 'read_' + pname) - if rfunc.poll: # TODO: handle this in poller - rfunc() + rfunc() except SilentError: pass except SECoPError as e: @@ -595,6 +602,63 @@ class Module(HasAccessibles): except Exception: self.log.error(formatException()) + def __pollThread(self, started_callback): + self.writeInitParams() + if not self.enablePoll: + return + polled_parameters = [] + # collect and call all read functions a first time + for pname, pobj in self.parameters.items(): + rfunc = getattr(self, 'read_' + pname) + if rfunc.poll: + polled_parameters.append((rfunc, pobj)) + self.callPollFunc(rfunc) + started_callback() + last_slow = last_main = 0 + last_error = None + error_count = 0 + to_poll = () + while True: + now = time.time() + wait_main = last_main + self.pollinterval - now + wait_slow = last_slow + self.slowinterval - now + wait_time = min(wait_main, wait_slow) + if wait_time > 0: + self.nextPollEvent.wait(wait_time) + self.nextPollEvent.clear() + # remark: if there would be a need to trigger polling all parameters, + # we might replace nextPollEvent by a Queue and act depending on the + # queued item + continue + # call doPoll, if due + if wait_main <= 0: + last_main = (now // self.pollinterval) * self.pollinterval + try: + self.doPoll() + if last_error and error_count > 1: + self.log.info('recovered after %d calls to doPoll (%r)', error_count, last_error) + last_error = None + except Exception as e: + if type(e) != last_error: + error_count = 0 + self.log.error('error in doPoll: %r', e) + error_count += 1 + last_error = e + now = time.time() + # find ONE due slow poll and call it + loop = True + while loop: # loops max. 2 times, when to_poll is at end + for rfunc, pobj in to_poll: + if now > pobj.timestamp + self.slowinterval * 0.5: + self.callPollFunc(rfunc) + loop = False + break + else: + if now < last_slow + self.slowinterval: + break + last_slow = (now // self.slowinterval) * self.slowinterval + to_poll = iter(polled_parameters) + def writeInitParams(self, started_callback=None): """write values for parameters with configured values @@ -640,55 +704,20 @@ class Readable(Module): UNKNOWN=401, ) #: status codes - value = Parameter('current value of the module', FloatRange(), poll=True) + value = Parameter('current value of the module', FloatRange()) status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()), - default=(Status.IDLE, ''), poll=True) - pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120), - default=5, readonly=False) + default=(Status.IDLE, '')) + pollinterval = Parameter('default poll interval', FloatRange(0.1, 120), + default=5, readonly=False, export=True) - def startModule(self, start_events): - """start basic polling thread""" - if self.pollerClass and issubclass(self.pollerClass, BasicPoller): - # use basic poller for legacy code - mkthread(self.__pollThread, start_events.get_trigger(timeout=30)) - else: - super().startModule(start_events) + def earlyInit(self): + super().earlyInit() + # in case pollinterval is reduced a lot, we do not want to wait + self.valueCallbacks['pollinterval'].append(self.triggerPollEvent) - def __pollThread(self, started_callback): - while True: - try: - self.__pollThread_inner(started_callback) - except Exception as e: - self.log.exception(e) - self.status = (self.Status.ERROR, 'polling thread could not start') - started_callback() - print(formatException(0, sys.exc_info(), verbose=True)) - time.sleep(10) - - def __pollThread_inner(self, started_callback): - """super simple and super stupid per-module polling thread""" - self.writeInitParams() - i = 0 - fastpoll = self.pollParams(i) - started_callback() - while True: - i += 1 - try: - time.sleep(self.pollinterval * (0.1 if fastpoll else 1)) - except TypeError: - time.sleep(min(self.pollinterval) - if fastpoll else max(self.pollinterval)) - fastpoll = self.pollParams(i) - - def pollParams(self, nr=0): - # Just poll all parameters regularly where polling is enabled - for pname, pobj in self.parameters.items(): - if not pobj.poll: - continue - if nr % abs(int(pobj.poll)) == 0: - # pollParams every 'pobj.pollParams' iteration - self.pollOneParam(pname) - return False + def doPoll(self): + self.read_value() + self.read_status() class Writable(Readable): @@ -739,24 +768,6 @@ class Drivable(Writable): """ return 300 <= (status or self.status)[0] < 390 - # improved polling: may poll faster if module is BUSY - def pollParams(self, nr=0): - # poll status first - self.read_status() - fastpoll = self.isBusy() - for pname, pobj in self.parameters.items(): - if not pobj.poll: - continue - if pname == 'status': - # status was already polled above - continue - if ((int(pobj.poll) < 0) and fastpoll) or ( - nr % abs(int(pobj.poll))) == 0: - # poll always if pobj.poll is negative and fastpoll (i.e. Module is busy) - # otherwise poll every 'pobj.poll' iteration - self.pollOneParam(pname) - return fastpoll - @Command(None, result=None) def stop(self): """cease driving, go to IDLE state""" diff --git a/secop/params.py b/secop/params.py index 05f99d2..c592153 100644 --- a/secop/params.py +++ b/secop/params.py @@ -26,10 +26,11 @@ import inspect from secop.datatypes import BoolType, CommandType, DataType, \ - DataTypeType, EnumType, IntRange, NoneOr, OrType, \ + DataTypeType, EnumType, NoneOr, OrType, \ StringType, StructOf, TextType, TupleOf, ValueType from secop.errors import BadValueError, ProgrammingError from secop.properties import HasProperties, Property +from secop.lib import generalConfig class Accessible(HasProperties): @@ -132,24 +133,9 @@ class Parameter(Accessible): * True: exported, name automatic. * a string: exported with custom name''', OrType(BoolType(), StringType()), export=False, default=True) - poll = Property( - '''[internal] polling indicator - - may be: - - * None (omitted): will be converted to True/False if handler is/is not None - * False or 0 (never poll this parameter) - * True or 1 (AUTO), converted to SLOW (readonly=False) - DYNAMIC (*status* and *value*) or REGULAR (else) - * 2 (SLOW), polled with lower priority and a multiple of pollinterval - * 3 (REGULAR), polled with pollperiod - * 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, - else polled with pollperiod - ''', NoneOr(IntRange()), - export=False, default=None) needscfg = Property( '[internal] needs value in config', NoneOr(BoolType()), - export=False, default=None) + export=False, default=False) optional = Property( '[internal] is this parameter optional?', BoolType(), export=False, settable=False, default=False) @@ -169,6 +155,8 @@ class Parameter(Accessible): def __init__(self, description=None, datatype=None, inherit=True, **kwds): super().__init__() + if 'poll' in kwds and generalConfig.tolerate_poll_property: + kwds.pop('poll') if datatype is None: # collect datatype properties. these are not applied, as we have no datatype self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict} diff --git a/secop/poller.py b/secop/poller.py deleted file mode 100644 index 5ef57c3..0000000 --- a/secop/poller.py +++ /dev/null @@ -1,278 +0,0 @@ -# -*- coding: utf-8 -*- -# ***************************************************************************** -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Module authors: -# Markus Zolliker -# -# ***************************************************************************** -"""general, advanced frappy poller - -Usage examples: - any Module which want to be polled with a specific Poller must define - the pollerClass class variable: - - class MyModule(Readable): - ... - pollerClass = poller.Poller - ... - - modules having a parameter 'io' with the same value will share the same poller -""" - -import time -from heapq import heapify, heapreplace -from threading import Event - -from secop.errors import ProgrammingError -from secop.lib import mkthread - -# poll types: -AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC -SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False) -REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value) -DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value) - - -class PollerBase: - - startup_timeout = 30 # default timeout for startup - name = 'unknown' # to be overridden in implementors __init__ method - - @classmethod - def add_to_table(cls, table, module): - """sort module into poller table - - table is a dict, with (, ) as the key, and the - poller as value. - is module.io.name or module.name, if io is not present - """ - # for modules with the same io, a common poller is used, - # modules without io all get their own poller - name = getattr(module, 'io', module).name - poller = table.get((cls, name), None) - if poller is None: - poller = cls(name) - table[(cls, name)] = poller - poller.add_to_poller(module) - - def start(self, started_callback): - """start poller thread - - started_callback to be called after all poll items were read at least once - """ - mkthread(self.run, started_callback) - return self.startup_timeout - - def run(self, started_callback): - """poller thread function - - started_callback to be called after all poll items were read at least once - """ - raise NotImplementedError - - def stop(self): - """stop polling""" - raise NotImplementedError - - def __bool__(self): - """is there any poll item?""" - raise NotImplementedError - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, self.name) - - -class Poller(PollerBase): - """a standard poller - - parameters may have the following polltypes: - - - REGULAR: by default used for readonly parameters with poll=True - - SLOW: by default used for readonly=False parameters with poll=True. - slow polls happen with lower priority, but at least one parameter - is polled with regular priority within self.module.pollinterval. - Scheduled to poll every slowfactor * module.pollinterval - - DYNAMIC: by default used for 'value' and 'status' - When busy, scheduled to poll every fastfactor * module.pollinterval - """ - - DEFAULT_FACTORS = {SLOW: 4, DYNAMIC: 0.25, REGULAR: 1} - - def __init__(self, name): - """create a poller""" - self.queues = {polltype: [] for polltype in self.DEFAULT_FACTORS} - self._event = Event() - self._stopped = False - self.maxwait = 3600 - self.name = name - self.modules = [] # used for writeInitParams only - - def add_to_poller(self, module): - self.modules.append(module) - factors = self.DEFAULT_FACTORS.copy() - try: - factors[DYNAMIC] = module.fast_pollfactor - except AttributeError: - pass - try: - factors[SLOW] = module.slow_pollfactor - except AttributeError: - pass - self.maxwait = min(self.maxwait, getattr(module, 'max_polltestperiod', 10)) - try: - self.startup_timeout = max(self.startup_timeout, module.startup_timeout) - except AttributeError: - pass - handlers = set() - # at the beginning, queues are simple lists - # later, they will be converted to heaps - for pname, pobj in module.parameters.items(): - polltype = pobj.poll - if not polltype: - continue - if not hasattr(module, 'pollinterval'): - raise ProgrammingError("module %s must have a pollinterval" - % module.name) - if pname == 'is_connected': - if hasattr(module, 'registerReconnectCallback'): - module.registerReconnectCallback(self.name, self.trigger_all) - else: - module.log.warning("%r has 'is_connected' but no 'registerReconnectCallback'" % module) - if polltype == AUTO: # covers also pobj.poll == True - if pname in ('value', 'status'): - polltype = DYNAMIC - elif pobj.readonly: - polltype = REGULAR - else: - polltype = SLOW - if polltype not in factors: - raise ProgrammingError("unknown poll type %r for parameter '%s'" - % (polltype, pname)) - if pobj.handler: - if pobj.handler in handlers: - continue # only one poller per handler - handlers.add(pobj.handler) - # placeholders 0 are used for due, lastdue and idx - self.queues[polltype].append( - (0, 0, (0, module, pobj, pname, factors[polltype]))) - - def poll_next(self, polltype): - """try to poll next item - - advance in queue until - - an item is found which is really due to poll. return 0 in this case - - or until the next item is not yet due. return next due time in this case - """ - queue = self.queues[polltype] - if not queue: - return float('inf') # queue is empty - now = time.time() - done = False - while not done: - due, lastdue, pollitem = queue[0] - if now < due: - return due - _, module, pobj, pname, factor = pollitem - - if polltype == DYNAMIC and not module.isBusy(): - interval = module.pollinterval # effective interval - mininterval = interval * factor # interval for calculating next due - else: - interval = module.pollinterval * factor - mininterval = interval - if due == 0: - due = now # do not look at timestamp after trigger_all - else: - due = max(lastdue + interval, pobj.timestamp + interval * 0.5) - if now >= due: - module.pollOneParam(pname) - done = True - lastdue = due - due = max(lastdue + mininterval, now + min(self.maxwait, mininterval * 0.5)) - # replace due, lastdue with new values and sort in - heapreplace(queue, (due, lastdue, pollitem)) - return 0 - - def trigger_all(self): - for _, queue in sorted(self.queues.items()): - for idx, (_, lastdue, pollitem) in enumerate(queue): - queue[idx] = (0, lastdue, pollitem) - self._event.set() - return True - - def run(self, started_callback): - """start poll loop - - To be called as a thread. After all parameters are polled once first, - started_callback is called. To be called in Module.startModule. - - poll strategy: - Slow polls are performed with lower priority than regular and dynamic polls. - If more polls are scheduled than time permits, at least every second poll is a - dynamic poll. After every n regular polls, one slow poll is done, if due - (where n is the number of regular parameters). - """ - if not self: - # nothing to do (else time.sleep(float('inf')) might be called below - started_callback() - return - # if writeInitParams is not yet done, we do it here - for module in self.modules: - module.writeInitParams() - # do all polls once and, at the same time, insert due info - for _, queue in sorted(self.queues.items()): # do SLOW polls first - for idx, (_, _, (_, module, pobj, pname, factor)) in enumerate(queue): - lastdue = time.time() - module.pollOneParam(pname) - due = lastdue + min(self.maxwait, module.pollinterval * factor) - # in python 3 comparing tuples need some care, as not all objects - # are comparable. Inserting a unique idx solves the problem. - queue[idx] = (due, lastdue, (idx, module, pobj, pname, factor)) - heapify(queue) - started_callback() # signal end of startup - nregular = len(self.queues[REGULAR]) - while not self._stopped: - due = float('inf') - for _ in range(nregular): - due = min(self.poll_next(DYNAMIC), self.poll_next(REGULAR)) - if due: - break # no dynamic or regular polls due - due = min(due, self.poll_next(DYNAMIC), self.poll_next(SLOW)) - delay = due - time.time() - if delay > 0: - self._event.wait(delay) - self._event.clear() - - def stop(self): - self._event.set() - self._stopped = True - - def __bool__(self): - """is there any poll item?""" - return any(self.queues.values()) - - -class BasicPoller(PollerBase): - """basic poller - - this is just a dummy, the poller thread is started in Readable.startModule - """ - # pylint: disable=abstract-method - - @classmethod - def add_to_table(cls, table, module): - pass diff --git a/secop/properties.py b/secop/properties.py index 74ba68a..f12d5a9 100644 --- a/secop/properties.py +++ b/secop/properties.py @@ -138,17 +138,18 @@ class HasProperties(HasDescriptors): # treat overriding properties with bare values for pn, po in properties.items(): value = getattr(cls, pn, po) - if not isinstance(value, Property): # attribute is a bare value + if not isinstance(value, (Property, HasProperties)): # attribute may be a bare value + # HasProperties is a base class of Parameter -> allow a Parameter to override a Property () po = po.copy() try: + # try to apply bare value to Property po.value = po.datatype(value) except BadValueError: - if pn in properties: - if callable(value): - raise ProgrammingError('method %s.%s collides with property of %s' % - (cls.__name__, pn, base.__name__)) from None - raise ProgrammingError('can not set property %s.%s to %r' % - (cls.__name__, pn, value)) from None + if callable(value): + raise ProgrammingError('method %s.%s collides with property of %s' % + (cls.__name__, pn, base.__name__)) from None + raise ProgrammingError('can not set property %s.%s to %r' % + (cls.__name__, pn, value)) from None cls.propertyDict[pn] = po def checkProperties(self): diff --git a/secop/proxy.py b/secop/proxy.py index 05a4292..7189fa8 100644 --- a/secop/proxy.py +++ b/secop/proxy.py @@ -35,9 +35,9 @@ from secop.io import HasIO class ProxyModule(HasIO, Module): module = Property('remote module name', datatype=StringType(), default='') - pollerClass = None _consistency_check_done = False _secnode = None + enablePoll = False def ioClass(self, name, logger, opts, srv): opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri']) @@ -123,7 +123,8 @@ class ProxyModule(HasIO, Module): self.announceUpdate('status', newstatus) def checkProperties(self): - pass # skip + pass # skip + class ProxyReadable(ProxyModule, Readable): pass @@ -184,7 +185,7 @@ def proxy_class(remote_class, name=None): for aname, aobj in rcls.accessibles.items(): if isinstance(aobj, Parameter): - pobj = aobj.merge(dict(poll=False, handler=None, needscfg=False)) + pobj = aobj.merge(dict(handler=None, needscfg=False)) attrs[aname] = pobj def rfunc(self, pname=aname): diff --git a/secop/server.py b/secop/server.py index 9734e1d..d6253f8 100644 --- a/secop/server.py +++ b/secop/server.py @@ -264,7 +264,6 @@ class Server: failure_traceback = traceback.format_exc() errors.append('error creating %s' % modname) - poll_table = dict() missing_super = set() # all objs created, now start them up and interconnect for modname, modobj in self.modules.items(): @@ -276,12 +275,6 @@ class Server: missing_super.add('%s was not called, probably missing super call' % modobj.earlyInit.__qualname__) - # handle polling - for modname, modobj in self.modules.items(): - if modobj.pollerClass is not None: - # a module might be explicitly excluded from polling by setting pollerClass to None - modobj.pollerClass.add_to_table(poll_table, modobj) - # call init on each module after registering all for modname, modobj in self.modules.items(): try: @@ -317,17 +310,13 @@ class Server: sys.stderr.write(failure_traceback) sys.exit(1) - for (_, pollname) , poller in poll_table.items(): - start_events.name = 'poller %s' % pollname - # poller.start must return either a timeout value or None (default 30 sec) - poller.start(start_events.get_trigger()) - self.log.info('waiting for modules and pollers being started') + self.log.info('waiting for modules being started') start_events.name = None if not start_events.wait(): # some timeout happened for name in start_events.waiting_for(): self.log.warning('timeout when starting %s' % name) - self.log.info('all modules and pollers started') + self.log.info('all modules started') history_path = os.environ.get('FRAPPY_HISTORY') if history_path: from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel diff --git a/secop/simulation.py b/secop/simulation.py index cb79fcf..c974ff9 100644 --- a/secop/simulation.py +++ b/secop/simulation.py @@ -27,13 +27,10 @@ from time import sleep from secop.datatypes import FloatRange from secop.lib import mkthread -from secop.modules import BasicPoller, Drivable, \ - Module, Parameter, Readable, Writable, Command +from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command class SimBase: - pollerClass = BasicPoller - def __new__(cls, devname, logger, cfgdict, dispatcher): extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '') attrs = {} @@ -120,7 +117,7 @@ class SimDrivable(SimReadable, Drivable): self._value = self.target speed *= self.interval try: - self.pollParams(0) + self.doPoll() except Exception: pass @@ -133,7 +130,7 @@ class SimDrivable(SimReadable, Drivable): self._value = self.target sleep(self.interval) try: - self.pollParams(0) + self.doPoll() except Exception: pass self.status = self.Status.IDLE, '' diff --git a/secop_mlz/amagnet.py b/secop_mlz/amagnet.py index 14ab47c..9763fbb 100644 --- a/secop_mlz/amagnet.py +++ b/secop_mlz/amagnet.py @@ -31,7 +31,7 @@ import math from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf from secop.errors import ConfigError, DisabledError from secop.lib.sequence import SequencerMixin, Step -from secop.modules import BasicPoller, Drivable, Parameter +from secop.modules import Drivable, Parameter class GarfieldMagnet(SequencerMixin, Drivable): @@ -47,9 +47,6 @@ class GarfieldMagnet(SequencerMixin, Drivable): the symmetry setting selects which. """ - pollerClass = BasicPoller - - # parameters subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False) subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False) @@ -57,10 +54,10 @@ class GarfieldMagnet(SequencerMixin, Drivable): subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False) userlimits = Parameter('User defined limits of device value', datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), - default=(float('-Inf'), float('+Inf')), readonly=False, poll=10) + default=(float('-Inf'), float('+Inf')), readonly=False) abslimits = Parameter('Absolute limits of device value', datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), - default=(-0.5, 0.5), poll=True, + default=(-0.5, 0.5), ) precision = Parameter('Precision of the device value (allowed deviation ' 'of stable values from target)', @@ -71,7 +68,7 @@ class GarfieldMagnet(SequencerMixin, Drivable): calibration = Parameter('Coefficients for calibration ' 'function: [c0, c1, c2, c3, c4] calculates ' 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' - ' in T', poll=1, + ' in T', datatype=ArrayOf(FloatRange(), 5, 5), default=(1.0, 0.0, 0.0, 0.0, 0.0)) calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting', diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index b5b1ec8..f6b289b 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -39,7 +39,7 @@ from secop.datatypes import ArrayOf, EnumType, FloatRange, \ from secop.errors import CommunicationFailedError, \ ConfigError, HardwareError, ProgrammingError from secop.lib import lazy_property -from secop.modules import BasicPoller, Command, \ +from secop.modules import Command, \ Drivable, Module, Parameter, Readable ##### @@ -157,8 +157,6 @@ class PyTangoDevice(Module): execution and attribute operations with logging and exception mapping. """ - pollerClass = BasicPoller - # parameters comtries = Parameter('Maximum retries for communication', datatype=IntRange(1, 100), default=3, readonly=False, @@ -425,7 +423,7 @@ class AnalogOutput(PyTangoDevice, Drivable): userlimits = Parameter('User defined limits of device value', datatype=LimitsType(FloatRange(unit='$')), default=(float('-Inf'), float('+Inf')), - readonly=False, poll=10, + readonly=False, ) abslimits = Parameter('Absolute limits of device value', datatype=LimitsType(FloatRange(unit='$')), @@ -463,8 +461,8 @@ class AnalogOutput(PyTangoDevice, Drivable): if attrInfo.unit != 'No unit': self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit) - def pollParams(self, nr=0): - super().pollParams(nr) + def doPoll(self): + super().doPoll() while len(self._history) > 2: # if history would be too short, break if self._history[-1][0] - self._history[1][0] <= self.window: @@ -607,7 +605,6 @@ class Actuator(AnalogOutput): ) ramp = Parameter('The speed of changing the value', readonly=False, datatype=FloatRange(0, unit='$/min'), - poll=30, ) def read_speed(self): @@ -686,17 +683,22 @@ class TemperatureController(Actuator): ) pid = Parameter('pid control Parameters', datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()), - readonly=False, group='pid', poll=30, + readonly=False, group='pid', ) - setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1, + setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), ) - heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1, + heateroutput = Parameter('Heater output', datatype=FloatRange(), ) # overrides precision = Parameter(default=0.1) ramp = Parameter(description='Temperature ramp') + def doPoll(self): + super().doPoll() + self.read_setpoint() + self.read_heateroutput() + def read_ramp(self): return self._dev.ramp @@ -750,13 +752,19 @@ class PowerSupply(Actuator): # parameters voltage = Parameter('Actual voltage', - datatype=FloatRange(unit='V'), poll=-5) + datatype=FloatRange(unit='V')) current = Parameter('Actual current', - datatype=FloatRange(unit='A'), poll=-5) + datatype=FloatRange(unit='A')) # overrides ramp = Parameter(description='Current/voltage ramp') + def doPoll(self): + super().doPoll() + # TODO: poll voltage and current faster when busy + self.read_voltage() + self.read_current() + def read_ramp(self): return self._dev.ramp diff --git a/secop_psi/ah2700.py b/secop_psi/ah2700.py index f2179fd..df403ee 100644 --- a/secop_psi/ah2700.py +++ b/secop_psi/ah2700.py @@ -20,7 +20,7 @@ # ***************************************************************************** """Andeen Hagerling capacitance bridge""" -from secop.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO +from secop.core import Done, FloatRange, HasIO, Parameter, Readable, StringIO, nopoll class Ah2700IO(StringIO): @@ -30,7 +30,7 @@ class Ah2700IO(StringIO): class Capacitance(HasIO, Readable): - value = Parameter('capacitance', FloatRange(unit='pF'), poll=True) + value = Parameter('capacitance', FloatRange(unit='pF')) freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0) voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0) loss = Parameter('loss', FloatRange(unit='deg'), default=0) @@ -69,14 +69,17 @@ class Capacitance(HasIO, Readable): self.parse_reply(self.communicate('SI')) # SI = single trigger return Done + @nopoll def read_freq(self): self.read_value() return Done + @nopoll def read_loss(self): self.read_value() return Done + @nopoll def read_voltage(self): self.read_value() return Done diff --git a/secop_psi/k2601b.py b/secop_psi/k2601b.py index 99b10d9..9cb0dd1 100644 --- a/secop_psi/k2601b.py +++ b/secop_psi/k2601b.py @@ -43,11 +43,11 @@ SOURCECMDS = { class SourceMeter(HasIO, Module): - resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True) - power = Parameter('readback power', FloatRange(unit='W'), poll=True) + resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm')) + power = Parameter('readback power', FloatRange(unit='W')) mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), readonly=False, default=0) - active = Parameter('output enable', BoolType(), readonly=False, poll=True) + active = Parameter('output enable', BoolType(), readonly=False) ioClass = K2601bIO @@ -76,10 +76,10 @@ class SourceMeter(HasIO, Module): class Current(HasIO, Writable): sourcemeter = Attached() - value = Parameter('measured current', FloatRange(unit='A'), poll=True) - target = Parameter('set current', FloatRange(unit='A'), poll=True) + value = Parameter('measured current', FloatRange(unit='A')) + target = Parameter('set current', FloatRange(unit='A')) active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage - limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True) + limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2) def read_value(self): return self.communicate('print(smua.measure.i())') @@ -119,10 +119,10 @@ class Current(HasIO, Writable): class Voltage(HasIO, Writable): sourcemeter = Attached() - value = Parameter('measured voltage', FloatRange(unit='V'), poll=True) - target = Parameter('set voltage', FloatRange(unit='V'), poll=True) - active = Parameter('voltage is controlled', BoolType(), poll=True) - limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True) + value = Parameter('measured voltage', FloatRange(unit='V')) + target = Parameter('set voltage', FloatRange(unit='V')) + active = Parameter('voltage is controlled', BoolType()) + limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2) def read_value(self): return self.communicate('print(smua.measure.v())') diff --git a/secop_psi/ls370res.py b/secop_psi/ls370res.py index 0855fd8..026ac01 100644 --- a/secop_psi/ls370res.py +++ b/secop_psi/ls370res.py @@ -27,7 +27,6 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange from secop.lib import formatStatusBits from secop.modules import Attached, Done, \ Drivable, Parameter, Property, Readable -from secop.poller import REGULAR, Poller from secop.io import HasIO Status = Drivable.Status @@ -60,12 +59,11 @@ class StringIO(secop.io.StringIO): class Main(HasIO, Drivable): - value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17)) + value = Parameter('the current channel', datatype=IntRange(0, 17)) target = Parameter('channel to select', datatype=IntRange(0, 17)) autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False) pollinterval = Parameter(default=1, export=False) - pollerClass = Poller ioClass = StringIO _channel_changed = 0 # time of last channel change _channels = None # dict of @@ -141,7 +139,6 @@ class ResChannel(HasIO, Readable): enumerate(mag % val for mag in ['%guV', '%gmV'] for val in [2, 6.32, 20, 63.2, 200, 632]))} - pollerClass = Poller ioClass = StringIO _main = None # main module _last_range_change = 0 # time of last range change diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 180b74b..1fe265f 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -42,7 +42,6 @@ from secop.lib import clamp from secop.lib.enum import Enum from secop.modules import Communicator, Done, \ Drivable, Parameter, Property, Readable -from secop.poller import Poller from secop.io import HasIO from secop.rwhandler import CommonReadHandler, CommonWriteHandler @@ -57,7 +56,7 @@ class Main(Communicator): """ppms communicator module""" pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2) - data = Parameter('internal', StringType(), poll=True, export=True, # export for test only + data = Parameter('internal', StringType(), export=True, # export for test only default="", readonly=True) class_id = Property('Quantum Design class id', StringType(), export=False) @@ -70,8 +69,6 @@ class Main(Communicator): _channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names))) _status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12} - pollerClass = Poller - def earlyInit(self): super().earlyInit() self.modules = {} @@ -89,6 +86,9 @@ class Main(Communicator): self.comLog("< %s", reply) return reply + def doPoll(self): + self.read_data() + def read_data(self): mask = 1 # always get packed_status for channelname, channel in self.modules.items(): @@ -116,12 +116,9 @@ class Main(Communicator): class PpmsBase(HasIO, Readable): """common base for all ppms modules""" - # polling is done by the main module - # and PPMS does not deliver really more fresh values when polled more often - value = Parameter(poll=False, needscfg=False) - status = Parameter(poll=False, needscfg=False) + value = Parameter(needscfg=False) + status = Parameter(needscfg=False) - pollerClass = Poller enabled = True # default, if no parameter enable is defined _last_settings = None # used by several modules slow_pollfactor = 1 @@ -134,6 +131,11 @@ class PpmsBase(HasIO, Readable): super().initModule() self.io.register(self) + def doPoll(self): + # polling is done by the main module + # and PPMS does not deliver really more fresh values when polled more often + pass + def update_value_status(self, value, packed_status): # update value and status # to be reimplemented for modules looking at packed_status @@ -157,7 +159,7 @@ class Channel(PpmsBase): """channel base class""" value = Parameter('main value of channels') - enabled = Parameter('is this channel used?', readonly=False, poll=False, + enabled = Parameter('is this channel used?', readonly=False, datatype=BoolType(), default=False) channel = Property('channel name', @@ -189,7 +191,7 @@ class UserChannel(Channel): class DriverChannel(Channel): """driver channel""" - current = Parameter('driver current', readonly=False, poll=True, # poll only one parameter + current = Parameter('driver current', readonly=False, datatype=FloatRange(0., 5000., unit='uA')) powerlimit = Parameter('power limit', readonly=False, datatype=FloatRange(0., 1000., unit='uW')) @@ -217,7 +219,7 @@ class DriverChannel(Channel): class BridgeChannel(Channel): """bridge channel""" - excitation = Parameter('excitation current', readonly=False, poll=True, # poll only one parameter + excitation = Parameter('excitation current', readonly=False, datatype=FloatRange(0.01, 5000., unit='uA')) powerlimit = Parameter('power limit', readonly=False, datatype=FloatRange(0.001, 1000., unit='uW')) @@ -263,10 +265,13 @@ class BridgeChannel(Channel): class Level(PpmsBase): """helium level""" - value = Parameter(datatype=FloatRange(unit='%'), poll=True) + value = Parameter(datatype=FloatRange(unit='%')) channel = 'level' + def doPoll(self): + self.read_value() + def update_value_status(self, value, packed_status): pass # must be a no-op @@ -303,8 +308,8 @@ class Chamber(PpmsBase, Drivable): name2opcode = {k: v for _, _, _, v, k in code_table if k} opcode2name = {v: k for _, _, _, v, k in code_table if k} status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table} - value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0, poll=True) - target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop', poll=True) + value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0) + target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop') channel = 'chamber' @@ -339,9 +344,9 @@ class Temp(PpmsBase, Drivable): ) value = Parameter(datatype=FloatRange(unit='K')) status = Parameter(datatype=StatusType(Status)) - target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False) + target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), needscfg=False) setpoint = Parameter('intermediate set point', - datatype=FloatRange(1.7, 402.0, unit='K'), poll=True) # poll only one parameter + datatype=FloatRange(1.7, 402.0, unit='K')) ramp = Parameter('ramping speed', readonly=False, default=0, datatype=FloatRange(0, 20, unit='K/min')) workingramp = Parameter('intermediate ramp value', @@ -509,7 +514,7 @@ class Field(PpmsBase, Drivable): ) value = Parameter(datatype=FloatRange(unit='T')) status = Parameter(datatype=StatusType(Status)) - target = Parameter(datatype=FloatRange(-15, 15, unit='T'), poll=True) # poll only one parameter + target = Parameter(datatype=FloatRange(-15, 15, unit='T')) # poll only one parameter ramp = Parameter('ramping speed', readonly=False, datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19) approachmode = Parameter('how to approach target', readonly=False, @@ -640,8 +645,8 @@ class Position(PpmsBase, Drivable): Status = Drivable.Status value = Parameter(datatype=FloatRange(unit='deg')) - target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), poll=True) # poll only one parameter - enabled = Parameter('is this channel used?', readonly=False, poll=False, + target = Parameter(datatype=FloatRange(-720., 720., unit='deg')) + enabled = Parameter('is this channel used?', readonly=False, datatype=BoolType(), default=True) speed = Parameter('motor speed', readonly=False, default=12, datatype=FloatRange(0.8, 12, unit='deg/sec')) diff --git a/secop_psi/softcal.py b/secop_psi/softcal.py index e7baf00..d3bcefb 100644 --- a/secop_psi/softcal.py +++ b/secop_psi/softcal.py @@ -160,9 +160,9 @@ class Sensor(Readable): pollinterval = Parameter(export=False) status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) - pollerClass = None description = 'a calibrated sensor value' _value_error = None + enablePoll = False def initModule(self): self._rawsensor.registerCallbacks(self, ['status']) # auto update status diff --git a/secop_psi/trinamic.py b/secop_psi/trinamic.py index 57ba270..9b3edad 100644 --- a/secop_psi/trinamic.py +++ b/secop_psi/trinamic.py @@ -74,7 +74,7 @@ STEPPOS_ADR = 1 def writable(*args, **kwds): """convenience function to create writable hardware parameters""" - return PersistentParam(*args, readonly=False, poll=True, initwrite=True, **kwds) + return PersistentParam(*args, readonly=False, initwrite=True, **kwds) class Motor(PersistentMixin, HasIO, Drivable): @@ -83,9 +83,9 @@ class Motor(PersistentMixin, HasIO, Drivable): value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f')) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0) encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), - readonly=True, initwrite=False, poll=True) + readonly=True, initwrite=False) steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'), - readonly=True, initwrite=False, poll=True) + readonly=True, initwrite=False) target = Parameter('', FloatRange(unit='$'), default=0) move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'), @@ -98,26 +98,26 @@ class Motor(PersistentMixin, HasIO, Drivable): minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=SPEED_SCALE, group='motorparam') currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), - poll=True, group='motorparam') + group='motorparam') maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'), default=1.4, group='motorparam') standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'), default=0.1, group='motorparam') acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'), default=150., group='motorparam') - target_reached = Parameter('', BoolType(), poll=True, group='hwstatus') - move_status = Parameter('', IntRange(0, 3), poll=True, group='hwstatus') - error_bits = Parameter('', IntRange(0, 255), poll=True, group='hwstatus') + target_reached = Parameter('', BoolType(), group='hwstatus') + move_status = Parameter('', IntRange(0, 3), group='hwstatus') + error_bits = Parameter('', IntRange(0, 255), group='hwstatus') free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'), default=0.1, group='motorparam') power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'), default=0.1, group='motorparam') baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}), - readonly=False, default=0, poll=True, visibility=3, group='more') + readonly=False, default=0, visibility=3, group='more') pollinterval = Parameter(group='more') ioClass = BytesIO - fast_pollfactor = 0.001 # poll as fast as possible when busy + fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running _started = 0 _calcTimeout = True _need_reset = None diff --git a/test/test_handler.py b/test/test_handler.py index 447be0d..6d00fa7 100644 --- a/test/test_handler.py +++ b/test/test_handler.py @@ -69,8 +69,8 @@ def test_handler(): data = [] class Mod(ModuleTest): - a = Parameter('', FloatRange(), readonly=False, poll=True) - b = Parameter('', FloatRange(), readonly=False, poll=True) + a = Parameter('', FloatRange(), readonly=False) + b = Parameter('', FloatRange(), readonly=False) @ReadHandler(['a', 'b']) def read_hdl(self, pname): @@ -115,8 +115,8 @@ def test_common_handler(): data = [] class Mod(ModuleTest): - a = Parameter('', FloatRange(), readonly=False, poll=True) - b = Parameter('', FloatRange(), readonly=False, poll=True) + a = Parameter('', FloatRange(), readonly=False) + b = Parameter('', FloatRange(), readonly=False) @CommonReadHandler(['a', 'b']) def read_hdl(self): @@ -164,8 +164,8 @@ def test_common_handler(): def test_nopoll(): class Mod1(ModuleTest): - a = Parameter('', FloatRange(), readonly=False, poll=True) - b = Parameter('', FloatRange(), readonly=False, poll=True) + a = Parameter('', FloatRange(), readonly=False) + b = Parameter('', FloatRange(), readonly=False) @ReadHandler(['a', 'b']) def read_hdl(self): @@ -175,8 +175,8 @@ def test_nopoll(): assert Mod1.read_b.poll is True class Mod2(ModuleTest): - a = Parameter('', FloatRange(), readonly=False, poll=True) - b = Parameter('', FloatRange(), readonly=False, poll=True) + a = Parameter('', FloatRange(), readonly=False) + b = Parameter('', FloatRange(), readonly=False) @CommonReadHandler(['a', 'b']) def read_hdl(self): @@ -186,8 +186,8 @@ def test_nopoll(): assert Mod2.read_b.poll is False class Mod3(ModuleTest): - a = Parameter('', FloatRange(), readonly=False, poll=True) - b = Parameter('', FloatRange(), readonly=False, poll=True) + a = Parameter('', FloatRange(), readonly=False) + b = Parameter('', FloatRange(), readonly=False) @ReadHandler(['a', 'b']) @nopoll @@ -198,8 +198,8 @@ def test_nopoll(): assert Mod3.read_b.poll is False class Mod4(ModuleTest): - a = Parameter('', FloatRange(), readonly=False, poll=True) - b = Parameter('', FloatRange(), readonly=False, poll=True) + a = Parameter('', FloatRange(), readonly=False) + b = Parameter('', FloatRange(), readonly=False) @nopoll @ReadHandler(['a', 'b']) diff --git a/test/test_modules.py b/test/test_modules.py index 794147d..178d222 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -22,15 +22,15 @@ # ***************************************************************************** """test data types.""" +import sys +import threading import pytest from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger from secop.errors import ProgrammingError, ConfigError from secop.modules import Communicator, Drivable, Readable, Module from secop.params import Command, Parameter -from secop.poller import BasicPoller -from secop.lib.multievent import MultiEvent -from secop.rwhandler import ReadHandler, WriteHandler +from secop.rwhandler import ReadHandler, WriteHandler, nopoll from secop.lib import generalConfig @@ -67,11 +67,19 @@ class ServerStub: self.dispatcher = DispatcherStub(updates) +class DummyMultiEvent(threading.Event): + def get_trigger(self): + def trigger(event=self): + event.set() + sys.exit() + return trigger + + def test_Communicator(): o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({})) o.earlyInit() o.initModule() - event = MultiEvent() + event = DummyMultiEvent() o.startModule(event) assert event.is_set() # event should be set immediately @@ -96,8 +104,6 @@ def test_ModuleMagic(): """another stuff""" return not arg - pollerClass = BasicPoller - def read_param1(self): return True @@ -107,6 +113,7 @@ def test_ModuleMagic(): def read_a1(self): return True + @nopoll def read_a2(self): return True @@ -140,8 +147,10 @@ def test_ModuleMagic(): value = Parameter(datatype=FloatRange(unit='deg')) target = Parameter(datatype=FloatRange(), default=0) a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False) + # remark: it might be a programming error to override the datatype + # and not overriding the read_* method. This is not checked! b2 = Parameter('', datatype=BoolType(), default=True, - poll=True, readonly=False, initwrite=True) + readonly=False, initwrite=True) def write_a1(self, value): self._a1_written = value @@ -182,12 +191,13 @@ def test_ModuleMagic(): 'value': 'first'} assert updates.pop('o1') == expectedBeforeStart o1.earlyInit() - event = MultiEvent() + event = DummyMultiEvent() o1.startModule(event) event.wait() # should contain polled values - expectedAfterStart = {'status': (Drivable.Status.IDLE, 'ok'), - 'value': 'second'} + expectedAfterStart = { + 'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second', + 'param1': True, 'param2': 0.0, 'a1': True} assert updates.pop('o1') == expectedAfterStart # check in addition if parameters are written @@ -197,11 +207,12 @@ def test_ModuleMagic(): expectedBeforeStart['target'] = 0.0 assert updates.pop('o2') == expectedBeforeStart o2.earlyInit() - event = MultiEvent() + event = DummyMultiEvent() o2.startModule(event) event.wait() # value has changed type, b2 and a1 are written - expectedAfterStart.update(value=0, b2=True, a1=2.7) + expectedAfterStart.update(value=0, b2=True, a1=True) + # ramerk: a1=True: this behaviour is a Porgamming error assert updates.pop('o2') == expectedAfterStart assert o2._a1_written == 2.7 assert o2._b2_written is True @@ -218,13 +229,15 @@ def test_ModuleMagic(): # check '$' in unit works properly assert o2.parameters['a1'].datatype.unit == 'mm/s' cfg = Newclass2.configurables - assert set(cfg.keys()) == {'export', 'group', 'description', 'disable_value_range_check', + assert set(cfg.keys()) == { + 'export', 'group', 'description', 'disable_value_range_check', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop', - 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value', - 'a1'} - assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution', + 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2', + 'cmd2', 'value', 'a1'} + assert set(cfg['value'].keys()) == { + 'group', 'export', 'relative_resolution', 'visibility', 'unit', 'default', 'datatype', 'fmtstr', - 'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant', + 'absolute_resolution', 'max', 'min', 'readonly', 'constant', 'description', 'needscfg'} # check on the level of classes diff --git a/test/test_poller.py b/test/test_poller.py index b35495a..ae94e29 100644 --- a/test/test_poller.py +++ b/test/test_poller.py @@ -21,18 +21,20 @@ # ***************************************************************************** """test poller.""" +import sys +import threading import time -from collections import OrderedDict +import logging import pytest -from secop.modules import Drivable -from secop.poller import DYNAMIC, REGULAR, SLOW, Poller +from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll +from secop.lib.multievent import MultiEvent -Status = Drivable.Status class Time: - STARTTIME = 1000 # artificial time zero + STARTTIME = 1000 # artificial time zero + def __init__(self): self.reset() self.finish = float('inf') @@ -61,190 +63,103 @@ class Time: self.seconds += seconds self.busytime += seconds + artime = Time() # artificial test time -@pytest.fixture(autouse=True) -def patch_time(monkeypatch): - monkeypatch.setattr(time, 'time', artime.time) + +class Event(threading.Event): + def wait(self, timeout=None): + artime.sleep(max(0, timeout)) -class Event: - def __init__(self): - self.flag = False +class DispatcherStub: + maxcycles = 10 - def wait(self, timeout): - artime.sleep(max(0,timeout)) - - def set(self): - self.flag = True - - def clear(self): - self.flag = False - - def is_set(self): - return self.flag - - -class Parameter: - def __init__(self, name, readonly, poll, polltype, interval): - self.poll = poll - self.polltype = polltype # used for check only - self.export = name - self.readonly = readonly - self.interval = interval - self.timestamp = 0 - self.handler = None - self.reset() - - def reset(self): - self.cnt = 0 - self.span = 0 - self.maxspan = 0 - - def rfunc(self): - artime.busy(artime.commtime) + def announce_update(self, modulename, pname, pobj): now = artime.time() - self.span = now - self.timestamp - self.maxspan = max(self.maxspan, self.span) - self.timestamp = now - self.cnt += 1 - return True - - def __repr__(self): - return 'Parameter(%s)' % ", ".join("%s=%r" % item for item in self.__dict__.items()) - - -class Module: - properties = {} - pollerClass = Poller - - class io: - name = 'common_io' - - def __init__(self, name, pollinterval=5, fastfactor=0.25, slowfactor=4, busy=False, - counts=(), auto=None): - '''create a dummy module - - nauto, ndynamic, nregular, nslow are the number of parameters of each polltype - ''' - self.pollinterval = pollinterval - self.fast_pollfactor = fastfactor - self.slow_pollfactor = slowfactor - self.parameters = OrderedDict() - self.name = name - self.is_busy = busy - if auto is not None: - self.pvalue = self.addPar('value', True, auto or DYNAMIC, DYNAMIC) - # readonly = False should not matter: - self.pstatus = self.addPar('status', False, auto or DYNAMIC, DYNAMIC) - self.pregular = self.addPar('regular', True, auto or REGULAR, REGULAR) - self.pslow = self.addPar('slow', False, auto or SLOW, SLOW) - self.addPar('notpolled', True, False, 0) - self.counts = 'auto' + if hasattr(pobj, 'stat'): + pobj.stat.append(now) else: - ndynamic, nregular, nslow = counts - for i in range(ndynamic): - self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC) - for i in range(nregular): - self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR) - for i in range(nslow): - self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW) - self.counts = counts + pobj.stat = [now] + self.maxcycles -= 1 + if self.maxcycles <= 0: + self.finish_event.set() + sys.exit() # stop thread - def addPar(self, name, readonly, poll, expected_polltype): - # self.count[polltype] += 1 - expected_interval = self.pollinterval - if expected_polltype == SLOW: - expected_interval *= self.slow_pollfactor - elif expected_polltype == DYNAMIC and self.is_busy: - expected_interval *= self.fast_pollfactor - pobj = Parameter(name, readonly, poll, expected_polltype, expected_interval) - setattr(self, 'read_' + pobj.export, pobj.rfunc) - self.parameters[pobj.export] = pobj - return pobj - def isBusy(self): - return self.is_busy +class ServerStub: + def __init__(self): + self.dispatcher = DispatcherStub() - def pollOneParam(self, pname): - getattr(self, 'read_' + pname)() - def writeInitParams(self): - pass +class Base(Module): + def __init__(self): + srv = ServerStub() + super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv) + self.dispatcher = srv.dispatcher + self.nextPollEvent = Event() - def __repr__(self): - rdict = self.__dict__.copy() - rdict.pop('parameters') - return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name, - self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1), - self.pollinterval, self.is_busy) + def run(self, maxcycles): + self.dispatcher.maxcycles = maxcycles + self.dispatcher.finish_event = threading.Event() + self.startModule(MultiEvent()) + self.dispatcher.finish_event.wait(1) -module_list = [ - [Module('x', 3.0, 0.125, 10, False, auto=True), - Module('y', 3.0, 0.125, 10, False, auto=False)], - [Module('a', 1.0, 0.25, 4, True, (5, 5, 10)), - Module('b', 2.0, 0.25, 4, True, (5, 5, 50))], - [Module('c', 1.0, 0.25, 4, False, (5, 0, 0))], - [Module('d', 1.0, 0.25, 4, True, (0, 9, 0))], - [Module('e', 1.0, 0.25, 4, True, (0, 0, 9))], - [Module('f', 1.0, 0.25, 4, True, (0, 0, 0))], - ] -@pytest.mark.parametrize('modules', module_list) -def test_Poller(modules): - # check for proper timing - for overloaded in False, True: - artime.reset() - count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0} - maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0} - pollTable = dict() - for module in modules: - Poller.add_to_table(pollTable, module) - for pobj in module.parameters.values(): - if pobj.poll: - maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval) - count[pobj.polltype] += 1 - pobj.reset() - assert len(pollTable) == 1 - poller = pollTable[(Poller, 'common_io')] - artime.stop = poller.stop - poller._event = Event() # patch Event.wait +class Mod1(Base, Readable): + param1 = Parameter('', FloatRange()) + param2 = Parameter('', FloatRange()) + param3 = Parameter('', FloatRange()) + param4 = Parameter('', FloatRange()) - assert (sum(count.values()) > 0) == bool(poller) + @ReadHandler(('param1', 'param2', 'param3')) + def read_param(self, name): + artime.sleep(1.0) + return 0 - def started_callback(modules=modules): - for module in modules: - for pobj in module.parameters.values(): - assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once - pobj.reset() # set maxspan and cnt to 0 + @nopoll + def read_param4(self): + return 0 - if overloaded: - # overloaded scenario - artime.commtime = 1.0 - ncycles = 10 - if count[SLOW] > 0: - cycletime = (count[REGULAR] + 1) * count[SLOW] * 2 - else: - cycletime = max(count[REGULAR], count[DYNAMIC]) * 2 - artime.reset(cycletime * ncycles * 1.01) # poller will quit given time - poller.run(started_callback) - total = artime.time() - artime.STARTTIME - for module in modules: - for pobj in module.parameters.values(): - if pobj.poll: - # average_span = total / (pobj.cnt + 1) - assert total / (pobj.cnt + 1) <= max(cycletime, pobj.interval * 1.1) - else: - # normal scenario - artime.commtime = 0.001 - artime.reset(max(maxspan.values()) * 5) # poller will quit given time - poller.run(started_callback) - total = artime.time() - artime.STARTTIME - for module in modules: - for pobj in module.parameters.values(): - if pobj.poll: - assert pobj.cnt > 0 - assert pobj.maxspan <= maxspan[pobj.polltype] * 1.1 - assert (pobj.cnt + 1) * pobj.interval >= total * 0.99 - assert abs(pobj.span - pobj.interval) < 0.01 - pobj.reset() + def read_status(self): + artime.sleep(1.0) + return 0 + + def read_value(self): + artime.sleep(1.0) + return 0 + + +@pytest.mark.parametrize( + 'ncycles, pollinterval, slowinterval, mspan, pspan', + [ # normal case: 5+-1 15+-1 + ( 60, 5, 15, (4, 6), (14, 16)), + # pollinterval faster then reading: mspan max 3 s (polls of value, status and ONE other parameter) + ( 60, 1, 5, (1, 3), (5, 16)), + ]) +def test_poll(ncycles, pollinterval, slowinterval, mspan, pspan, monkeypatch): + monkeypatch.setattr(time, 'time', artime.time) + artime.reset() + m = Mod1() + m.pollinterval = pollinterval + m.slowInterval = slowinterval + m.run(ncycles) + assert not hasattr(m.parameters['param4'], 'stat') + for pname in ['value', 'status']: + pobj = m.parameters[pname] + lowcnt = 0 + for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]): + if t2 - t1 < mspan[0]: + print(t2 - t1) + lowcnt += 1 + assert t2 - t1 <= mspan[1] + assert lowcnt <= 1 + for pname in ['param1', 'param2', 'param3']: + pobj = m.parameters[pname] + lowcnt = 0 + for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]): + if t2 - t1 < pspan[0]: + print(pname, t2 - t1) + lowcnt += 1 + assert t2 - t1 <= pspan[1] + assert lowcnt <= 1 diff --git a/test/test_properties.py b/test/test_properties.py index 8ffa18e..6c1d8e4 100644 --- a/test/test_properties.py +++ b/test/test_properties.py @@ -26,6 +26,7 @@ import pytest from secop.datatypes import FloatRange, IntRange, StringType, ValueType from secop.errors import BadValueError, ConfigError, ProgrammingError from secop.properties import HasProperties, Property +from secop.core import Parameter def Prop(*args, name=None, **kwds): @@ -149,17 +150,25 @@ def test_Property_override(): assert o2.a == 3 with pytest.raises(ProgrammingError) as e: - class cx(c): # pylint: disable=unused-variable + class cx(c): # pylint: disable=unused-variable def a(self): pass assert 'collides with' in str(e.value) with pytest.raises(ProgrammingError) as e: - class cz(c): # pylint: disable=unused-variable + class cy(c): # pylint: disable=unused-variable a = 's' assert 'can not set' in str(e.value) + with pytest.raises(ProgrammingError) as e: + class cz(c): # pylint: disable=unused-variable + a = 's' + + class cp(c): # pylint: disable=unused-variable + # overriding a Property with a Parameter is allowed + a = Parameter('x', IntRange()) + def test_Properties_mro(): class Base(HasProperties):