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
2 changes: 1 addition & 1 deletion tango/pyaml/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.3.3"
__version__ = "0.4.0"

import logging.config
import os
Expand Down
80 changes: 72 additions & 8 deletions tango/pyaml/attribute.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import logging
from typing import Optional, Tuple

Expand Down Expand Up @@ -27,11 +28,16 @@ class ConfigModel(BaseModel):
The unit of the attribute.
range : tuple(min, max), optional
Range of valid values. Use null for -∞ or +∞.
index : int, optional
Zero-based index into a SPECTRUM attribute. When set, the instance
behaves as a read-only scalar view of one vector element; writes are
always rejected and a SPECTRUM data_format is enforced on init.
"""

attribute: str
unit: str = ""
range: Optional[Tuple[Optional[float], Optional[float]]] = None
index: Optional[int] = None


class Attribute(DeviceAccess, InitializableElement):
Expand All @@ -52,7 +58,9 @@ class Attribute(DeviceAccess, InitializableElement):
def __init__(self, cfg: ConfigModel, writable=True):
super().__init__()
self._cfg = cfg
self._writable = writable
self._index = cfg.index
# Indexed access never writes individual array elements.
self._writable = writable and self._index is None
self._attribute_dev: tango.DeviceProxy = None
self._attr_config: tango.AttributeConfig = None
self._attribute_dev_name: str = None
Expand All @@ -72,6 +80,13 @@ def initialize(self):
self._attribute_dev.get_attribute_config(self._attr_name, wait=True)
)

if self._index is not None:
if self._attr_config.data_format != tango.AttrDataFormat.SPECTRUM:
raise pyaml.PyAMLException(
f"Tango attribute '{self._cfg.attribute}' is not a SPECTRUM; "
"indexed access requires a vector attribute."
)

if self._writable:
if self._attr_config.writable not in [
tango.AttrWriteType.READ_WRITE,
Expand All @@ -97,8 +112,13 @@ def set(self, value: float):
Raises
------
pyaml.PyAMLException
If the Tango write fails.
If the Tango write fails or this is an indexed attribute.
"""
if self._index is not None:
raise pyaml.PyAMLException(
f"Indexed attribute '{self._cfg.attribute}[{self._index}]' "
"does not support individual element writes."
)
self._ensure_initialized()
logger.log(
logging.DEBUG, f"Setting asynchronously {self._cfg.attribute} to {value}"
Expand All @@ -120,8 +140,13 @@ def set_and_wait(self, value: float):
Raises
------
pyaml.PyAMLException
If the Tango write fails.
If the Tango write fails or this is an indexed attribute.
"""
if self._index is not None:
raise pyaml.PyAMLException(
f"Indexed attribute '{self._cfg.attribute}[{self._index}]' "
"does not support individual element writes."
)
self._ensure_initialized()
logger.log(logging.DEBUG, f"Setting {self._cfg.attribute} to {value}")
try:
Expand Down Expand Up @@ -150,7 +175,8 @@ def readback(self) -> Value:
quality = Quality[
attr_value.quality.name.rsplit("_", 1)[1]
] # AttrQuality.ATTR_VALID gives Quality.VALID
value = Value(attr_value.value, quality, attr_value.time.todatetime())
raw = attr_value.value[self._index] if self._index is not None else attr_value.value
value = Value(raw, quality, attr_value.time.todatetime())
except tango.DevFailed as df:
raise tango_to_PyAMLException(df)
return value
Expand All @@ -173,25 +199,60 @@ def name(self) -> str:
Returns
-------
str
The attribute path (e.g., 'my/ps/device/current').
The attribute path (e.g., 'my/ps/device/current'), or with index
notation when indexed (e.g., 'my/ps/device/current[2]').
"""
if self._index is not None:
return f"{self._cfg.attribute}[{self._index}]"
return self._cfg.attribute

def get_tango_attribute(self) -> str:
"""
Return the raw Tango attribute path without index decoration.

Returns
-------
str
Tango attribute path stored in the configuration.
"""
return self._cfg.attribute

def clone_with_tango_attribute(self, attribute: str) -> "Attribute":
"""
Return a shallow copy configured with another Tango attribute path.

Parameters
----------
attribute : str
Tango attribute path to store in the cloned instance.
"""
new_obj = copy.copy(self)
new_obj._cfg = copy.copy(self._cfg)
new_obj._cfg.attribute = attribute
return new_obj

def measure_name(self) -> str:
"""
Return the short attribute name (last component).

Returns
-------
str
The attribute name (e.g., 'current').
The attribute name (e.g., 'current'), with index notation when
indexed (e.g., 'current[2]').
"""
return self._cfg.attribute.rsplit("/", 1)[1]
short = self._cfg.attribute.rsplit("/", 1)[1]
if self._index is not None:
return f"{short}[{self._index}]"
return short

def get(self) -> float:
"""
Get the last written value of the attribute.

For indexed attributes, returns the setpoint element at the configured
index (``w_value[index]``).

Returns
-------
float
Expand All @@ -204,7 +265,10 @@ def get(self) -> float:
"""
self._ensure_initialized()
try:
return self._attribute_dev.read_attribute(self._attr_name).w_value
attr_val = self._attribute_dev.read_attribute(self._attr_name)
if self._index is not None:
return attr_val.w_value[self._index]
return attr_val.w_value
except tango.DevFailed as df:
raise tango_to_PyAMLException(df)

Expand Down
11 changes: 11 additions & 0 deletions tango/pyaml/attribute_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,17 @@ def measure_name(self) -> str:
"""
return self._cfg.name

def get_tango_attributes(self) -> list[str]:
"""
Return the raw Tango attribute paths stored in the configuration.

Returns
-------
list[str]
Tango attribute paths in configured order.
"""
return self._cfg.attributes

def set(self, value: float):
"""
Write a value asynchronously to all Tango attributes.
Expand Down
6 changes: 5 additions & 1 deletion tango/pyaml/attribute_list_read_only.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging

import pyaml
from .attribute_list import AttributeList, ConfigModel
from .attribute_list import AttributeList, ConfigModel as AttributeListConfigModel

PYAMLCLASS: str = "AttributeListReadOnly"

logger = logging.getLogger(__name__)


class ConfigModel(AttributeListConfigModel):
"""Configuration model for a read-only Tango attribute list."""


class AttributeListReadOnly(AttributeList):
"""
Handle a list of Tango attributes using Tango Groups.
Expand Down
6 changes: 5 additions & 1 deletion tango/pyaml/attribute_read_only.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import logging

from .attribute import Attribute, ConfigModel
from .attribute import Attribute, ConfigModel as AttributeConfigModel
from .tango_pyaml_utils import *

PYAMLCLASS: str = "AttributeReadOnly"

logger = logging.getLogger(__name__)


class ConfigModel(AttributeConfigModel):
"""Configuration model for a read-only Tango attribute."""


class AttributeReadOnly(Attribute):
"""
Read-only Tango attribute.
Expand Down
24 changes: 24 additions & 0 deletions tango/pyaml/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Configuration helpers for backend-provided catalogs."""

from abc import ABCMeta, abstractmethod

from pydantic import BaseModel


class Catalog(metaclass=ABCMeta):
r"""
Abstract class for backend catalog configuration objects.

Notes
-----
Concrete catalogs live in each control-system package. They may expose
backend-specific resolution APIs, but those APIs are not called by the
PyAML core.
"""

@abstractmethod
def resolve(self, key: str) -> BaseModel:
"""
Return a configuration model for a DeviceAccess
"""
pass
Loading
Loading