new poll mechanism

- remove secop.poller and basic poller
- regular polls for 'important' parameters done by method doPoll
- all other parameters are polled slower (slowInterval) and
  with lower priority (only one at a time when main poll is due)
- nopoll decorator for read_* to disable poll
- enablePoll attribute (default True) for disabling polling a module
- fast polls may be implemented by means of a statemachine
- configurable slow poll interval
+ allow a Parameter to override a Property (parameter
  Readable.pollinterval overrides Module.pollinterval)

Change-Id: Ib1b3453041a233678b7c4b4add22ac399670e447
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27832
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
This commit is contained in:
zolliker 2022-02-23 16:42:28 +01:00
parent aa82bc580d
commit b423235c5d
23 changed files with 343 additions and 683 deletions

View File

@ -94,6 +94,7 @@ def main(argv=None):
generalConfig.defaults['lazy_number_validation'] = True generalConfig.defaults['lazy_number_validation'] = True
generalConfig.defaults['disable_value_range_check'] = True generalConfig.defaults['disable_value_range_check'] = True
generalConfig.defaults['legacy_hasiodev'] = True generalConfig.defaults['legacy_hasiodev'] = True
generalConfig.defaults['tolerate_poll_property'] = True
generalConfig.init(args.gencfg) generalConfig.init(args.gencfg)
logger.init(loglevel) logger.init(loglevel)

View File

@ -33,8 +33,9 @@ from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, \ from secop.modules import Attached, Communicator, \
Done, Drivable, Module, Readable, Writable Done, Drivable, Module, Readable, Writable
from secop.params import Command, Parameter from secop.params import Command, Parameter
from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
from secop.properties import Property from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class from secop.proxy import Proxy, SecNode, proxy_class
from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff) from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
from secop.persistent import PersistentMixin, PersistentParam from secop.persistent import PersistentMixin, PersistentParam
from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
CommonWriteHandler, nopoll

View File

@ -35,7 +35,6 @@ from secop.errors import CommunicationFailedError, CommunicationSilentError, \
ConfigError, ProgrammingError ConfigError, ProgrammingError
from secop.modules import Attached, Command, \ from secop.modules import Attached, Command, \
Communicator, Done, Module, Parameter, Property Communicator, Done, Module, Parameter, Property
from secop.poller import REGULAR
from secop.lib import generalConfig from secop.lib import generalConfig
@ -109,7 +108,7 @@ class IOBase(Communicator):
uri = Property('hostname:portnumber', datatype=StringType()) uri = Property('hostname:portnumber', datatype=StringType())
timeout = Parameter('timeout', datatype=FloatRange(0), default=2) timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0) 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) pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
_reconnectCallbacks = None _reconnectCallbacks = None
@ -133,6 +132,9 @@ class IOBase(Communicator):
self._conn = None self._conn = None
self.is_connected = False self.is_connected = False
def doPoll(self):
self.read_is_connected()
def read_is_connected(self): def read_is_connected(self):
"""try to reconnect, when not connected """try to reconnect, when not connected

View File

@ -153,7 +153,7 @@ class SequencerMixin:
self._seq_error = str(e) self._seq_error = str(e)
finally: finally:
self._seq_thread = None self._seq_thread = None
self.pollParams(0) self.doPoll()
def _seq_thread_inner(self, seq, store_init): def _seq_thread_inner(self, seq, store_init):
store = Namespace() store = Namespace()

View File

@ -23,8 +23,8 @@
"""Define base classes for real Modules implemented in the server""" """Define base classes for real Modules implemented in the server"""
import sys
import time import time
import threading
from collections import OrderedDict from collections import OrderedDict
from functools import wraps 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 import formatException, mkthread, UniqueObject, generalConfig
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.params import Accessible, Command, Parameter from secop.params import Accessible, Command, Parameter
from secop.poller import BasicPoller, Poller
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
from secop.logging import RemoteLogHandler, HasComlog from secop.logging import RemoteLogHandler, HasComlog
@ -90,14 +89,17 @@ class HasAccessibles(HasProperties):
else: else:
aobj.merge(merged_properties[aname]) aobj.merge(merged_properties[aname])
accessibles[aname] = aobj accessibles[aname] = aobj
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles # rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
# move (2) to the end # 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: if aname in accessibles:
accessibles.move_to_end(aname) accessibles.move_to_end(aname)
# ignore unknown names # ignore unknown names
# move (3) to the end # move (3) to the end
for aname in new_names: for aname in new_names:
if aname not in paramOrder:
accessibles.move_to_end(aname) accessibles.move_to_end(aname)
# note: for python < 3.6 the order of inherited items is not ensured between # note: for python < 3.6 the order of inherited items is not ensured between
# declarations within the same class # declarations within the same class
@ -147,7 +149,7 @@ class HasAccessibles(HasProperties):
setattr(self, pname, value) # important! trigger the setter setattr(self, pname, value) # important! trigger the setter
return value return value
new_rfunc.poll = getattr(rfunc, 'poll', True) and pobj.poll new_rfunc.poll = getattr(rfunc, 'poll', True)
else: else:
def new_rfunc(self, pname=pname): def new_rfunc(self, pname=pname):
@ -268,6 +270,9 @@ class Module(HasAccessibles):
extname='implementation') extname='implementation')
interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()), interface_classes = Property('offical highest interface-class of the module', ArrayOf(StringType()),
extname='interface_classes') 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 # properties, parameters and commands are auto-merged upon subclassing
parameters = {} parameters = {}
@ -275,7 +280,6 @@ class Module(HasAccessibles):
# reference to the dispatcher (used for sending async updates) # reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
pollerClass = Poller #: default poller used
def __init__(self, name, logger, cfgdict, srv): def __init__(self, name, logger, cfgdict, srv):
# remember the dispatcher object (for the async callbacks) # remember the dispatcher object (for the async callbacks)
@ -289,6 +293,7 @@ class Module(HasAccessibles):
self.initModuleDone = False self.initModuleDone = False
self.startModuleDone = False self.startModuleDone = False
self.remoteLogHandler = None self.remoteLogHandler = None
self.nextPollEvent = threading.Event()
errors = [] errors = []
# handle module properties # handle module properties
@ -333,13 +338,6 @@ class Module(HasAccessibles):
for aname, aobj in self.accessibles.items(): for aname, aobj in self.accessibles.items():
# make a copy of the Parameter/Command object # make a copy of the Parameter/Command object
aobj = aobj.copy() 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 if not self.export: # do not export parameters of a module not exported
aobj.export = False aobj.export = False
if aobj.export: if aobj.export:
@ -578,15 +576,24 @@ class Module(HasAccessibles):
registers it in the server for waiting registers it in the server for waiting
<timeout> defaults to 30 seconds <timeout> defaults to 30 seconds
""" """
if self.writeDict: if self.enablePoll or self.writeDict:
mkthread(self.writeInitParams, start_events.get_trigger()) # enablePoll == False: start poll thread for writing values from writeDict only
mkthread(self.__pollThread, start_events.get_trigger())
self.startModuleDone = True self.startModuleDone = True
def pollOneParam(self, pname): def doPoll(self):
"""poll parameter <pname> with proper error handling""" """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: try:
rfunc = getattr(self, 'read_' + pname)
if rfunc.poll: # TODO: handle this in poller
rfunc() rfunc()
except SilentError: except SilentError:
pass pass
@ -595,6 +602,63 @@ class Module(HasAccessibles):
except Exception: except Exception:
self.log.error(formatException()) 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): def writeInitParams(self, started_callback=None):
"""write values for parameters with configured values """write values for parameters with configured values
@ -640,55 +704,20 @@ class Readable(Module):
UNKNOWN=401, UNKNOWN=401,
) #: status codes ) #: 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()), status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()),
default=(Status.IDLE, ''), poll=True) default=(Status.IDLE, ''))
pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120), pollinterval = Parameter('default poll interval', FloatRange(0.1, 120),
default=5, readonly=False) default=5, readonly=False, export=True)
def startModule(self, start_events): def earlyInit(self):
"""start basic polling thread""" super().earlyInit()
if self.pollerClass and issubclass(self.pollerClass, BasicPoller): # in case pollinterval is reduced a lot, we do not want to wait
# use basic poller for legacy code self.valueCallbacks['pollinterval'].append(self.triggerPollEvent)
mkthread(self.__pollThread, start_events.get_trigger(timeout=30))
else:
super().startModule(start_events)
def __pollThread(self, started_callback): def doPoll(self):
while True: self.read_value()
try: self.read_status()
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
class Writable(Readable): class Writable(Readable):
@ -739,24 +768,6 @@ class Drivable(Writable):
""" """
return 300 <= (status or self.status)[0] < 390 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) @Command(None, result=None)
def stop(self): def stop(self):
"""cease driving, go to IDLE state""" """cease driving, go to IDLE state"""

View File

@ -26,10 +26,11 @@
import inspect import inspect
from secop.datatypes import BoolType, CommandType, DataType, \ from secop.datatypes import BoolType, CommandType, DataType, \
DataTypeType, EnumType, IntRange, NoneOr, OrType, \ DataTypeType, EnumType, NoneOr, OrType, \
StringType, StructOf, TextType, TupleOf, ValueType StringType, StructOf, TextType, TupleOf, ValueType
from secop.errors import BadValueError, ProgrammingError from secop.errors import BadValueError, ProgrammingError
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
from secop.lib import generalConfig
class Accessible(HasProperties): class Accessible(HasProperties):
@ -132,24 +133,9 @@ class Parameter(Accessible):
* True: exported, name automatic. * True: exported, name automatic.
* a string: exported with custom name''', OrType(BoolType(), StringType()), * a string: exported with custom name''', OrType(BoolType(), StringType()),
export=False, default=True) 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( needscfg = Property(
'[internal] needs value in config', NoneOr(BoolType()), '[internal] needs value in config', NoneOr(BoolType()),
export=False, default=None) export=False, default=False)
optional = Property( optional = Property(
'[internal] is this parameter optional?', BoolType(), '[internal] is this parameter optional?', BoolType(),
export=False, settable=False, default=False) export=False, settable=False, default=False)
@ -169,6 +155,8 @@ class Parameter(Accessible):
def __init__(self, description=None, datatype=None, inherit=True, **kwds): def __init__(self, description=None, datatype=None, inherit=True, **kwds):
super().__init__() super().__init__()
if 'poll' in kwds and generalConfig.tolerate_poll_property:
kwds.pop('poll')
if datatype is None: if datatype is None:
# collect datatype properties. these are not applied, as we have no datatype # 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} self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}

View File

@ -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 <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""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 (<pollerClass>, <name>) as the key, and the
poller as value.
<name> 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

View File

@ -138,12 +138,13 @@ class HasProperties(HasDescriptors):
# treat overriding properties with bare values # treat overriding properties with bare values
for pn, po in properties.items(): for pn, po in properties.items():
value = getattr(cls, pn, po) 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() po = po.copy()
try: try:
# try to apply bare value to Property
po.value = po.datatype(value) po.value = po.datatype(value)
except BadValueError: except BadValueError:
if pn in properties:
if callable(value): if callable(value):
raise ProgrammingError('method %s.%s collides with property of %s' % raise ProgrammingError('method %s.%s collides with property of %s' %
(cls.__name__, pn, base.__name__)) from None (cls.__name__, pn, base.__name__)) from None

View File

@ -35,9 +35,9 @@ from secop.io import HasIO
class ProxyModule(HasIO, Module): class ProxyModule(HasIO, Module):
module = Property('remote module name', datatype=StringType(), default='') module = Property('remote module name', datatype=StringType(), default='')
pollerClass = None
_consistency_check_done = False _consistency_check_done = False
_secnode = None _secnode = None
enablePoll = False
def ioClass(self, name, logger, opts, srv): def ioClass(self, name, logger, opts, srv):
opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri']) opts['description'] = 'secnode %s on %s' % (opts.get('module', name), opts['uri'])
@ -125,6 +125,7 @@ class ProxyModule(HasIO, Module):
def checkProperties(self): def checkProperties(self):
pass # skip pass # skip
class ProxyReadable(ProxyModule, Readable): class ProxyReadable(ProxyModule, Readable):
pass pass
@ -184,7 +185,7 @@ def proxy_class(remote_class, name=None):
for aname, aobj in rcls.accessibles.items(): for aname, aobj in rcls.accessibles.items():
if isinstance(aobj, Parameter): 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 attrs[aname] = pobj
def rfunc(self, pname=aname): def rfunc(self, pname=aname):

View File

@ -264,7 +264,6 @@ class Server:
failure_traceback = traceback.format_exc() failure_traceback = traceback.format_exc()
errors.append('error creating %s' % modname) errors.append('error creating %s' % modname)
poll_table = dict()
missing_super = set() missing_super = set()
# all objs created, now start them up and interconnect # all objs created, now start them up and interconnect
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
@ -276,12 +275,6 @@ class Server:
missing_super.add('%s was not called, probably missing super call' missing_super.add('%s was not called, probably missing super call'
% modobj.earlyInit.__qualname__) % 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 # call init on each module after registering all
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
try: try:
@ -317,17 +310,13 @@ class Server:
sys.stderr.write(failure_traceback) sys.stderr.write(failure_traceback)
sys.exit(1) sys.exit(1)
for (_, pollname) , poller in poll_table.items(): self.log.info('waiting for modules being started')
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')
start_events.name = None start_events.name = None
if not start_events.wait(): if not start_events.wait():
# some timeout happened # some timeout happened
for name in start_events.waiting_for(): for name in start_events.waiting_for():
self.log.warning('timeout when starting %s' % name) 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') history_path = os.environ.get('FRAPPY_HISTORY')
if history_path: if history_path:
from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel from secop_psi.historywriter import FrappyHistoryWriter # pylint: disable=import-outside-toplevel

View File

@ -27,13 +27,10 @@ from time import sleep
from secop.datatypes import FloatRange from secop.datatypes import FloatRange
from secop.lib import mkthread from secop.lib import mkthread
from secop.modules import BasicPoller, Drivable, \ from secop.modules import Drivable, Module, Parameter, Readable, Writable, Command
Module, Parameter, Readable, Writable, Command
class SimBase: class SimBase:
pollerClass = BasicPoller
def __new__(cls, devname, logger, cfgdict, dispatcher): def __new__(cls, devname, logger, cfgdict, dispatcher):
extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '') extra_params = cfgdict.pop('extra_params', '') or cfgdict.pop('.extra_params', '')
attrs = {} attrs = {}
@ -120,7 +117,7 @@ class SimDrivable(SimReadable, Drivable):
self._value = self.target self._value = self.target
speed *= self.interval speed *= self.interval
try: try:
self.pollParams(0) self.doPoll()
except Exception: except Exception:
pass pass
@ -133,7 +130,7 @@ class SimDrivable(SimReadable, Drivable):
self._value = self.target self._value = self.target
sleep(self.interval) sleep(self.interval)
try: try:
self.pollParams(0) self.doPoll()
except Exception: except Exception:
pass pass
self.status = self.Status.IDLE, '' self.status = self.Status.IDLE, ''

View File

@ -31,7 +31,7 @@ import math
from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf from secop.datatypes import ArrayOf, FloatRange, StringType, StructOf, TupleOf
from secop.errors import ConfigError, DisabledError from secop.errors import ConfigError, DisabledError
from secop.lib.sequence import SequencerMixin, Step from secop.lib.sequence import SequencerMixin, Step
from secop.modules import BasicPoller, Drivable, Parameter from secop.modules import Drivable, Parameter
class GarfieldMagnet(SequencerMixin, Drivable): class GarfieldMagnet(SequencerMixin, Drivable):
@ -47,9 +47,6 @@ class GarfieldMagnet(SequencerMixin, Drivable):
the symmetry setting selects which. the symmetry setting selects which.
""" """
pollerClass = BasicPoller
# parameters # parameters
subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False) 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) 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) subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False)
userlimits = Parameter('User defined limits of device value', userlimits = Parameter('User defined limits of device value',
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), 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', abslimits = Parameter('Absolute limits of device value',
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), 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 ' precision = Parameter('Precision of the device value (allowed deviation '
'of stable values from target)', 'of stable values from target)',
@ -71,7 +68,7 @@ class GarfieldMagnet(SequencerMixin, Drivable):
calibration = Parameter('Coefficients for calibration ' calibration = Parameter('Coefficients for calibration '
'function: [c0, c1, c2, c3, c4] calculates ' 'function: [c0, c1, c2, c3, c4] calculates '
'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)'
' in T', poll=1, ' in T',
datatype=ArrayOf(FloatRange(), 5, 5), datatype=ArrayOf(FloatRange(), 5, 5),
default=(1.0, 0.0, 0.0, 0.0, 0.0)) default=(1.0, 0.0, 0.0, 0.0, 0.0))
calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting', calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting',

View File

@ -39,7 +39,7 @@ from secop.datatypes import ArrayOf, EnumType, FloatRange, \
from secop.errors import CommunicationFailedError, \ from secop.errors import CommunicationFailedError, \
ConfigError, HardwareError, ProgrammingError ConfigError, HardwareError, ProgrammingError
from secop.lib import lazy_property from secop.lib import lazy_property
from secop.modules import BasicPoller, Command, \ from secop.modules import Command, \
Drivable, Module, Parameter, Readable Drivable, Module, Parameter, Readable
##### #####
@ -157,8 +157,6 @@ class PyTangoDevice(Module):
execution and attribute operations with logging and exception mapping. execution and attribute operations with logging and exception mapping.
""" """
pollerClass = BasicPoller
# parameters # parameters
comtries = Parameter('Maximum retries for communication', comtries = Parameter('Maximum retries for communication',
datatype=IntRange(1, 100), default=3, readonly=False, datatype=IntRange(1, 100), default=3, readonly=False,
@ -425,7 +423,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
userlimits = Parameter('User defined limits of device value', userlimits = Parameter('User defined limits of device value',
datatype=LimitsType(FloatRange(unit='$')), datatype=LimitsType(FloatRange(unit='$')),
default=(float('-Inf'), float('+Inf')), default=(float('-Inf'), float('+Inf')),
readonly=False, poll=10, readonly=False,
) )
abslimits = Parameter('Absolute limits of device value', abslimits = Parameter('Absolute limits of device value',
datatype=LimitsType(FloatRange(unit='$')), datatype=LimitsType(FloatRange(unit='$')),
@ -463,8 +461,8 @@ class AnalogOutput(PyTangoDevice, Drivable):
if attrInfo.unit != 'No unit': if attrInfo.unit != 'No unit':
self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit) self.accessibles['value'].datatype.setProperty('unit', attrInfo.unit)
def pollParams(self, nr=0): def doPoll(self):
super().pollParams(nr) super().doPoll()
while len(self._history) > 2: while len(self._history) > 2:
# if history would be too short, break # if history would be too short, break
if self._history[-1][0] - self._history[1][0] <= self.window: 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', ramp = Parameter('The speed of changing the value',
readonly=False, datatype=FloatRange(0, unit='$/min'), readonly=False, datatype=FloatRange(0, unit='$/min'),
poll=30,
) )
def read_speed(self): def read_speed(self):
@ -686,17 +683,22 @@ class TemperatureController(Actuator):
) )
pid = Parameter('pid control Parameters', pid = Parameter('pid control Parameters',
datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()), 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 # overrides
precision = Parameter(default=0.1) precision = Parameter(default=0.1)
ramp = Parameter(description='Temperature ramp') ramp = Parameter(description='Temperature ramp')
def doPoll(self):
super().doPoll()
self.read_setpoint()
self.read_heateroutput()
def read_ramp(self): def read_ramp(self):
return self._dev.ramp return self._dev.ramp
@ -750,13 +752,19 @@ class PowerSupply(Actuator):
# parameters # parameters
voltage = Parameter('Actual voltage', voltage = Parameter('Actual voltage',
datatype=FloatRange(unit='V'), poll=-5) datatype=FloatRange(unit='V'))
current = Parameter('Actual current', current = Parameter('Actual current',
datatype=FloatRange(unit='A'), poll=-5) datatype=FloatRange(unit='A'))
# overrides # overrides
ramp = Parameter(description='Current/voltage ramp') 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): def read_ramp(self):
return self._dev.ramp return self._dev.ramp

View File

@ -20,7 +20,7 @@
# ***************************************************************************** # *****************************************************************************
"""Andeen Hagerling capacitance bridge""" """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): class Ah2700IO(StringIO):
@ -30,7 +30,7 @@ class Ah2700IO(StringIO):
class Capacitance(HasIO, Readable): 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) freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0)
voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0) voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0)
loss = Parameter('loss', FloatRange(unit='deg'), 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 self.parse_reply(self.communicate('SI')) # SI = single trigger
return Done return Done
@nopoll
def read_freq(self): def read_freq(self):
self.read_value() self.read_value()
return Done return Done
@nopoll
def read_loss(self): def read_loss(self):
self.read_value() self.read_value()
return Done return Done
@nopoll
def read_voltage(self): def read_voltage(self):
self.read_value() self.read_value()
return Done return Done

View File

@ -43,11 +43,11 @@ SOURCECMDS = {
class SourceMeter(HasIO, Module): class SourceMeter(HasIO, Module):
resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True) resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'))
power = Parameter('readback power', FloatRange(unit='W'), poll=True) power = Parameter('readback power', FloatRange(unit='W'))
mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2),
readonly=False, default=0) readonly=False, default=0)
active = Parameter('output enable', BoolType(), readonly=False, poll=True) active = Parameter('output enable', BoolType(), readonly=False)
ioClass = K2601bIO ioClass = K2601bIO
@ -76,10 +76,10 @@ class SourceMeter(HasIO, Module):
class Current(HasIO, Writable): class Current(HasIO, Writable):
sourcemeter = Attached() sourcemeter = Attached()
value = Parameter('measured current', FloatRange(unit='A'), poll=True) value = Parameter('measured current', FloatRange(unit='A'))
target = Parameter('set current', FloatRange(unit='A'), poll=True) target = Parameter('set current', FloatRange(unit='A'))
active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage 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): def read_value(self):
return self.communicate('print(smua.measure.i())') return self.communicate('print(smua.measure.i())')
@ -119,10 +119,10 @@ class Current(HasIO, Writable):
class Voltage(HasIO, Writable): class Voltage(HasIO, Writable):
sourcemeter = Attached() sourcemeter = Attached()
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True) value = Parameter('measured voltage', FloatRange(unit='V'))
target = Parameter('set voltage', FloatRange(unit='V'), poll=True) target = Parameter('set voltage', FloatRange(unit='V'))
active = Parameter('voltage is controlled', BoolType(), poll=True) active = Parameter('voltage is controlled', BoolType())
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True) limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2)
def read_value(self): def read_value(self):
return self.communicate('print(smua.measure.v())') return self.communicate('print(smua.measure.v())')

View File

@ -27,7 +27,6 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
from secop.lib import formatStatusBits from secop.lib import formatStatusBits
from secop.modules import Attached, Done, \ from secop.modules import Attached, Done, \
Drivable, Parameter, Property, Readable Drivable, Parameter, Property, Readable
from secop.poller import REGULAR, Poller
from secop.io import HasIO from secop.io import HasIO
Status = Drivable.Status Status = Drivable.Status
@ -60,12 +59,11 @@ class StringIO(secop.io.StringIO):
class Main(HasIO, Drivable): 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)) target = Parameter('channel to select', datatype=IntRange(0, 17))
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False) autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
pollinterval = Parameter(default=1, export=False) pollinterval = Parameter(default=1, export=False)
pollerClass = Poller
ioClass = StringIO ioClass = StringIO
_channel_changed = 0 # time of last channel change _channel_changed = 0 # time of last channel change
_channels = None # dict <channel no> of <module object> _channels = None # dict <channel no> of <module object>
@ -141,7 +139,6 @@ class ResChannel(HasIO, Readable):
enumerate(mag % val for mag in ['%guV', '%gmV'] enumerate(mag % val for mag in ['%guV', '%gmV']
for val in [2, 6.32, 20, 63.2, 200, 632]))} for val in [2, 6.32, 20, 63.2, 200, 632]))}
pollerClass = Poller
ioClass = StringIO ioClass = StringIO
_main = None # main module _main = None # main module
_last_range_change = 0 # time of last range change _last_range_change = 0 # time of last range change

View File

@ -42,7 +42,6 @@ from secop.lib import clamp
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Communicator, Done, \ from secop.modules import Communicator, Done, \
Drivable, Parameter, Property, Readable Drivable, Parameter, Property, Readable
from secop.poller import Poller
from secop.io import HasIO from secop.io import HasIO
from secop.rwhandler import CommonReadHandler, CommonWriteHandler from secop.rwhandler import CommonReadHandler, CommonWriteHandler
@ -57,7 +56,7 @@ class Main(Communicator):
"""ppms communicator module""" """ppms communicator module"""
pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2) 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) default="", readonly=True)
class_id = Property('Quantum Design class id', StringType(), export=False) 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))) _channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names)))
_status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12} _status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12}
pollerClass = Poller
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
self.modules = {} self.modules = {}
@ -89,6 +86,9 @@ class Main(Communicator):
self.comLog("< %s", reply) self.comLog("< %s", reply)
return reply return reply
def doPoll(self):
self.read_data()
def read_data(self): def read_data(self):
mask = 1 # always get packed_status mask = 1 # always get packed_status
for channelname, channel in self.modules.items(): for channelname, channel in self.modules.items():
@ -116,12 +116,9 @@ class Main(Communicator):
class PpmsBase(HasIO, Readable): class PpmsBase(HasIO, Readable):
"""common base for all ppms modules""" """common base for all ppms modules"""
# polling is done by the main module value = Parameter(needscfg=False)
# and PPMS does not deliver really more fresh values when polled more often status = Parameter(needscfg=False)
value = Parameter(poll=False, needscfg=False)
status = Parameter(poll=False, needscfg=False)
pollerClass = Poller
enabled = True # default, if no parameter enable is defined enabled = True # default, if no parameter enable is defined
_last_settings = None # used by several modules _last_settings = None # used by several modules
slow_pollfactor = 1 slow_pollfactor = 1
@ -134,6 +131,11 @@ class PpmsBase(HasIO, Readable):
super().initModule() super().initModule()
self.io.register(self) 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): def update_value_status(self, value, packed_status):
# update value and status # update value and status
# to be reimplemented for modules looking at packed_status # to be reimplemented for modules looking at packed_status
@ -157,7 +159,7 @@ class Channel(PpmsBase):
"""channel base class""" """channel base class"""
value = Parameter('main value of channels') 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) datatype=BoolType(), default=False)
channel = Property('channel name', channel = Property('channel name',
@ -189,7 +191,7 @@ class UserChannel(Channel):
class DriverChannel(Channel): class DriverChannel(Channel):
"""driver 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')) datatype=FloatRange(0., 5000., unit='uA'))
powerlimit = Parameter('power limit', readonly=False, powerlimit = Parameter('power limit', readonly=False,
datatype=FloatRange(0., 1000., unit='uW')) datatype=FloatRange(0., 1000., unit='uW'))
@ -217,7 +219,7 @@ class DriverChannel(Channel):
class BridgeChannel(Channel): class BridgeChannel(Channel):
"""bridge 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')) datatype=FloatRange(0.01, 5000., unit='uA'))
powerlimit = Parameter('power limit', readonly=False, powerlimit = Parameter('power limit', readonly=False,
datatype=FloatRange(0.001, 1000., unit='uW')) datatype=FloatRange(0.001, 1000., unit='uW'))
@ -263,10 +265,13 @@ class BridgeChannel(Channel):
class Level(PpmsBase): class Level(PpmsBase):
"""helium level""" """helium level"""
value = Parameter(datatype=FloatRange(unit='%'), poll=True) value = Parameter(datatype=FloatRange(unit='%'))
channel = 'level' channel = 'level'
def doPoll(self):
self.read_value()
def update_value_status(self, value, packed_status): def update_value_status(self, value, packed_status):
pass pass
# must be a no-op # must be a no-op
@ -303,8 +308,8 @@ class Chamber(PpmsBase, Drivable):
name2opcode = {k: v for _, _, _, v, k in code_table if k} name2opcode = {k: v for _, _, _, v, k in code_table if k}
opcode2name = {v: k 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} 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) value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0)
target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop', poll=True) target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop')
channel = 'chamber' channel = 'chamber'
@ -339,9 +344,9 @@ class Temp(PpmsBase, Drivable):
) )
value = Parameter(datatype=FloatRange(unit='K')) value = Parameter(datatype=FloatRange(unit='K'))
status = Parameter(datatype=StatusType(Status)) 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', 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, ramp = Parameter('ramping speed', readonly=False, default=0,
datatype=FloatRange(0, 20, unit='K/min')) datatype=FloatRange(0, 20, unit='K/min'))
workingramp = Parameter('intermediate ramp value', workingramp = Parameter('intermediate ramp value',
@ -509,7 +514,7 @@ class Field(PpmsBase, Drivable):
) )
value = Parameter(datatype=FloatRange(unit='T')) value = Parameter(datatype=FloatRange(unit='T'))
status = Parameter(datatype=StatusType(Status)) 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, ramp = Parameter('ramping speed', readonly=False,
datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19) datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19)
approachmode = Parameter('how to approach target', readonly=False, approachmode = Parameter('how to approach target', readonly=False,
@ -640,8 +645,8 @@ class Position(PpmsBase, Drivable):
Status = Drivable.Status Status = Drivable.Status
value = Parameter(datatype=FloatRange(unit='deg')) value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), poll=True) # poll only one parameter target = Parameter(datatype=FloatRange(-720., 720., unit='deg'))
enabled = Parameter('is this channel used?', readonly=False, poll=False, enabled = Parameter('is this channel used?', readonly=False,
datatype=BoolType(), default=True) datatype=BoolType(), default=True)
speed = Parameter('motor speed', readonly=False, default=12, speed = Parameter('motor speed', readonly=False, default=12,
datatype=FloatRange(0.8, 12, unit='deg/sec')) datatype=FloatRange(0.8, 12, unit='deg/sec'))

View File

@ -160,9 +160,9 @@ class Sensor(Readable):
pollinterval = Parameter(export=False) pollinterval = Parameter(export=False)
status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
pollerClass = None
description = 'a calibrated sensor value' description = 'a calibrated sensor value'
_value_error = None _value_error = None
enablePoll = False
def initModule(self): def initModule(self):
self._rawsensor.registerCallbacks(self, ['status']) # auto update status self._rawsensor.registerCallbacks(self, ['status']) # auto update status

View File

@ -74,7 +74,7 @@ STEPPOS_ADR = 1
def writable(*args, **kwds): def writable(*args, **kwds):
"""convenience function to create writable hardware parameters""" """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): class Motor(PersistentMixin, HasIO, Drivable):
@ -83,9 +83,9 @@ class Motor(PersistentMixin, HasIO, Drivable):
value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f')) value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), 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'), 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) target = Parameter('', FloatRange(unit='$'), default=0)
move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'), 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'), minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
default=SPEED_SCALE, group='motorparam') default=SPEED_SCALE, group='motorparam')
currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), 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'), maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
default=1.4, group='motorparam') default=1.4, group='motorparam')
standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'), standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
default=0.1, group='motorparam') default=0.1, group='motorparam')
acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'), acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'),
default=150., group='motorparam') default=150., group='motorparam')
target_reached = Parameter('', BoolType(), poll=True, group='hwstatus') target_reached = Parameter('', BoolType(), group='hwstatus')
move_status = Parameter('', IntRange(0, 3), poll=True, group='hwstatus') move_status = Parameter('', IntRange(0, 3), group='hwstatus')
error_bits = Parameter('', IntRange(0, 255), poll=True, group='hwstatus') error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'), free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
default=0.1, group='motorparam') default=0.1, group='motorparam')
power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'), power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
default=0.1, group='motorparam') default=0.1, group='motorparam')
baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}), 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') pollinterval = Parameter(group='more')
ioClass = BytesIO 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 _started = 0
_calcTimeout = True _calcTimeout = True
_need_reset = None _need_reset = None

View File

@ -69,8 +69,8 @@ def test_handler():
data = [] data = []
class Mod(ModuleTest): class Mod(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True) a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False, poll=True) b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b']) @ReadHandler(['a', 'b'])
def read_hdl(self, pname): def read_hdl(self, pname):
@ -115,8 +115,8 @@ def test_common_handler():
data = [] data = []
class Mod(ModuleTest): class Mod(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True) a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False, poll=True) b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b']) @CommonReadHandler(['a', 'b'])
def read_hdl(self): def read_hdl(self):
@ -164,8 +164,8 @@ def test_common_handler():
def test_nopoll(): def test_nopoll():
class Mod1(ModuleTest): class Mod1(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True) a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False, poll=True) b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b']) @ReadHandler(['a', 'b'])
def read_hdl(self): def read_hdl(self):
@ -175,8 +175,8 @@ def test_nopoll():
assert Mod1.read_b.poll is True assert Mod1.read_b.poll is True
class Mod2(ModuleTest): class Mod2(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True) a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False, poll=True) b = Parameter('', FloatRange(), readonly=False)
@CommonReadHandler(['a', 'b']) @CommonReadHandler(['a', 'b'])
def read_hdl(self): def read_hdl(self):
@ -186,8 +186,8 @@ def test_nopoll():
assert Mod2.read_b.poll is False assert Mod2.read_b.poll is False
class Mod3(ModuleTest): class Mod3(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True) a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False, poll=True) b = Parameter('', FloatRange(), readonly=False)
@ReadHandler(['a', 'b']) @ReadHandler(['a', 'b'])
@nopoll @nopoll
@ -198,8 +198,8 @@ def test_nopoll():
assert Mod3.read_b.poll is False assert Mod3.read_b.poll is False
class Mod4(ModuleTest): class Mod4(ModuleTest):
a = Parameter('', FloatRange(), readonly=False, poll=True) a = Parameter('', FloatRange(), readonly=False)
b = Parameter('', FloatRange(), readonly=False, poll=True) b = Parameter('', FloatRange(), readonly=False)
@nopoll @nopoll
@ReadHandler(['a', 'b']) @ReadHandler(['a', 'b'])

View File

@ -22,15 +22,15 @@
# ***************************************************************************** # *****************************************************************************
"""test data types.""" """test data types."""
import sys
import threading
import pytest import pytest
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger from secop.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
from secop.errors import ProgrammingError, ConfigError from secop.errors import ProgrammingError, ConfigError
from secop.modules import Communicator, Drivable, Readable, Module from secop.modules import Communicator, Drivable, Readable, Module
from secop.params import Command, Parameter from secop.params import Command, Parameter
from secop.poller import BasicPoller from secop.rwhandler import ReadHandler, WriteHandler, nopoll
from secop.lib.multievent import MultiEvent
from secop.rwhandler import ReadHandler, WriteHandler
from secop.lib import generalConfig from secop.lib import generalConfig
@ -67,11 +67,19 @@ class ServerStub:
self.dispatcher = DispatcherStub(updates) 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(): def test_Communicator():
o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({})) o = Communicator('communicator', LoggerStub(), {'.description': ''}, ServerStub({}))
o.earlyInit() o.earlyInit()
o.initModule() o.initModule()
event = MultiEvent() event = DummyMultiEvent()
o.startModule(event) o.startModule(event)
assert event.is_set() # event should be set immediately assert event.is_set() # event should be set immediately
@ -96,8 +104,6 @@ def test_ModuleMagic():
"""another stuff""" """another stuff"""
return not arg return not arg
pollerClass = BasicPoller
def read_param1(self): def read_param1(self):
return True return True
@ -107,6 +113,7 @@ def test_ModuleMagic():
def read_a1(self): def read_a1(self):
return True return True
@nopoll
def read_a2(self): def read_a2(self):
return True return True
@ -140,8 +147,10 @@ def test_ModuleMagic():
value = Parameter(datatype=FloatRange(unit='deg')) value = Parameter(datatype=FloatRange(unit='deg'))
target = Parameter(datatype=FloatRange(), default=0) target = Parameter(datatype=FloatRange(), default=0)
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False) 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('<b2>', datatype=BoolType(), default=True, b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True) readonly=False, initwrite=True)
def write_a1(self, value): def write_a1(self, value):
self._a1_written = value self._a1_written = value
@ -182,12 +191,13 @@ def test_ModuleMagic():
'value': 'first'} 'value': 'first'}
assert updates.pop('o1') == expectedBeforeStart assert updates.pop('o1') == expectedBeforeStart
o1.earlyInit() o1.earlyInit()
event = MultiEvent() event = DummyMultiEvent()
o1.startModule(event) o1.startModule(event)
event.wait() event.wait()
# should contain polled values # should contain polled values
expectedAfterStart = {'status': (Drivable.Status.IDLE, 'ok'), expectedAfterStart = {
'value': 'second'} 'status': (Drivable.Status.IDLE, 'ok'), 'value': 'second',
'param1': True, 'param2': 0.0, 'a1': True}
assert updates.pop('o1') == expectedAfterStart assert updates.pop('o1') == expectedAfterStart
# check in addition if parameters are written # check in addition if parameters are written
@ -197,11 +207,12 @@ def test_ModuleMagic():
expectedBeforeStart['target'] = 0.0 expectedBeforeStart['target'] = 0.0
assert updates.pop('o2') == expectedBeforeStart assert updates.pop('o2') == expectedBeforeStart
o2.earlyInit() o2.earlyInit()
event = MultiEvent() event = DummyMultiEvent()
o2.startModule(event) o2.startModule(event)
event.wait() event.wait()
# value has changed type, b2 and a1 are written # 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 updates.pop('o2') == expectedAfterStart
assert o2._a1_written == 2.7 assert o2._a1_written == 2.7
assert o2._b2_written is True assert o2._b2_written is True
@ -218,13 +229,15 @@ def test_ModuleMagic():
# check '$' in unit works properly # check '$' in unit works properly
assert o2.parameters['a1'].datatype.unit == 'mm/s' assert o2.parameters['a1'].datatype.unit == 'mm/s'
cfg = Newclass2.configurables 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', 'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value', 'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
'a1'} 'cmd2', 'value', 'a1'}
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution', assert set(cfg['value'].keys()) == {
'group', 'export', 'relative_resolution',
'visibility', 'unit', 'default', 'datatype', 'fmtstr', 'visibility', 'unit', 'default', 'datatype', 'fmtstr',
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant', 'absolute_resolution', 'max', 'min', 'readonly', 'constant',
'description', 'needscfg'} 'description', 'needscfg'}
# check on the level of classes # check on the level of classes

View File

@ -21,18 +21,20 @@
# ***************************************************************************** # *****************************************************************************
"""test poller.""" """test poller."""
import sys
import threading
import time import time
from collections import OrderedDict import logging
import pytest import pytest
from secop.modules import Drivable from secop.core import Module, Parameter, FloatRange, Readable, ReadHandler, nopoll
from secop.poller import DYNAMIC, REGULAR, SLOW, Poller from secop.lib.multievent import MultiEvent
Status = Drivable.Status
class Time: class Time:
STARTTIME = 1000 # artificial time zero STARTTIME = 1000 # artificial time zero
def __init__(self): def __init__(self):
self.reset() self.reset()
self.finish = float('inf') self.finish = float('inf')
@ -61,190 +63,103 @@ class Time:
self.seconds += seconds self.seconds += seconds
self.busytime += seconds self.busytime += seconds
artime = Time() # artificial test time artime = Time() # artificial test time
@pytest.fixture(autouse=True)
def patch_time(monkeypatch):
monkeypatch.setattr(time, 'time', artime.time)
class Event(threading.Event):
class Event: def wait(self, timeout=None):
def __init__(self):
self.flag = False
def wait(self, timeout):
artime.sleep(max(0, timeout)) artime.sleep(max(0, timeout))
def set(self):
self.flag = True
def clear(self): class DispatcherStub:
self.flag = False maxcycles = 10
def is_set(self): def announce_update(self, modulename, pname, pobj):
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)
now = artime.time() now = artime.time()
self.span = now - self.timestamp if hasattr(pobj, 'stat'):
self.maxspan = max(self.maxspan, self.span) pobj.stat.append(now)
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'
else: else:
ndynamic, nregular, nslow = counts pobj.stat = [now]
for i in range(ndynamic): self.maxcycles -= 1
self.addPar('%s:d%d' % (name, i), True, DYNAMIC, DYNAMIC) if self.maxcycles <= 0:
for i in range(nregular): self.finish_event.set()
self.addPar('%s:r%d' % (name, i), True, REGULAR, REGULAR) sys.exit() # stop thread
for i in range(nslow):
self.addPar('%s:s%d' % (name, i), False, SLOW, SLOW)
self.counts = counts
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): class ServerStub:
return self.is_busy def __init__(self):
self.dispatcher = DispatcherStub()
def pollOneParam(self, pname):
getattr(self, 'read_' + pname)()
def writeInitParams(self): class Base(Module):
pass def __init__(self):
srv = ServerStub()
super().__init__('mod', logging.getLogger('dummy'), dict(description=''), srv)
self.dispatcher = srv.dispatcher
self.nextPollEvent = Event()
def __repr__(self): def run(self, maxcycles):
rdict = self.__dict__.copy() self.dispatcher.maxcycles = maxcycles
rdict.pop('parameters') self.dispatcher.finish_event = threading.Event()
return 'Module(%r, counts=%r, f=%r, pollinterval=%g, is_busy=%r)' % (self.name, self.startModule(MultiEvent())
self.counts, (self.fast_pollfactor, self.slow_pollfactor, 1), self.dispatcher.finish_event.wait(1)
self.pollinterval, self.is_busy)
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: class Mod1(Base, Readable):
param1 = Parameter('', FloatRange())
param2 = Parameter('', FloatRange())
param3 = Parameter('', FloatRange())
param4 = Parameter('', FloatRange())
@ReadHandler(('param1', 'param2', 'param3'))
def read_param(self, name):
artime.sleep(1.0)
return 0
@nopoll
def read_param4(self):
return 0
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() artime.reset()
count = {DYNAMIC: 0, REGULAR: 0, SLOW: 0} m = Mod1()
maxspan = {DYNAMIC: 0, REGULAR: 0, SLOW: 0} m.pollinterval = pollinterval
pollTable = dict() m.slowInterval = slowinterval
for module in modules: m.run(ncycles)
Poller.add_to_table(pollTable, module) assert not hasattr(m.parameters['param4'], 'stat')
for pobj in module.parameters.values(): for pname in ['value', 'status']:
if pobj.poll: pobj = m.parameters[pname]
maxspan[pobj.polltype] = max(maxspan[pobj.polltype], pobj.interval) lowcnt = 0
count[pobj.polltype] += 1 for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
pobj.reset() if t2 - t1 < mspan[0]:
assert len(pollTable) == 1 print(t2 - t1)
poller = pollTable[(Poller, 'common_io')] lowcnt += 1
artime.stop = poller.stop assert t2 - t1 <= mspan[1]
poller._event = Event() # patch Event.wait assert lowcnt <= 1
for pname in ['param1', 'param2', 'param3']:
assert (sum(count.values()) > 0) == bool(poller) pobj = m.parameters[pname]
lowcnt = 0
def started_callback(modules=modules): for t1, t2 in zip(pobj.stat[1:], pobj.stat[2:-1]):
for module in modules: if t2 - t1 < pspan[0]:
for pobj in module.parameters.values(): print(pname, t2 - t1)
assert pobj.cnt == bool(pobj.poll) # all parameters have to be polled once lowcnt += 1
pobj.reset() # set maxspan and cnt to 0 assert t2 - t1 <= pspan[1]
assert lowcnt <= 1
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()

View File

@ -26,6 +26,7 @@ import pytest
from secop.datatypes import FloatRange, IntRange, StringType, ValueType from secop.datatypes import FloatRange, IntRange, StringType, ValueType
from secop.errors import BadValueError, ConfigError, ProgrammingError from secop.errors import BadValueError, ConfigError, ProgrammingError
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
from secop.core import Parameter
def Prop(*args, name=None, **kwds): def Prop(*args, name=None, **kwds):
@ -155,11 +156,19 @@ def test_Property_override():
assert 'collides with' in str(e.value) assert 'collides with' in str(e.value)
with pytest.raises(ProgrammingError) as e: with pytest.raises(ProgrammingError) as e:
class cz(c): # pylint: disable=unused-variable class cy(c): # pylint: disable=unused-variable
a = 's' a = 's'
assert 'can not set' in str(e.value) 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(): def test_Properties_mro():
class Base(HasProperties): class Base(HasProperties):