Skip to content

Commit

Permalink
Merge branch 'main' into 131-make-a-simulated-detector-that-can-write…
Browse files Browse the repository at this point in the history
…-hdf-files
  • Loading branch information
stan-dot authored Apr 4, 2024
2 parents c3d5beb + b134436 commit d04c5dd
Show file tree
Hide file tree
Showing 17 changed files with 407 additions and 203 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies = [
"numpy",
"packaging",
"pint",
"bluesky @ git+https://github.com/bluesky/bluesky.git@collect-multiple-detectors", # Pin to Bluesky branch while still under work
"bluesky>=1.13.0a3",
"event-model",
"p4p",
"pyyaml",
Expand Down
20 changes: 4 additions & 16 deletions src/ophyd_async/epics/_backend/_aioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
)
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected

from .common import get_supported_enum_class

dbr_to_dtype: Dict[Dbr, Dtype] = {
dbr.DBR_STRING: "string",
dbr.DBR_SHORT: "integer",
Expand Down Expand Up @@ -90,9 +92,7 @@ def value(self, value: AugmentedValue):

def descriptor(self, source: str, value: AugmentedValue) -> Descriptor:
choices = [e.value for e in self.enum_class]
return dict(
source=source, dtype="string", shape=[], choices=choices
) # type: ignore
return dict(source=source, dtype="string", shape=[], choices=choices)


class DisconnectedCaConverter(CaConverter):
Expand Down Expand Up @@ -138,19 +138,7 @@ def make_converter(
pv_choices = get_unique(
{k: tuple(v.enums) for k, v in values.items()}, "choices"
)
if datatype:
if not issubclass(datatype, Enum):
raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
if not issubclass(datatype, str):
raise TypeError(f"{pv} has type Enum but doesn't inherit from String")
choices = tuple(v.value for v in datatype)
if set(choices) != set(pv_choices):
raise TypeError(f"{pv} has choices {pv_choices} not {choices}")
enum_class = datatype
else:
enum_class = Enum( # type: ignore
"GeneratedChoices", {x: x for x in pv_choices}, type=str
)
enum_class = get_supported_enum_class(pv, datatype, pv_choices)
return CaEnumConverter(dbr.DBR_STRING, None, enum_class)
else:
value = list(values.values())[0]
Expand Down
62 changes: 45 additions & 17 deletions src/ophyd_async/epics/_backend/_p4p.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import atexit
import logging
import time
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Type, Union
Expand All @@ -19,6 +20,8 @@
)
from ophyd_async.core.utils import DEFAULT_TIMEOUT, NotConnected

from .common import get_supported_enum_class

# https://mdavidsaver.github.io/p4p/values.html
specifier_to_dtype: Dict[str, Dtype] = {
"?": "integer", # bool
Expand Down Expand Up @@ -119,9 +122,7 @@ def value(self, value):

def descriptor(self, source: str, value) -> Descriptor:
choices = [e.value for e in self.enum_class]
return dict(
source=source, dtype="string", shape=[], choices=choices
) # type: ignore
return dict(source=source, dtype="string", shape=[], choices=choices)


class PvaEnumBoolConverter(PvaConverter):
Expand All @@ -141,6 +142,32 @@ def descriptor(self, source: str, value) -> Descriptor:
return dict(source=source, dtype="object", shape=[]) # type: ignore


class PvaDictConverter(PvaConverter):
def reading(self, value):
ts = time.time()
value = value.todict()
# Alarm severity is vacuously 0 for a table
return dict(value=value, timestamp=ts, alarm_severity=0)

def value(self, value: Value):
return value.todict()

def descriptor(self, source: str, value) -> Descriptor:
raise NotImplementedError("Describing Dict signals not currently supported")

def metadata_fields(self) -> List[str]:
"""
Fields to request from PVA for metadata.
"""
return []

def value_fields(self) -> List[str]:
"""
Fields to request from PVA for the value.
"""
return []


class DisconnectedPvaConverter(PvaConverter):
def __getattribute__(self, __name: str) -> Any:
raise NotImplementedError("No PV has been set as connect() has not been called")
Expand All @@ -149,7 +176,9 @@ def __getattribute__(self, __name: str) -> Any:
def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConverter:
pv = list(values)[0]
typeid = get_unique({k: v.getID() for k, v in values.items()}, "typeids")
typ = get_unique({k: type(v["value"]) for k, v in values.items()}, "value types")
typ = get_unique(
{k: type(v.get("value")) for k, v in values.items()}, "value types"
)
if "NTScalarArray" in typeid and typ == list:
# Waveform of strings, check we wanted this
if datatype and datatype != Sequence[str]:
Expand Down Expand Up @@ -185,24 +214,15 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
pv_choices = get_unique(
{k: tuple(v["value"]["choices"]) for k, v in values.items()}, "choices"
)
if datatype:
if not issubclass(datatype, Enum):
raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
choices = tuple(v.value for v in datatype)
if set(choices) != set(pv_choices):
raise TypeError(f"{pv} has choices {pv_choices} not {choices}")
enum_class = datatype
else:
enum_class = Enum( # type: ignore
"GeneratedChoices", {x or "_": x for x in pv_choices}, type=str
)
return PvaEnumConverter(enum_class)
return PvaEnumConverter(get_supported_enum_class(pv, datatype, pv_choices))
elif "NTScalar" in typeid:
if datatype and not issubclass(typ, datatype):
raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
return PvaConverter()
elif "NTTable" in typeid:
return PvaTableConverter()
elif "structure" in typeid:
return PvaDictConverter()
else:
raise TypeError(f"{pv}: Unsupported typeid {typeid}")

Expand Down Expand Up @@ -260,7 +280,15 @@ async def put(self, value: Optional[T], wait=True, timeout=None):
else:
write_value = self.converter.write_value(value)
coro = self.ctxt.put(self.write_pv, dict(value=write_value), wait=wait)
await asyncio.wait_for(coro, timeout)
try:
await asyncio.wait_for(coro, timeout)
except asyncio.TimeoutError as exc:
logging.debug(
f"signal pva://{self.write_pv} timed out \
put value: {write_value}",
exc_info=True,
)
raise NotConnected(f"pva://{self.write_pv}") from exc

async def get_descriptor(self) -> Descriptor:
value = await self.ctxt.get(self.read_pv)
Expand Down
20 changes: 20 additions & 0 deletions src/ophyd_async/epics/_backend/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from enum import Enum
from typing import Any, Optional, Tuple, Type


def get_supported_enum_class(
pv: str,
datatype: Optional[Type[Enum]],
pv_choices: Tuple[Any, ...],
) -> Type[Enum]:
if datatype:
if not issubclass(datatype, Enum):
raise TypeError(f"{pv} has type Enum not {datatype.__name__}")
if not issubclass(datatype, str):
raise TypeError(f"{pv} has type Enum but doesn't inherit from String")
choices = tuple(v.value for v in datatype)
if set(choices).difference(pv_choices):
raise TypeError(f"{pv} has choices {pv_choices}: not all in {choices}")
return Enum(
"GeneratedChoices", {x or "_": x for x in pv_choices}, type=str
) # type: ignore
70 changes: 70 additions & 0 deletions src/ophyd_async/epics/pvi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from typing import Callable, Dict, FrozenSet, Optional, Type, TypedDict, TypeVar

from ophyd_async.core.signal import Signal
from ophyd_async.core.signal_backend import SignalBackend
from ophyd_async.core.utils import DEFAULT_TIMEOUT
from ophyd_async.epics._backend._p4p import PvaSignalBackend
from ophyd_async.epics.signal.signal import (
epics_signal_r,
epics_signal_rw,
epics_signal_w,
epics_signal_x,
)

T = TypeVar("T")


_pvi_mapping: Dict[FrozenSet[str], Callable[..., Signal]] = {
frozenset({"r", "w"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
dtype, read_pv, write_pv
),
frozenset({"rw"}): lambda dtype, read_pv, write_pv: epics_signal_rw(
dtype, read_pv, write_pv
),
frozenset({"r"}): lambda dtype, read_pv, _: epics_signal_r(dtype, read_pv),
frozenset({"w"}): lambda dtype, _, write_pv: epics_signal_w(dtype, write_pv),
frozenset({"x"}): lambda _, __, write_pv: epics_signal_x(write_pv),
}


class PVIEntry(TypedDict, total=False):
d: str
r: str
rw: str
w: str
x: str


async def pvi_get(
read_pv: str, timeout: float = DEFAULT_TIMEOUT
) -> Dict[str, PVIEntry]:
"""Makes a PvaSignalBackend purely to connect to PVI information.
This backend is simply thrown away at the end of this method. This is useful
because the backend handles a CancelledError exception that gets thrown on
timeout, and therefore can be used for error reporting."""
backend: SignalBackend = PvaSignalBackend(None, read_pv, read_pv)
await backend.connect(timeout=timeout)
d: Dict[str, Dict[str, Dict[str, str]]] = await backend.get_value()
pv_info = d.get("pvi") or {}
result = {}

for attr_name, attr_info in pv_info.items():
result[attr_name] = PVIEntry(**attr_info) # type: ignore

return result


def make_signal(signal_pvi: PVIEntry, dtype: Optional[Type[T]] = None) -> Signal[T]:
"""Make a signal.
This assumes datatype is None so it can be used to create dynamic signals.
"""
operations = frozenset(signal_pvi.keys())
pvs = [signal_pvi[i] for i in operations] # type: ignore
signal_factory = _pvi_mapping[operations]

write_pv = "pva://" + pvs[0]
read_pv = write_pv if len(pvs) < 2 else "pva://" + pvs[1]

return signal_factory(dtype, read_pv, write_pv)
2 changes: 0 additions & 2 deletions src/ophyd_async/epics/signal/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from .pvi_get import pvi_get
from .signal import epics_signal_r, epics_signal_rw, epics_signal_w, epics_signal_x

__all__ = [
"pvi_get",
"epics_signal_r",
"epics_signal_rw",
"epics_signal_w",
Expand Down
22 changes: 0 additions & 22 deletions src/ophyd_async/epics/signal/pvi_get.py

This file was deleted.

5 changes: 3 additions & 2 deletions src/ophyd_async/panda/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .panda import PandA, PcapBlock, PulseBlock, PVIEntry, SeqBlock, SeqTable, pvi
from .panda import PandA, PcapBlock, PulseBlock, PVIEntry, SeqBlock, SeqTable
from .panda_controller import PandaPcapController
from .table import (
SeqTable,
SeqTableRow,
Expand All @@ -19,6 +20,6 @@
"SeqTable",
"SeqTableRow",
"SeqTrigger",
"pvi",
"phase_sorter",
"PandaPcapController",
]
Loading

0 comments on commit d04c5dd

Please sign in to comment.