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:
parent
aa82bc580d
commit
b423235c5d
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
179
secop/modules.py
179
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
|
||||
<timeout> 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 <pname> 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"""
|
||||
|
@ -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}
|
||||
|
278
secop/poller.py
278
secop/poller.py
@ -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
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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, ''
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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())')
|
||||
|
@ -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 <channel no> of <module object>
|
||||
@ -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
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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('<b2>', 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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
x
Reference in New Issue
Block a user