core: Add Acquisition Interface
+ Adds first implementation for the Acquisition interface, split into Controller and Channel Modules + frappy_demo: adds an example simulation + new property AttachedDict for a collection of attached modules + move Attach and AttachDict to a new file frappy/attached.py + interface_classes creation changed. includes now also Acquisition Change-Id: I198a96065a65bb28f73e468ce0465fca2d8734d7
This commit is contained in:
40
cfg/acquisition_cfg.py
Normal file
40
cfg/acquisition_cfg.py
Normal file
@@ -0,0 +1,40 @@
|
||||
Node('measure.frappy.demo',
|
||||
'''Measureable demo''',
|
||||
'tcp://10770',
|
||||
)
|
||||
Mod('control',
|
||||
'frappy_demo.acquisition.Controller',
|
||||
'simple demo controller',
|
||||
channels = {'first': 'chan1', 'second': 'chan2', 'third': 'chan3'},
|
||||
pollinterval = 1,
|
||||
)
|
||||
Mod('chan1',
|
||||
'frappy_demo.acquisition.Channel',
|
||||
'simple channel demo',
|
||||
goal = 50,
|
||||
goal_enable = True,
|
||||
pollinterval = 1,
|
||||
)
|
||||
Mod('chan2',
|
||||
'frappy_demo.acquisition.Channel',
|
||||
'simple channel demo',
|
||||
pollinterval = 1,
|
||||
)
|
||||
Mod('chan3',
|
||||
'frappy_demo.acquisition.Channel',
|
||||
'simple channel demo',
|
||||
pollinterval = 1,
|
||||
)
|
||||
Mod('single',
|
||||
'frappy_demo.acquisition.SimpleAcquisition',
|
||||
'Acquisition demo',
|
||||
pollinterval = 1,
|
||||
goal = 20,
|
||||
goal_enable=True,
|
||||
acquisition_key='single',
|
||||
)
|
||||
Mod('ng',
|
||||
'frappy_demo.acquisition.NoGoalAcquisition',
|
||||
'Acquisition demo',
|
||||
pollinterval = 5,
|
||||
)
|
||||
131
frappy/attached.py
Normal file
131
frappy/attached.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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:
|
||||
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||
#
|
||||
# *****************************************************************************
|
||||
|
||||
from frappy.errors import ConfigError
|
||||
from frappy.modulebase import Module
|
||||
from frappy.datatypes import StringType, ValueType
|
||||
from frappy.properties import Property
|
||||
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached module
|
||||
|
||||
assign a module name to this property in the cfg file,
|
||||
and the server will create an attribute with this module
|
||||
|
||||
When mandatory is set to False, and there is no value or an empty string
|
||||
given in the config file, the value of the attribute will be None.
|
||||
"""
|
||||
def __init__(self, basecls=Module, description='attached module', mandatory=True):
|
||||
self.basecls = basecls
|
||||
super().__init__(description, StringType(), mandatory=mandatory)
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
modobj = obj.attachedModules.get(self.name)
|
||||
if not modobj:
|
||||
modulename = super().__get__(obj, owner)
|
||||
if not modulename:
|
||||
return None # happens when mandatory=False and modulename is not given
|
||||
modobj = obj.secNode.get_module(modulename)
|
||||
if not modobj:
|
||||
raise ConfigError(f'attached module {self.name}={modulename!r} '
|
||||
f'does not exist')
|
||||
if not isinstance(modobj, self.basecls):
|
||||
raise ConfigError(f'attached module {self.name}={modobj.name!r} '
|
||||
f'must inherit from {self.basecls.__qualname__!r}')
|
||||
obj.attachedModules[self.name] = modobj
|
||||
return modobj
|
||||
|
||||
def copy(self):
|
||||
return Attached(self.basecls, self.description, self.mandatory)
|
||||
|
||||
|
||||
class DictWithFlag(dict):
|
||||
flag = False
|
||||
|
||||
|
||||
class AttachDictType(ValueType):
|
||||
"""a custom datatype for a dict <key> of names or modules"""
|
||||
def __init__(self):
|
||||
super().__init__(DictWithFlag)
|
||||
|
||||
def copy(self):
|
||||
return AttachDictType()
|
||||
|
||||
def export_value(self, value):
|
||||
"""export either names or the name attribute
|
||||
|
||||
to treat bare names and modules the same
|
||||
"""
|
||||
return {k: getattr(v, 'name', v) for k, v in value.items()}
|
||||
|
||||
|
||||
class AttachedDict(Property):
|
||||
def __init__(self, description='attached modules', elements=None, optional=None, basecls=None,
|
||||
**kwds):
|
||||
"""a mapping of attached modules
|
||||
|
||||
:param elements: None or a dict <key> of <basecls> for mandatory elements
|
||||
:param optional: None or a dict <key> of <basecls> for optional elements
|
||||
:param basecls: None or a base class for arbitrary keys
|
||||
if not given, only keys given in parameters 'elements' and 'optional' are allowed
|
||||
:param description: the property description
|
||||
|
||||
<key> might also be a number or any other immutable
|
||||
"""
|
||||
self.elements = elements or {}
|
||||
self.basecls = basecls
|
||||
self.baseclasses = {**self.elements, **(optional or {})}
|
||||
super().__init__(description, AttachDictType(), default={}, **kwds)
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
attach_dict = super().__get__(obj, owner) or DictWithFlag({})
|
||||
if attach_dict.flag:
|
||||
return attach_dict
|
||||
|
||||
for key, modulename in attach_dict.items():
|
||||
basecls = self.baseclasses.get(key, self.basecls)
|
||||
if basecls is None:
|
||||
raise ConfigError(f'unknown key {key!r} for attached modules {self.name}')
|
||||
modobj = obj.secNode.get_module(modulename)
|
||||
if modobj is None:
|
||||
raise ConfigError(f'attached modules {self.name}: '
|
||||
f'{key}={modulename!r} does not exist')
|
||||
if not isinstance(modobj, basecls):
|
||||
raise ConfigError(f'attached modules {self.name}: '
|
||||
f'module {key}={modulename!r} must inherit '
|
||||
f'from {basecls.__qualname__!r}')
|
||||
obj.attachedModules[self.name, key] = attach_dict[key] = modobj
|
||||
missing_keys = set(self.elements) - set(attach_dict)
|
||||
if missing_keys:
|
||||
raise ConfigError(f'attached modules {self.name}: '
|
||||
f"missing {', '.join(missing_keys)} ")
|
||||
attach_dict.flag = True
|
||||
return attach_dict
|
||||
|
||||
def copy(self):
|
||||
return AttachedDict(self.elements, self.baseclasses, self.basecls, self.description)
|
||||
@@ -29,8 +29,8 @@ from frappy.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
||||
FloatRange, IntRange, ScaledInteger, StringType, StructOf, TupleOf, StatusType
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.modulebase import Done, Module, Feature
|
||||
from frappy.modules import Attached, Communicator, \
|
||||
Drivable, Readable, Writable
|
||||
from frappy.modules import Communicator, Drivable, Readable, Writable
|
||||
from frappy.attached import Attached, AttachedDict
|
||||
from frappy.params import Command, Parameter, Limit
|
||||
from frappy.properties import Property
|
||||
from frappy.proxy import Proxy, SecNode, proxy_class
|
||||
|
||||
@@ -320,6 +320,9 @@ class Module(HasAccessibles):
|
||||
pollInfo = None
|
||||
triggerPoll = None # trigger event for polls. used on io modules and modules without io
|
||||
__poller = None # the poller thread, if used
|
||||
SECoP_CLASS = None
|
||||
SECoP_BASE_CLASSES = [] # predefined SECoP base classes
|
||||
SECoP_CLASSES = [] # all predefined SECoP interface classes
|
||||
|
||||
def __init__(self, name, logger, cfgdict, srv):
|
||||
# remember the secnode for interacting with other modules and the
|
||||
|
||||
@@ -22,16 +22,19 @@
|
||||
# *****************************************************************************
|
||||
"""Define base classes for real Modules implemented in the server"""
|
||||
|
||||
|
||||
from frappy.datatypes import FloatRange, \
|
||||
StatusType, StringType
|
||||
from frappy.datatypes import BoolType, FloatRange, StatusType, StringType
|
||||
from frappy.errors import ConfigError, ProgrammingError
|
||||
from frappy.lib.enum import Enum
|
||||
from frappy.params import Command, Parameter
|
||||
from frappy.properties import Property
|
||||
from frappy.logging import HasComlog
|
||||
from frappy.params import Command, Parameter
|
||||
|
||||
from .modulebase import Module
|
||||
from .attached import AttachedDict
|
||||
|
||||
# import compatibility:
|
||||
# pylint: disable=unused-import
|
||||
from .properties import Property
|
||||
from .attached import Attached
|
||||
|
||||
|
||||
class Readable(Module):
|
||||
@@ -98,6 +101,77 @@ class Drivable(Writable):
|
||||
"""not implemented - this is a no-op"""
|
||||
|
||||
|
||||
class AcquisitionChannel(Readable):
|
||||
"""A Readable which is part of a data acquisition."""
|
||||
interface_classes = ['AcquisitionChannel', 'Readable']
|
||||
# copy Readable.status and extend it with BUSY
|
||||
status = Parameter(datatype=StatusType(Readable, 'BUSY'))
|
||||
goal = Parameter('stops the data acquisition when it is reached',
|
||||
FloatRange(), default=0, readonly=False, optional=True)
|
||||
goal_enable = Parameter('enable goal', BoolType(), readonly=False,
|
||||
default=False, optional=True)
|
||||
|
||||
# clear is no longer part of the proposed spec, so it does not appear
|
||||
# as optional command here. however, a subclass may still implement it
|
||||
|
||||
|
||||
class AcquisitionController(Module):
|
||||
"""Controls other modules.
|
||||
|
||||
Controls the data acquisition from AcquisitionChannels.
|
||||
"""
|
||||
interface_classes = ['AcquisitionController']
|
||||
# channels might be configured to an arbitrary number of channels with arbitrary roles
|
||||
# - to forbid the use fo arbitrary roles, override base=None
|
||||
# - to restrict roles and base classes override elements={<key>: <basecls>}
|
||||
# and/or optional={<key>: <basecls>}
|
||||
channels = AttachedDict('mapping of role to module name for attached channels',
|
||||
elements=None, optional=None,
|
||||
basecls=AcquisitionChannel,
|
||||
extname='acquisition_channels')
|
||||
status = Drivable.status
|
||||
isBusy = Drivable.isBusy
|
||||
# add pollinterval parameter to enable faster polling of the status
|
||||
pollinterval = Readable.pollinterval
|
||||
|
||||
def doPoll(self):
|
||||
self.read_status()
|
||||
|
||||
@Command()
|
||||
def go(self):
|
||||
"""Start the acquisition. No-op if the controller is already Busy."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@Command(optional=True)
|
||||
def prepare(self):
|
||||
"""Prepare the hardware so 'go' can trigger immediately."""
|
||||
|
||||
@Command(optional=True)
|
||||
def hold(self):
|
||||
"""Pause the operation.
|
||||
|
||||
The next go will continue without clearing any channels or resetting hardware."""
|
||||
|
||||
@Command(optional=True)
|
||||
def stop(self):
|
||||
"""Stop the data acquisition or operation."""
|
||||
|
||||
|
||||
class Acquisition(AcquisitionController, AcquisitionChannel): # pylint: disable=abstract-method
|
||||
"""Combines AcquisitionController and AcquisitionChannel into one Module
|
||||
|
||||
for the special case where there is only one channel.
|
||||
remark: when using multiple inheritance, Acquisition must appear
|
||||
before any base class inheriting from AcquisitionController
|
||||
"""
|
||||
interface_classes = ['Acquisition', 'Readable']
|
||||
channels = None # remove property
|
||||
acquisition_key = Property('acquisition role (equivalent to NICOS preset name)',
|
||||
StringType(), export=True, default='')
|
||||
|
||||
doPoll = Readable.doPoll
|
||||
|
||||
|
||||
class Communicator(HasComlog, Module):
|
||||
"""basic abstract communication module"""
|
||||
interface_classes = ['Communicator']
|
||||
@@ -110,38 +184,3 @@ class Communicator(HasComlog, Module):
|
||||
:return: the reply
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class Attached(Property):
|
||||
"""a special property, defining an attached module
|
||||
|
||||
assign a module name to this property in the cfg file,
|
||||
and the server will create an attribute with this module
|
||||
|
||||
When mandatory is set to False, and there is no value or an empty string
|
||||
given in the config file, the value of the attribute will be None.
|
||||
"""
|
||||
def __init__(self, basecls=Module, description='attached module', mandatory=True):
|
||||
self.basecls = basecls
|
||||
super().__init__(description, StringType(), mandatory=mandatory)
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
modobj = obj.attachedModules.get(self.name)
|
||||
if not modobj:
|
||||
modulename = super().__get__(obj, owner)
|
||||
if not modulename:
|
||||
return None # happens when mandatory=False and modulename is not given
|
||||
modobj = obj.secNode.get_module(modulename)
|
||||
if not modobj:
|
||||
raise ConfigError(f'attached module {self.name}={modulename!r} '
|
||||
f'does not exist')
|
||||
if not isinstance(modobj, self.basecls):
|
||||
raise ConfigError(f'attached module {self.name}={modobj.name!r} '
|
||||
f'must inherit from {self.basecls.__qualname__!r}')
|
||||
obj.attachedModules[self.name] = modobj
|
||||
return modobj
|
||||
|
||||
def copy(self):
|
||||
return Attached(self.basecls, self.description, self.mandatory)
|
||||
|
||||
241
frappy_demo/acquisition.py
Normal file
241
frappy_demo/acquisition.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# 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:
|
||||
# Alexander Zaft <a.zaft@fz-juelich.de>
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
from frappy.lib import clamp, mkthread
|
||||
from frappy.core import IntRange, Parameter, ArrayOf, TupleOf, FloatRange, \
|
||||
IDLE, ERROR, BUSY
|
||||
from frappy.modules import AcquisitionController, AcquisitionChannel, Acquisition
|
||||
from frappy.params import Command
|
||||
|
||||
|
||||
class AcquisitionSimulation:
|
||||
def __init__(self, keys):
|
||||
self.values = {k: 0 for k in keys}
|
||||
self.err = None
|
||||
self._stopflag = threading.Event()
|
||||
self.run_acquisition = threading.Event()
|
||||
self.lock = threading.Lock()
|
||||
self.need_reset = False
|
||||
self._thread = None
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
if self.need_reset:
|
||||
self.reset()
|
||||
if self._thread is None:
|
||||
self._thread = mkthread(self.threadfun)
|
||||
|
||||
def threadfun(self):
|
||||
self.sim_interval = 1
|
||||
self.err = None
|
||||
try:
|
||||
self.__sim()
|
||||
except Exception as e:
|
||||
self.err = str(e)
|
||||
# the thread stops here, but will be restarted with the go command
|
||||
self._thread = None
|
||||
|
||||
def __sim(self):
|
||||
timestamp = time.time()
|
||||
delay = 0
|
||||
while not self._stopflag.wait(delay):
|
||||
self.run_acquisition.wait()
|
||||
t = time.time()
|
||||
diff = t - timestamp
|
||||
if diff < self.sim_interval:
|
||||
delay = clamp(0.1, self.sim_interval, 10)
|
||||
continue
|
||||
delay = 0
|
||||
with self.lock:
|
||||
self.values = {k: v + max(0., random.normalvariate(4., 1.))
|
||||
for k, v in self.values.items()}
|
||||
timestamp = t
|
||||
|
||||
def reset(self):
|
||||
with self.lock:
|
||||
for key in self.values:
|
||||
self.values[key] = 0
|
||||
self.need_reset = False
|
||||
|
||||
def shutdown(self):
|
||||
# unblock thread:
|
||||
self._stopflag.set()
|
||||
self.run_acquisition.set()
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join()
|
||||
|
||||
|
||||
class Controller(AcquisitionController):
|
||||
_status = None # for sticky status values
|
||||
|
||||
def init_ac(self):
|
||||
self.ac = AcquisitionSimulation(m.name for m in self.channels.values())
|
||||
self.ac.reset()
|
||||
for key, channel in self.channels.items():
|
||||
self.log.debug('register %s: %s', key, channel.name)
|
||||
channel.register_acq(self.ac)
|
||||
|
||||
def initModule(self):
|
||||
super().initModule()
|
||||
self.init_ac()
|
||||
|
||||
def read_status(self):
|
||||
with self.ac.lock:
|
||||
if self.ac.err:
|
||||
status = self.Status.ERROR, self.ac.err
|
||||
elif self.ac.run_acquisition.is_set():
|
||||
status = self.Status.BUSY, 'running acquisition'
|
||||
else:
|
||||
status = self._status or (self.Status.IDLE, '')
|
||||
for chan in self.channels.values():
|
||||
chan.read_status()
|
||||
return status
|
||||
|
||||
def go(self):
|
||||
self.ac.start() # restart sim thread if it failed
|
||||
self.ac.run_acquisition.set()
|
||||
self._status = None
|
||||
self.read_status()
|
||||
|
||||
def hold(self):
|
||||
self.ac.run_acquisition.clear()
|
||||
self._status = IDLE, 'paused'
|
||||
self.read_status()
|
||||
|
||||
def stop(self):
|
||||
self.ac.run_acquisition.clear()
|
||||
self.ac.need_reset = True
|
||||
self._status = IDLE, 'stopped'
|
||||
|
||||
@Command()
|
||||
def clear(self):
|
||||
"""clear all channels"""
|
||||
self.ac.reset()
|
||||
|
||||
def shutdownModule(self):
|
||||
self.ac.shutdown()
|
||||
|
||||
|
||||
class Channel(AcquisitionChannel):
|
||||
_status = None # for sticky status values
|
||||
# activate optional parameters:
|
||||
goal = Parameter()
|
||||
goal_enable = Parameter()
|
||||
|
||||
def register_acq(self, ac):
|
||||
self.ac = ac
|
||||
|
||||
def read_value(self):
|
||||
with self.ac.lock:
|
||||
try:
|
||||
ret = self.ac.values[self.name]
|
||||
except KeyError:
|
||||
return -1
|
||||
if self.goal_enable and self.goal < ret:
|
||||
if self.ac.run_acquisition.is_set():
|
||||
self.ac.run_acquisition.clear()
|
||||
self.ac.need_reset = True
|
||||
self._status = IDLE, 'hit goal'
|
||||
else:
|
||||
self._status = None
|
||||
return ret
|
||||
|
||||
def read_status(self):
|
||||
if self.ac.err:
|
||||
return ERROR, self.ac.err
|
||||
if self.ac.run_acquisition.is_set():
|
||||
return BUSY, 'running acquisition'
|
||||
return self._status or (IDLE, '')
|
||||
|
||||
@Command()
|
||||
def clear(self):
|
||||
"""clear this channel"""
|
||||
with self.ac.lock:
|
||||
try:
|
||||
self.ac.values[self.name] = 0.
|
||||
except KeyError:
|
||||
pass
|
||||
self.read_value()
|
||||
|
||||
|
||||
class SimpleAcquisition(Acquisition, Controller, Channel):
|
||||
def init_ac(self):
|
||||
self.channels = {}
|
||||
self.ac = AcquisitionSimulation([self.name])
|
||||
self.ac.reset()
|
||||
|
||||
|
||||
class NoGoalAcquisition(Acquisition):
|
||||
_value = 0
|
||||
_deadline = 0
|
||||
|
||||
def read_value(self):
|
||||
return self._value
|
||||
|
||||
def read_status(self):
|
||||
if self.status[0] == BUSY:
|
||||
overtime = time.time() - self._deadline
|
||||
if overtime < 0:
|
||||
return BUSY, ''
|
||||
self.setFastPoll(False)
|
||||
self._value = overtime
|
||||
self.read_value()
|
||||
return IDLE, ''
|
||||
|
||||
def go(self):
|
||||
self._value = 0
|
||||
self.status = BUSY, 'started'
|
||||
self.setFastPoll(True, 0.1)
|
||||
self._deadline = time.time() + 1
|
||||
self.read_status()
|
||||
|
||||
|
||||
# TODO
|
||||
class MatrixChannel(AcquisitionChannel):
|
||||
roi = Parameter('region of interest',
|
||||
ArrayOf(TupleOf(IntRange(), IntRange()), 0, 1),
|
||||
default=[], readonly=False)
|
||||
|
||||
def initModule(self):
|
||||
self.data = [0.] * 128
|
||||
|
||||
def read_value(self):
|
||||
# mean of data or roi
|
||||
if self.roi:
|
||||
b, e = self.roi[0]
|
||||
else:
|
||||
b, e = 0, len(self.data) - 1
|
||||
return self.data[b:e] / (e - b)
|
||||
|
||||
def write_roi(self, roi):
|
||||
pass
|
||||
|
||||
@Command(result=ArrayOf(FloatRange()))
|
||||
def get_data(self):
|
||||
return self.data
|
||||
|
||||
# axes
|
||||
# binning
|
||||
def clear(self):
|
||||
raise NotImplementedError()
|
||||
@@ -53,6 +53,7 @@ class WithAtt(Readable):
|
||||
def read_value(self):
|
||||
return self.att.read_value()
|
||||
|
||||
|
||||
class LN2(Readable):
|
||||
"""Just a readable.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user