Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

282-standardise-handling-of-read-config-and-hinted-signals-for-standarddetector #468

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
6 changes: 4 additions & 2 deletions src/ophyd_async/epics/adcore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
FileWriteMode,
ImageMode,
NDAttributeDataType,
NDAttributesXML,
NDAttributeParam,
NDAttributePv,
stop_busy_record,
)

Expand All @@ -30,7 +31,8 @@
"ADBaseDataType",
"FileWriteMode",
"ImageMode",
"NDAttributePv",
"NDAttributeParam",
"NDAttributeDataType",
"NDAttributesXML",
"stop_busy_record",
]
44 changes: 28 additions & 16 deletions src/ophyd_async/epics/adcore/_hdf_writer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import asyncio
from collections.abc import Sequence
from pathlib import Path
from typing import AsyncGenerator, AsyncIterator, Dict, List, Optional
from xml.etree import ElementTree as ET

from bluesky.protocols import DataKey, Hints, StreamAsset

Expand All @@ -18,8 +20,8 @@
wait_for_value,
)

from ._core_io import NDFileHDFIO
from ._utils import FileWriteMode, convert_ad_dtype_to_np
from ._core_io import NDArrayBaseIO, NDFileHDFIO
from ._utils import ADBaseDataType, FileWriteMode, convert_ad_dtype_to_np


class ADHDFWriter(DetectorWriter):
Expand All @@ -29,13 +31,14 @@ def __init__(
path_provider: PathProvider,
name_provider: NameProvider,
shape_provider: ShapeProvider,
**scalar_datasets_paths: str,
plugins: Sequence[NDArrayBaseIO] | None = None,
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
self.hdf = hdf
self._path_provider = path_provider
self._name_provider = name_provider
self._shape_provider = shape_provider
self._scalar_datasets_paths = scalar_datasets_paths

self._plugins = plugins or []
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
self._capture_status: Optional[AsyncStatus] = None
self._datasets: List[HDFDataset] = []
self._file: Optional[HDFFile] = None
Expand Down Expand Up @@ -90,16 +93,25 @@ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:
)
]
# And all the scalar datasets
for ds_name, ds_path in self._scalar_datasets_paths.items():
self._datasets.append(
HDFDataset(
f"{name}-{ds_name}",
f"/entry/instrument/NDAttributes/{ds_path}",
(),
"",
multiplier,
)
)
try:
for plugin in self._plugins:
tree = ET.parse((await plugin.nd_attributes_file.get_value()))
root = tree.getroot()
for child in root:
datakey = child.attrib["name"]
self._datasets.append(
HDFDataset(
datakey,
f"/entry/instrument/NDAttributes/{datakey}",
(),
convert_ad_dtype_to_np(
ADBaseDataType((child.attrib.get("datatype", None)))
),
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
multiplier,
)
)
except ET.ParseError:
raise ValueError("Error parsing XML")
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved

describe = {
ds.data_key: DataKey(
Expand Down Expand Up @@ -148,8 +160,8 @@ async def collect_stream_docs(

async def close(self):
# Already done a caput callback in _capture_status, so can't do one here
await self.hdf.capture.set(0, wait=False)
await wait_for_value(self.hdf.capture, 0, DEFAULT_TIMEOUT)
await self.hdf.capture.set(False, wait=False)
await wait_for_value(self.hdf.capture, False, DEFAULT_TIMEOUT)
if self._capture_status:
# We kicked off an open, so wait for it to return
await self._capture_status
Expand Down
93 changes: 23 additions & 70 deletions src/ophyd_async/epics/adcore/_utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from xml.etree import cElementTree as ET

from ophyd_async.core import DEFAULT_TIMEOUT, SignalRW, T, wait_for_value
from ophyd_async.core._signal import SignalR


class ADBaseDataType(str, Enum):
Expand All @@ -16,6 +17,7 @@ class ADBaseDataType(str, Enum):
UInt64 = "UInt64"
Float32 = "Float32"
Float64 = "Float64"
Double = "DOUBLE"


def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
Expand All @@ -30,6 +32,7 @@ def convert_ad_dtype_to_np(ad_dtype: ADBaseDataType) -> str:
ADBaseDataType.UInt64: "<u8",
ADBaseDataType.Float32: "<f4",
ADBaseDataType.Float64: "<f8",
ADBaseDataType.Double: "d",
}
return ad_dtype_to_np_dtype[ad_dtype]

Expand All @@ -52,75 +55,25 @@ class NDAttributeDataType(str, Enum):
STRING = "STRING"


class NDAttributesXML:
"""Helper to make NDAttributesFile XML for areaDetector"""

_dbr_types = {
None: "DBR_NATIVE",
NDAttributeDataType.INT: "DBR_LONG",
NDAttributeDataType.DOUBLE: "DBR_DOUBLE",
NDAttributeDataType.STRING: "DBR_STRING",
}

def __init__(self):
self._root = ET.Element("Attributes")

def add_epics_pv(
self,
name: str,
pv: str,
datatype: Optional[NDAttributeDataType] = None,
description: str = "",
):
"""Add a PV to the attribute list

Args:
name: The attribute name
pv: The pv to get from
datatype: An override datatype, otherwise will use native EPICS type
description: A description that appears in the HDF file as an attribute
"""
ET.SubElement(
self._root,
"Attribute",
name=name,
type="EPICS_PV",
source=pv,
datatype=self._dbr_types[datatype],
description=description,
)

def add_param(
self,
name: str,
param: str,
datatype: NDAttributeDataType,
addr: int = 0,
description: str = "",
):
"""Add a driver or plugin parameter to the attribute list

Args:
name: The attribute name
param: The parameter string as seen in the INP link of the record
datatype: The datatype of the parameter
description: A description that appears in the HDF file as an attribute
"""
ET.SubElement(
self._root,
"Attribute",
name=name,
type="PARAM",
source=param,
addr=str(addr),
datatype=datatype.value,
description=description,
)

def __str__(self) -> str:
"""Output the XML pretty printed"""
ET.indent(self._root, space=" ", level=0)
return ET.tostring(self._root, xml_declaration=True, encoding="utf-8").decode()
@dataclass
class NDAttributePv:
name: str # name of attribute stamped on array, also scientifically useful name
# when appended to device.name
signal: SignalR # caget the pv given by signal.source and attach to each frame
datatype: Optional[NDAttributeDataType] = (
None # An override datatype, otherwise will use native EPICS type
)
description: str = "" # A description that appears in the HDF file as an attribute


@dataclass
class NDAttributeParam:
name: str # name of attribute stamped on array, also scientifically useful name
# when appended to device.name
param: str # The parameter string as seen in the INP link of the record
datatype: NDAttributeDataType # The datatype of the parameter
addr: int = 0 # The address as seen in the INP link of the record
description: str = "" # A description that appears in the HDF file as an attribute


async def stop_busy_record(
Expand Down
3 changes: 3 additions & 0 deletions src/ophyd_async/plan_stubs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
prepare_static_seq_table_flyer_and_detectors_with_same_trigger,
time_resolved_fly_and_collect_with_static_seq_table,
)
from ._nd_attributes import setup_ndattributes, setup_ndstats_sum

__all__ = [
"fly_and_collect",
"prepare_static_seq_table_flyer_and_detectors_with_same_trigger",
"time_resolved_fly_and_collect_with_static_seq_table",
"ensure_connected",
"setup_ndattributes",
"setup_ndstats_sum",
]
68 changes: 68 additions & 0 deletions src/ophyd_async/plan_stubs/_nd_attributes.py
ZohebShaikh marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Sequence
from xml.etree import cElementTree as ET

import bluesky.plan_stubs as bps

from ophyd_async.core._device import Device
from ophyd_async.epics.adcore._core_io import NDArrayBaseIO
from ophyd_async.epics.adcore._utils import (
NDAttributeDataType,
NDAttributeParam,
NDAttributePv,
)


def setup_ndattributes(
device: NDArrayBaseIO, ndattributes: Sequence[NDAttributePv | NDAttributeParam]
):
xml_text = ET.Element("Attributes")
_dbr_types = {
None: "DBR_NATIVE",
NDAttributeDataType.INT: "DBR_LONG",
NDAttributeDataType.DOUBLE: "DBR_DOUBLE",
NDAttributeDataType.STRING: "DBR_STRING",
}
for ndattribute in ndattributes:
if isinstance(ndattribute, NDAttributeParam):
ET.SubElement(
xml_text,
"Attribute",
name=ndattribute.name,
type="PARAM",
source=ndattribute.param,
addr=str(ndattribute.addr),
datatype=_dbr_types[ndattribute.datatype],
description=ndattribute.description,
)
elif isinstance(ndattribute, NDAttributePv):
ET.SubElement(
xml_text,
"Attribute",
name=ndattribute.name,
type="EPICS_PV",
source=ndattribute.signal.source,
datatype=_dbr_types[ndattribute.datatype],
description=ndattribute.description,
)
else:
raise ValueError(
f"Invalid type for ndattributes: {type(ndattribute)}. "
"Expected NDAttributePv or NDAttributeParam."
)
yield from bps.mv(device.nd_attributes_file, xml_text)


def setup_ndstats_sum(detector: Device):
yield from (
setup_ndattributes(
detector.hdf,
[
NDAttributeParam(
name=f"{detector.name}-sum",
param="NDPluginStatsTotal",
datatype=NDAttributeDataType.INT,
description="Sum of the array",
)
],
)
)
19 changes: 0 additions & 19 deletions tests/epics/adcore/test_utils_adcore.py

This file was deleted.

Loading
Loading