Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions tango/pyaml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
"""
Tango backend for pyAML.

This package bridges `Tango Controls <https://www.tango-controls.org/>`_ and
the pyAML abstraction layer. It exposes Tango attributes and attribute groups
as pyAML :class:`~pyaml.control.deviceaccess.DeviceAccess` objects, provides a
:class:`~tango.pyaml.controlsystem.TangoControlSystem` implementation and
catalogs that resolve pyAML device keys into Tango attribute references.

The package registers its configuration schemas with pyAML through the
``pyaml.schemas`` entry point, so the classes below can be used directly in
pyAML YAML configuration files.

Logging is configured at import time from two optional environment variables:

``TANGO_PYAML_LOG_CONFIG``
Path to a :mod:`logging.config` file (default ``tango_pyaml_logging.conf``
in the current directory). Loaded only if the file exists.
``TANGO_PYAML_LOG_LEVEL``
Level name (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``)
applied to the ``tango.pyaml`` logger.
"""

import logging.config
import os

Expand Down
115 changes: 115 additions & 0 deletions tango/pyaml/attribute.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
Scalar Tango attribute access.

This module maps a single Tango attribute (or one element of a SPECTRUM
attribute) onto the pyAML :class:`~pyaml.control.deviceaccess.DeviceAccess`
interface.
"""

import copy
import logging

Expand Down Expand Up @@ -48,6 +56,10 @@ class Attribute(DeviceAccess, InitializableElement, DynamicValidation):
"""
Tango attribute that can be written to.

The Tango device proxy is obtained lazily from
:class:`~tango.pyaml.device_factory.DeviceFactory` on first access, so
building an ``Attribute`` never contacts the control system.

Parameters
----------
attribute : str
Expand All @@ -63,6 +75,56 @@ class Attribute(DeviceAccess, InitializableElement, DynamicValidation):
writable : bool, optional
If the attribute should be writable. Default is True.

Attributes
----------
_attribute : str
Full path of the Tango attribute.
_unit : str
Unit of the attribute.
_range : tuple of (float or None, float or None) or None
Configured range, or ``None`` to query Tango on first use.
_index : int or None
Index into a SPECTRUM attribute, or ``None`` for scalar access.
_writable : bool
``True`` if writes are allowed (always ``False`` when indexed).
_attribute_dev : tango.DeviceProxy or None
Proxy of the device owning the attribute, set on initialization.
_attr_config : tango.AttributeInfoEx or None
Tango attribute configuration, set on initialization.
_attribute_dev_name : str or None
Device part of the attribute path, set on initialization.
_attr_name : str or None
Attribute part of the attribute path, set on initialization.

Methods
-------
initialize()
Connect to the Tango device and check the attribute configuration.
is_writable()
Tell whether the attribute accepts writes.
set(value)
Write a value asynchronously to the Tango attribute.
set_and_wait(value)
Write a value synchronously to the Tango attribute.
get()
Get the last written value of the attribute.
readback()
Return the readback value with metadata.
unit()
Return the unit of the attribute.
name()
Return the full attribute name.
measure_name()
Return the short attribute name (last component).
get_tango_attribute()
Return the raw Tango attribute path without index decoration.
clone_with_tango_attribute(attribute)
Return a shallow copy configured with another Tango attribute path.
get_range()
Return the valid range of the attribute.
check_device_availability()
Check whether the Tango device answers to a ping.

Raises
------
pyaml.PyAMLException
Expand Down Expand Up @@ -92,6 +154,20 @@ def __init__(
self._attr_name: str = None

def initialize(self):
"""
Connect to the Tango device and check the attribute configuration.

Splits the attribute path into device and attribute names, obtains
the device proxy from :class:`~tango.pyaml.device_factory.DeviceFactory`
and reads the attribute configuration.

Raises
------
pyaml.PyAMLException
If the device proxy cannot be created, if an indexed attribute is
not a SPECTRUM, or if a writable attribute is not writable in
Tango.
"""
super().initialize()
try:
self._attribute_dev_name, self._attr_name = self._attribute.rsplit("/", 1)
Expand Down Expand Up @@ -122,6 +198,14 @@ def initialize(self):
)

def is_writable(self):
"""
Tell whether the attribute accepts writes.

Returns
-------
bool
``True`` if :meth:`set` and :meth:`set_and_wait` are allowed.
"""
return self._writable

def set(self, value: float):
Expand Down Expand Up @@ -253,6 +337,11 @@ def clone_with_tango_attribute(self, attribute: str) -> "Attribute":
----------
attribute : str
Tango attribute path to store in the cloned instance.

Returns
-------
Attribute
Copy of this instance pointing to ``attribute``.
"""
new_obj = copy.copy(self)
new_obj._attribute = attribute
Expand Down Expand Up @@ -300,6 +389,23 @@ def get(self) -> float:
raise tango_to_PyAMLException(df)

def get_range(self) -> list[float]:
"""
Return the valid range of the attribute.

The configured ``range`` takes precedence; otherwise the ``min_value``
and ``max_value`` limits of the Tango attribute configuration are
used, which requires initialization.

Returns
-------
list of float or None
``[min, max]`` where an unbounded limit is ``None``.

Raises
------
pyaml.PyAMLException
If no range is configured and initialization fails.
"""
attr_range: list[float] = [None, None]
if self._range is not None:
attr_range[0] = self._range[0] if self._range[0] is not None else None
Expand All @@ -314,6 +420,15 @@ def get_range(self) -> list[float]:
return attr_range

def check_device_availability(self) -> bool:
"""
Check whether the Tango device answers to a ping.

Returns
-------
bool
``True`` if the device is reachable, ``False`` if initialization
or the ping fails.
"""
available = True
try:
self._ensure_initialized()
Expand Down
91 changes: 86 additions & 5 deletions tango/pyaml/attribute_list.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
Grouped Tango attribute access.

This module handles a list of Tango attributes through :class:`tango.Group`
objects, one per distinct attribute name, so that a value can be written to or
read from many devices in a single call.
"""

import logging

from numpy import array
Expand Down Expand Up @@ -42,6 +50,10 @@ class AttributeList(DeviceAccess, InitializableElement, DynamicValidation):
"""
Handle a list of Tango attributes using Tango Groups.

Attributes are grouped by attribute name: one :class:`tango.Group` is
created per distinct attribute name and holds every device exposing it.
Groups are created lazily on first access.

Parameters
----------
attributes : list of str
Expand All @@ -50,6 +62,44 @@ class AttributeList(DeviceAccess, InitializableElement, DynamicValidation):
Group name.
unit : str, optional
Unit of the attributes.

Attributes
----------
_attributes : list of str
Tango attribute paths in configured order.
_name : str
Group name.
_unit : str
Unit of the attributes.
_tango_groups : dict of str to tango.Group
Tango groups indexed by attribute name, created on initialization.
_attr_dev : dict of str to list of str
Device names indexed by attribute name.

Methods
-------
initialize()
Create one Tango group per attribute name.
name()
Return the group name.
measure_name()
Return the group name (alias for measurement name).
get_tango_attributes()
Return the raw Tango attribute paths stored in the configuration.
set(value)
Write a value asynchronously to all Tango attributes.
set_and_wait(value)
Write a value synchronously to all Tango attributes.
get()
Return the last written values of all attributes.
readback()
Return readback values with metadata for all attributes.
unit()
Return the unit for the attribute list.
get_range()
Return the valid ranges of the attributes.
check_device_availability()
Check whether every device of the groups answers to a ping.
"""

def __init__(self, attributes: list[str], name: str = "", unit: str = ""):
Expand All @@ -70,6 +120,12 @@ def __init__(self, attributes: list[str], name: str = "", unit: str = ""):
self._attr_dev[attr_name].append(attribute_dev_name)

def initialize(self):
"""
Create one Tango group per attribute name.

Each group is named after the list and populated with the devices
exposing that attribute.
"""
super().initialize()
for attr_name, dev_list in self._attr_dev.items():
self._tango_groups[attr_name] = tango.Group(self._name)
Expand Down Expand Up @@ -103,7 +159,7 @@ def get_tango_attributes(self) -> list[str]:

Returns
-------
list[str]
list of str
Tango attribute paths in configured order.
"""
return self._attributes
Expand Down Expand Up @@ -148,8 +204,9 @@ def get(self) -> array:

Returns
-------
numpy.array
Array of last written values ordered as in configuration.
numpy.ndarray
Array of last written values ordered as in configuration. Entries
whose read failed are ``None``.
"""
self._ensure_initialized()
result = {}
Expand All @@ -172,8 +229,10 @@ def readback(self) -> array:

Returns
-------
numpy.array
Array of Value objects ordered as in configuration.
numpy.ndarray
Array of :class:`~pyaml.control.readback_value.Value` objects
ordered as in configuration. Entries whose read failed are
``None``.
"""
self._ensure_initialized()
logger.log(logging.DEBUG, f"Reading list {self.name()}")
Expand Down Expand Up @@ -210,6 +269,19 @@ def unit(self) -> str:
return self._unit

def get_range(self) -> list[float]:
"""
Return the valid ranges of the attributes.

If a ``_range`` is configured it is returned as ``[min, max]``.
Otherwise the limits are read from the Tango attribute configuration
of every device and returned flattened as
``[min0, max0, min1, max1, ...]``, in group order.

Returns
-------
list of float or None
Range limits, where an unbounded limit is ``None``.
"""
attr_range: list[float] = [None, None]
if self._range is not None:
attr_range[0] = self._range[0] if self._range[0] is not None else None
Expand All @@ -230,6 +302,15 @@ def get_range(self) -> list[float]:
return attr_range

def check_device_availability(self) -> bool:
"""
Check whether every device of the groups answers to a ping.

Returns
-------
bool
``True`` if all devices are reachable, ``False`` if initialization
or any ping fails.
"""
available = True
try:
self._ensure_initialized()
Expand Down
Loading
Loading