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:
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)
|
||||
|
||||
Reference in New Issue
Block a user