From c2e859ab6e865b41c93dae221e852299035c4525 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 14 Oct 2024 17:08:09 +0100 Subject: [PATCH] implemented panda controller --- src/fastcs_pandablocks/__init__.py | 44 +- src/fastcs_pandablocks/__main__.py | 22 +- src/fastcs_pandablocks/block.py | 493 ++++++++++++++++++ src/fastcs_pandablocks/blocks.py | 39 ++ .../{panda => }/client_wrapper.py | 33 +- src/fastcs_pandablocks/controller.py | 52 ++ src/fastcs_pandablocks/fastcs/__init__.py | 34 -- src/fastcs_pandablocks/fastcs/controller.py | 14 - src/fastcs_pandablocks/{fastcs => }/gui.py | 1 + src/fastcs_pandablocks/handler.py | 15 + src/fastcs_pandablocks/panda/__init__.py | 1 - src/fastcs_pandablocks/panda/blocks.py | 239 --------- src/fastcs_pandablocks/panda/panda.py | 71 --- src/fastcs_pandablocks/types.py | 198 ------- src/fastcs_pandablocks/types/__init__.py | 17 + src/fastcs_pandablocks/types/annotations.py | 32 ++ src/fastcs_pandablocks/types/string_types.py | 208 ++++++++ tests/test_types.py | 20 +- 18 files changed, 933 insertions(+), 600 deletions(-) create mode 100644 src/fastcs_pandablocks/block.py create mode 100644 src/fastcs_pandablocks/blocks.py rename src/fastcs_pandablocks/{panda => }/client_wrapper.py (76%) create mode 100644 src/fastcs_pandablocks/controller.py delete mode 100644 src/fastcs_pandablocks/fastcs/__init__.py delete mode 100644 src/fastcs_pandablocks/fastcs/controller.py rename src/fastcs_pandablocks/{fastcs => }/gui.py (99%) create mode 100644 src/fastcs_pandablocks/handler.py delete mode 100644 src/fastcs_pandablocks/panda/__init__.py delete mode 100644 src/fastcs_pandablocks/panda/blocks.py delete mode 100644 src/fastcs_pandablocks/panda/panda.py delete mode 100644 src/fastcs_pandablocks/types.py create mode 100644 src/fastcs_pandablocks/types/__init__.py create mode 100644 src/fastcs_pandablocks/types/annotations.py create mode 100644 src/fastcs_pandablocks/types/string_types.py diff --git a/src/fastcs_pandablocks/__init__.py b/src/fastcs_pandablocks/__init__.py index a2ffbf3..49fa8fb 100644 --- a/src/fastcs_pandablocks/__init__.py +++ b/src/fastcs_pandablocks/__init__.py @@ -1,11 +1,45 @@ -"""Top level API. +"""Contains logic relevant to fastcs. Will use `fastcs_pandablocks.panda`.""" -.. data:: __version__ - :type: str +from pathlib import Path - Version number as calculated by https://github.com/pypa/setuptools_scm -""" +from fastcs.backends.epics.backend import EpicsBackend +from fastcs.backends.epics.gui import EpicsGUIFormat from ._version import __version__ +from .controller import PandaController +from .gui import PandaGUIOptions +from .types import EpicsName __all__ = ["__version__"] + + +def ioc( + prefix: EpicsName, + hostname: str, + screens_directory: Path | None, + clear_bobfiles: bool = False, +): + controller = PandaController(prefix, hostname) + backend = EpicsBackend(controller, pv_prefix=str(prefix)) + + if clear_bobfiles and not screens_directory: + raise ValueError("`clear_bobfiles` is True with no `screens_directory`") + + if screens_directory: + if not screens_directory.is_dir(): + raise ValueError( + f"`screens_directory` {screens_directory} is not a directory" + ) + if not clear_bobfiles: + if list(screens_directory.iterdir()): + raise RuntimeError("`screens_directory` is not empty.") + + backend.create_gui( + PandaGUIOptions( + output_path=screens_directory / "output.bob", + file_format=EpicsGUIFormat.bob, + title="PandA", + ) + ) + + backend.run() diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index e89fe3b..894be47 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -2,11 +2,10 @@ import argparse import logging -import asyncio - -#from fastcs_pandablocks.fastcs import ioc -from fastcs_pandablocks.panda.panda import Panda +from pathlib import Path +from fastcs_pandablocks import ioc +from fastcs_pandablocks.types import EpicsName from . import __version__ @@ -18,7 +17,7 @@ def main(): parser = argparse.ArgumentParser( description="Connect to the given HOST and create an IOC with the given PREFIX." ) - parser.add_argument("host", type=str, help="The host to connect to.") + parser.add_argument("hostname", type=str, help="The host to connect to.") parser.add_argument("prefix", type=str, help="The prefix for the IOC.") parser.add_argument( "--screens-dir", @@ -52,19 +51,12 @@ def main(): level = getattr(logging, parsed_args.log_level.upper(), None) logging.basicConfig(format="%(levelname)s:%(message)s", level=level) - async def meh(): - await Panda(parsed_args.host).connect() - asyncio.run(meh()) - - - """ ioc( - parsed_args.host, - parsed_args.prefix, - parsed_args.screens_dir, + EpicsName(prefix=parsed_args.prefix), + parsed_args.hostname, + Path(parsed_args.screens_dir), parsed_args.clear_bobfiles, ) - """ if __name__ == "__main__": diff --git a/src/fastcs_pandablocks/block.py b/src/fastcs_pandablocks/block.py new file mode 100644 index 0000000..4e74e8c --- /dev/null +++ b/src/fastcs_pandablocks/block.py @@ -0,0 +1,493 @@ +from __future__ import annotations + +from fastcs.attributes import AttrR, AttrRW, AttrW +from fastcs.datatypes import Bool, Float, Int, String + +from fastcs_pandablocks.types import EpicsName, PandaName, ResponseType + +from .handler import FieldSender + + +class Field: + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None, + datatype: Int | Float | String | Bool, + attribute: type[AttrRW] | type[AttrR] | type[AttrW], + sub_fields: dict[str, Field] | None = None + ): + self.sub_fields = sub_fields or {} + self.epics_name = epics_name + self.panda_name = panda_name + self.description = description + + self.datatype = datatype + handler = FieldSender(panda_name) if attribute is AttrW else None + if attribute is AttrW: + self.attribute = attribute(datatype, handler=handler) + else: + self.attribute = attribute(datatype) + + async def update_value(self, sub_field: str | None, value: str): + if sub_field: + await self.sub_fields[sub_field].update_value(None, value) + elif isinstance(self.attribute, AttrW): + await self.attribute.process(value) + else: + await self.attribute.set(value) + + +class TableField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + +class TimeField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + +class BitOutField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class PosOutField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class ExtOutField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class ExtOutBitsField(ExtOutField): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class BitMuxField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class PosMuxField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class UintParamField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class UintReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class UintWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class IntParamField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class IntReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class IntWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class ScalarParamField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class ScalarReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + + ... + +class ScalarWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class BitParamField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class BitReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class BitWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class ActionReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class ActionWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class LutParamField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class LutReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class LutWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class EnumParamField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class EnumReadField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class EnumWriteField(Field): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class TimeSubTypeParamField(TimeField): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class TimeSubTypeReadField(TimeField): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +class TimeSubTypeWriteField(TimeField): + def __init__( + self, + epics_name: EpicsName, + panda_name: PandaName, + description: str | None + ): + ... + ... + +FieldType = ( + TableField | + TimeField | + BitOutField | + PosOutField | + ExtOutField | + ExtOutBitsField | + BitMuxField | + PosMuxField | + UintParamField | + UintReadField | + UintWriteField | + IntParamField | + IntReadField | + IntWriteField | + ScalarParamField | + ScalarReadField | + ScalarWriteField | + BitParamField | + BitReadField | + BitWriteField | + ActionReadField | + ActionWriteField | + LutParamField | + LutReadField | + LutWriteField | + EnumParamField | + EnumReadField | + EnumWriteField | + TimeSubTypeParamField | + TimeSubTypeReadField | + TimeSubTypeWriteField +) + +FIELD_TYPE_TO_FASTCS_TYPE: dict[str, dict[str | None, type[FieldType]]] = { + "table": { + None: TableField + }, + "time": { + None: TimeField, + "param": TimeSubTypeParamField, + "read": TimeSubTypeReadField, + "write": TimeSubTypeWriteField, + }, + "bit_out": { + None: BitOutField, + }, + "pos_out": { + None: PosOutField, + }, + "ext_out": { + "timestamp": ExtOutField, + "samples": ExtOutField, + "bits": ExtOutBitsField, + }, + "bit_mux": { + None: BitMuxField, + }, + "pos_mux": { + None: PosMuxField, + }, + "param": { + "uint": UintParamField, + "int": IntParamField, + "scalar": ScalarParamField, + "bit": BitParamField, + "action": ActionReadField, + "lut": LutParamField, + "enum": EnumParamField, + "time": TimeSubTypeParamField, + }, + "read": { + "uint": UintReadField, + "int": IntReadField, + "scalar": ScalarReadField, + "bit": BitReadField, + "action": ActionReadField, + "lut": LutReadField, + "enum": EnumReadField, + "time": TimeSubTypeReadField, + }, + "write": { + "uint": UintWriteField, + "int": IntWriteField, + "scalar": ScalarWriteField, + "bit": BitWriteField, + "action": ActionWriteField, + "lut": LutWriteField, + "enum": EnumWriteField, + "time": TimeSubTypeWriteField, + }, +} + +class Block: + _fields: dict[int | None, dict[str, FieldType]] + + def __init__( + self, + epics_name: EpicsName, + number: int, + description: str | None | None, + raw_fields: dict[str, ResponseType] + ): + self.epics_name = epics_name + self.number = number + self.description = description + self._fields = {} + + iterator = range(1, number + 1) if number > 1 else iter([None]) + + for block_number in iterator: + numbered_block_name = epics_name + EpicsName(block_number=block_number) + self._fields[block_number] = {} + + for field_raw_name, field_info in ( + raw_fields.items() + ): + field_epics_name = numbered_block_name + EpicsName(field=field_raw_name) + field_panda_name = field_epics_name.to_panda_name() + + field = FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( + field_epics_name, field_panda_name, field_info.description + ) + self._fields[block_number][field_raw_name] = field + + async def update_field(self, panda_name: PandaName, value: str): + assert panda_name.field + await self._fields[panda_name.block_number][panda_name.field].update_value( + panda_name.sub_field, value + ) diff --git a/src/fastcs_pandablocks/blocks.py b/src/fastcs_pandablocks/blocks.py new file mode 100644 index 0000000..46a89cd --- /dev/null +++ b/src/fastcs_pandablocks/blocks.py @@ -0,0 +1,39 @@ + +from pandablocks.responses import BlockInfo + +from fastcs_pandablocks.types.string_types import EpicsName, PandaName + +from .block import Block +from .types import ResponseType + + +class Blocks: + _blocks: dict[str, Block] + epics_prefix: EpicsName + + def __init__(self, prefix: EpicsName): + self.prefix = prefix + self._blocks = {} + + def parse_introspected_data( + self, + blocks: dict[str, BlockInfo], + fields: list[dict[str, ResponseType]] + ): + self._blocks = {} + + for (block_name, block_info), raw_fields in zip( + blocks.items(), fields, strict=True + ): + + self._blocks[block_name] = Block( + self.prefix + EpicsName(block=block_name), + block_info.number, + block_info.description, + raw_fields + ) + + async def update_field(self, panda_name: PandaName, value: str): + assert panda_name.block + await self._blocks[panda_name.block].update_field(panda_name, value) + diff --git a/src/fastcs_pandablocks/panda/client_wrapper.py b/src/fastcs_pandablocks/client_wrapper.py similarity index 76% rename from src/fastcs_pandablocks/panda/client_wrapper.py rename to src/fastcs_pandablocks/client_wrapper.py index 4eb3a50..331ce60 100644 --- a/src/fastcs_pandablocks/panda/client_wrapper.py +++ b/src/fastcs_pandablocks/client_wrapper.py @@ -1,27 +1,23 @@ """ -Over the years we've had to add little adjustments on top of the `BlockInfo`, `BlockAndFieldInfo`, etc. - This method has a `RawPanda` which handles all the io with the client. """ import asyncio -from dataclasses import dataclass -from pprint import pprint -from typing import TypedDict + from pandablocks.asyncio import AsyncioClient from pandablocks.commands import ( ChangeGroup, - Changes, GetBlockInfo, GetChanges, GetFieldInfo, + Put, ) from pandablocks.responses import ( BlockInfo, - Changes, ) -from fastcs_pandablocks.types import PandaName, ResponseType +from fastcs_pandablocks.types import ResponseType + class RawPanda: blocks: dict[str, BlockInfo] | None = None @@ -29,9 +25,9 @@ class RawPanda: metadata: dict[str, str] | None = None changes: dict[str, str] | None = None - def __init__(self, host: str): - self._client = AsyncioClient(host) - + def __init__(self, hostname: str): + self._client = AsyncioClient(host=hostname) + async def connect(self): await self._client.connect() await self.introspect() @@ -42,14 +38,16 @@ async def disconnect(self): self.fields = None self.metadata = None self.changes = None - + async def introspect(self): self.blocks, self.fields, self.metadata, self.changes = {}, [], {}, {} self.blocks = await self._client.send(GetBlockInfo()) self.fields = await asyncio.gather( *[self._client.send(GetFieldInfo(block)) for block in self.blocks], ) - initial_values = (await self._client.send(GetChanges(ChangeGroup.ALL, True))).values + initial_values = ( + await self._client.send(GetChanges(ChangeGroup.ALL, True)) + ).values for field_name, value in initial_values.items(): if field_name.startswith("*METADATA"): @@ -57,11 +55,16 @@ async def introspect(self): else: self.changes[field_name] = value + async def send(self, name: str, value: str): + await self._client.send(Put(name, value)) + async def get_changes(self): if not self.changes: raise RuntimeError("Panda not introspected.") - self.changes = (await self._client.send(GetChanges(ChangeGroup.ALL, False))).values - + self.changes = ( + await self._client.send(GetChanges(ChangeGroup.ALL, False)) + ).values + async def _ensure_connected(self): if not self.blocks: await self.connect() diff --git a/src/fastcs_pandablocks/controller.py b/src/fastcs_pandablocks/controller.py new file mode 100644 index 0000000..f0a27f3 --- /dev/null +++ b/src/fastcs_pandablocks/controller.py @@ -0,0 +1,52 @@ +import asyncio + +from fastcs.controller import Controller +from fastcs.wrappers import scan + +from fastcs_pandablocks.types.string_types import PandaName + +from .blocks import Blocks +from .client_wrapper import RawPanda +from .types import EpicsName + +POLL_PERIOD = 0.1 + +class PandaController(Controller): + def __init__(self, prefix: EpicsName, hostname: str) -> None: + self._raw_panda = RawPanda(hostname) + self._blocks = Blocks(prefix) + super().__init__() + + async def initialise(self) -> None: + ... + + async def put_value_to_panda(self, name: PandaName, value: str): + await self._raw_panda.send(str(name), value) + + async def connect(self) -> None: + if ( + self._raw_panda.blocks is None or + self._raw_panda.fields is None or + self._raw_panda.metadata is None or + self._raw_panda.changes is None + ): + await self._raw_panda.connect() + + + assert self._raw_panda.blocks + assert self._raw_panda.fields + self._blocks.parse_introspected_data( + self._raw_panda.blocks, self._raw_panda.fields + ) + + + @scan(POLL_PERIOD) + async def update(self): + await self._raw_panda.get_changes() + assert self._raw_panda.changes + await asyncio.gather( + *[ + self._blocks.update_field(PandaName(raw_panda_name), value) + for raw_panda_name, value in self._raw_panda.changes.items() + ] + ) diff --git a/src/fastcs_pandablocks/fastcs/__init__.py b/src/fastcs_pandablocks/fastcs/__init__.py deleted file mode 100644 index 276d44d..0000000 --- a/src/fastcs_pandablocks/fastcs/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Contains logic relevant to fastcs. Will use `fastcs_pandablocks.panda`.""" - - -from pathlib import Path - -from fastcs.backends.epics.backend import EpicsBackend - -from .gui import PandaGUIOptions -from .controller import PandaController -from fastcs_pandablocks.types import EpicsName - - -def ioc( - panda_hostname: str, - pv_prefix: EpicsName, - screens_directory: Path | None, - clear_bobfiles: bool = False, -): - controller = PandaController(panda_hostname) - backend = EpicsBackend(controller, pv_prefix=str(pv_prefix)) - - if clear_bobfiles and not screens_directory: - raise ValueError("`clear_bobfiles` is True with no `screens_directory`") - - if screens_directory: - if not screens_directory.is_dir(): - raise ValueError( - f"`screens_directory` {screens_directory} is not a directory" - ) - backend.create_gui( - PandaGUIOptions() - ) - - backend.run() diff --git a/src/fastcs_pandablocks/fastcs/controller.py b/src/fastcs_pandablocks/fastcs/controller.py deleted file mode 100644 index bd564b5..0000000 --- a/src/fastcs_pandablocks/fastcs/controller.py +++ /dev/null @@ -1,14 +0,0 @@ -# TODO: tackle after I have a MVP of the panda part. -from fastcs.controller import Controller -from fastcs.datatypes import Bool, Float, Int, String - - -class PandaController(Controller): - def __init__(self, hostname: str) -> None: - super().__init__() - - async def initialise(self) -> None: - pass - - async def connect(self) -> None: - pass diff --git a/src/fastcs_pandablocks/fastcs/gui.py b/src/fastcs_pandablocks/gui.py similarity index 99% rename from src/fastcs_pandablocks/fastcs/gui.py rename to src/fastcs_pandablocks/gui.py index 7334017..4285c76 100644 --- a/src/fastcs_pandablocks/fastcs/gui.py +++ b/src/fastcs_pandablocks/gui.py @@ -1,4 +1,5 @@ from fastcs.backends.epics.gui import EpicsGUIOptions + class PandaGUIOptions(EpicsGUIOptions): ... diff --git a/src/fastcs_pandablocks/handler.py b/src/fastcs_pandablocks/handler.py new file mode 100644 index 0000000..7d87956 --- /dev/null +++ b/src/fastcs_pandablocks/handler.py @@ -0,0 +1,15 @@ +from typing import Any + +from fastcs.attributes import AttrW, Sender + +from fastcs_pandablocks.types.string_types import PandaName + +#from fastcs_pandablocks.controller import PandaController + +class FieldSender(Sender): + def __init__(self, panda_name: PandaName): + self.panda_name = panda_name + + async def put(self, controller: Any, attr: AttrW, value: str) -> None: + await controller.put_value_to_panda(self.panda_name, value) + diff --git a/src/fastcs_pandablocks/panda/__init__.py b/src/fastcs_pandablocks/panda/__init__.py deleted file mode 100644 index f0a9692..0000000 --- a/src/fastcs_pandablocks/panda/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Contains the logic relevant to the Panda's operation.""" diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py deleted file mode 100644 index afbb11b..0000000 --- a/src/fastcs_pandablocks/panda/blocks.py +++ /dev/null @@ -1,239 +0,0 @@ -import itertools -from pprint import pprint -from typing import Type -from fastcs_pandablocks.types import EpicsName, PandaName, ResponseType - -panda_name_to_field = {} - -class Field: - def __init__(self, epics_name: EpicsName, panda_name: PandaName, field_info: ResponseType): - self.epics_name = epics_name - self.panda_name = panda_name - self.field_info = field_info - self.value = None - panda_name_to_field[panda_name] = self - - def update_value(self, value): - self.value = value - -class TableField(Field): - ... - -class TimeField(Field): - ... - -class BitOutField(Field): - ... - -class PosOutField(Field): - ... - -class ExtOutField(Field): - ... - -class ExtOutBitsField(ExtOutField): - ... - -class BitMuxField(Field): - ... - -class PosMuxField(Field): - ... - -class UintParamField(Field): - ... - -class UintReadField(Field): - ... - -class UintWriteField(Field): - ... - -class IntParamField(Field): - ... - -class IntReadField(Field): - ... - -class IntWriteField(Field): - ... - -class ScalarParamField(Field): - ... - -class ScalarReadField(Field): - ... - -class ScalarWriteField(Field): - ... - -class BitParamField(Field): - ... - -class BitReadField(Field): - ... - -class BitWriteField(Field): - ... - -class ActionReadField(Field): - ... - -class ActionWriteField(Field): - ... - -class LutParamField(Field): - ... - -class LutReadField(Field): - ... - -class LutWriteField(Field): - ... - -class EnumParamField(Field): - ... - -class EnumReadField(Field): - ... - -class EnumWriteField(Field): - ... - -class TimeSubTypeParamField(TimeField): - ... - -class TimeSubTypeReadField(TimeField): - ... - -class TimeSubTypeWriteField(TimeField): - ... - -FieldType = ( - TableField | - TimeField | - BitOutField | - PosOutField | - ExtOutField | - ExtOutBitsField | - BitMuxField | - PosMuxField | - UintParamField | - UintReadField | - UintWriteField | - IntParamField | - IntReadField | - IntWriteField | - ScalarParamField | - ScalarReadField | - ScalarWriteField | - BitParamField | - BitReadField | - BitWriteField | - ActionReadField | - ActionWriteField | - LutParamField | - LutReadField | - LutWriteField | - EnumParamField | - EnumReadField | - EnumWriteField | - TimeSubTypeParamField | - TimeSubTypeReadField | - TimeSubTypeWriteField -) - -FIELD_TYPE_TO_FASTCS_TYPE: dict[str, dict[str | None, Type[FieldType]]] = { - "table": { - None: TableField - }, - "time": { - None: TimeField, - "param": TimeSubTypeParamField, - "read": TimeSubTypeReadField, - "write": TimeSubTypeWriteField, - }, - "bit_out": { - None: BitOutField, - }, - "pos_out": { - None: PosOutField, - }, - "ext_out": { - "timestamp": ExtOutField, - "samples": ExtOutField, - "bits": ExtOutBitsField, - }, - "bit_mux": { - None: BitMuxField, - }, - "pos_mux": { - None: PosMuxField, - }, - "param": { - "uint": UintParamField, - "int": IntParamField, - "scalar": ScalarParamField, - "bit": BitParamField, - "action": ActionReadField, - "lut": LutParamField, - "enum": EnumParamField, - "time": TimeSubTypeParamField, - }, - "read": { - "uint": UintReadField, - "int": IntReadField, - "scalar": ScalarReadField, - "bit": BitReadField, - "action": ActionReadField, - "lut": LutReadField, - "enum": EnumReadField, - "time": TimeSubTypeReadField, - }, - "write": { - "uint": UintWriteField, - "int": IntWriteField, - "scalar": ScalarWriteField, - "bit": BitWriteField, - "action": ActionWriteField, - "lut": LutWriteField, - "enum": EnumWriteField, - "time": TimeSubTypeWriteField, - }, -} - -class Block: - _fields: dict[int | None, dict[str, FieldType]] - - def __init__( - self, - epics_name: EpicsName, - number: int, - description: str | None, - raw_fields: dict[str, ResponseType] - ): - self.epics_name = epics_name - self.number = number - self.description = description - self._fields = {} - - for number in range(1, number + 1): - numbered_block_name = epics_name + EpicsName(block_number=number) - self._fields[number] = {} - - for field_raw_name, field_info in ( - raw_fields.items() - ): - field_epics_name_without_block = field_panda_name.to_epics_name() - print("part", field_epics_name_without_block) - field_epics_name = ( - numbered_block_name + field_epics_name_without_block - ) - print("WHOE", field_epics_name) - field = FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( - field_epics_name, field_panda_name, field_info - ) - self._fields[number][field_name] = field - - def update_value(self, number: int | None, field_name: str, value): - self._fields[number][field_name].update_value(value) diff --git a/src/fastcs_pandablocks/panda/panda.py b/src/fastcs_pandablocks/panda/panda.py deleted file mode 100644 index 322b760..0000000 --- a/src/fastcs_pandablocks/panda/panda.py +++ /dev/null @@ -1,71 +0,0 @@ -import asyncio -from pprint import pprint -from typing import Callable -from dataclasses import dataclass -from .client_wrapper import RawPanda -from .blocks import Block -from fastcs_pandablocks.types import EpicsName, PandaName -from pandablocks.responses import Changes -import logging - - - -class Panda: - _raw_panda: RawPanda - _blocks: dict[EpicsName, Block] - POLL_PERIOD = 0.1 - - def __init__(self, host: str): - self._raw_panda = RawPanda(host) - self._blocks = {} - - async def connect(self): - logging.info("Connecting to the panda.") - await self._raw_panda.connect() - logging.info("Parsing data.") - self._parse_introspected_data() - - def _parse_introspected_data(self): - self._blocks = {} - if ( - self._raw_panda.blocks is None or - self._raw_panda.fields is None or - self._raw_panda.metadata is None or - self._raw_panda.changes is None - ): - raise ValueError("Panda not introspected.") - - for (block_name, block_info), raw_fields in zip( - self._raw_panda.blocks.items(), self._raw_panda.fields - ): - self._blocks[EpicsName(block=block_name)] = Block( - EpicsName(block=block_name), - block_info.number, - block_info.description, - raw_fields - ) - self._parse_changes() - - - def _parse_changes(self): - assert self._raw_panda.changes is not None - for field_raw_name, field_value in self._raw_panda.changes.items(): - epics_name = PandaName.from_string(field_raw_name).to_epics_name() - block = self._blocks[EpicsName(block=epics_name.block)] - assert epics_name.field - block.update_value(epics_name.block_number, epics_name.field, field_value) - - async def poll_for_changes(self): - logging.info("Polling for data.") - # We make this a coroutine so it can happen alongside the - # sleep instead of before it. - async def parse_changes(): - self._parse_changes() - - async for _ in self._raw_panda: - await asyncio.gather( - parse_changes(), - asyncio.sleep(self.POLL_PERIOD) - ) - - async def disconnect(self): await self._raw_panda.disconnect() diff --git a/src/fastcs_pandablocks/types.py b/src/fastcs_pandablocks/types.py deleted file mode 100644 index 8a85325..0000000 --- a/src/fastcs_pandablocks/types.py +++ /dev/null @@ -1,198 +0,0 @@ -from __future__ import annotations -from fastcs.attributes import AttrR - -from dataclasses import dataclass -import re -from pandablocks.responses import ( - BitMuxFieldInfo, - BitOutFieldInfo, - BlockInfo, - Changes, - EnumFieldInfo, - ExtOutBitsFieldInfo, - ExtOutFieldInfo, - FieldInfo, - PosMuxFieldInfo, - PosOutFieldInfo, - ScalarFieldInfo, - SubtypeTimeFieldInfo, - TableFieldInfo, - TimeFieldInfo, - UintFieldInfo, -) -from typing import Union, TypeVar - -T = TypeVar("T") - -EPICS_SEPERATOR = ":" -PANDA_SEPERATOR = "." - -def _extract_number_at_of_string(string: str) -> tuple[str, int | None]: - pattern = r"(\D+)(\d+)$" - match = re.match(pattern, string) - if match: - return (match.group(1), int(match.group(2))) - return string, None - - -@dataclass(frozen=True) -class _Name: - _name: str - - def __str__(self): - return str(self._name) - def __repr__(self): - return str(self) - -class PandaName(_Name): - block: str | None = None - block_number: int | None = None - field: str | None = None - field_number: int | None = None - - def __init__( - self, - block: str | None = None, - block_number: int | None = None, - field: str | None = None, - field_number: int | None = None, - ): - self.block=block - self.block_number=block_number - self.field=field - self.field_number=field_number - super().__init__(f"{self.block}{self.block_number}{PANDA_SEPERATOR}{self.field}") - - @classmethod - def from_string(cls, name: str): - split_name = name.split(PANDA_SEPERATOR) - assert len(split_name) == 2 - block, block_number = _extract_number_at_of_string(split_name[0]) - field, field_number = _extract_number_at_of_string(split_name[1]) - return PandaName( - block=block, block_number=block_number, field=field, field_number=field_number - ) - - def to_epics_name(self): - split_panda_name = self._name.split(PANDA_SEPERATOR) - return EpicsName( - block=self.block, block_number=self.block_number, field=self.field, field_number=self.field_number - ) - -class EpicsName(_Name): - prefix: str | None = None - block: str | None = None - block_number: int | None = None - field: str | None = None - field_number: int | None = None - - def __init__( - self, - *, - prefix: str | None = None, - block: str | None = None, - block_number: int | None = None, - field: str | None = None, - field_number: int | None = None - ): - assert block_number != 0 or field_number != 0 - - self.prefix = prefix - self.block = block - self.block_number = block_number - self.field = field - self.field_number = field_number - - prefix_string = f"{self.prefix}{EPICS_SEPERATOR}" if self.prefix is not None else "" - block_with_number = f"{self.block}{self.block_number or ''}{EPICS_SEPERATOR}" if self.block is not None else "" - field_with_number = f"{self.field}{self.field_number or ''}" if self.field is not None else "" - - super().__init__(f"{prefix_string}{block_with_number}{field_with_number}") - - @classmethod - def from_string(cls, name: str): - """Converts a string to an EPICS name, must contain a prefix.""" - split_name = name.split(EPICS_SEPERATOR) - assert len(split_name) == 3 - prefix, block_with_number, field_with_number = name.split(EPICS_SEPERATOR) - block, block_number = _extract_number_at_of_string(block_with_number) - field, field_number = _extract_number_at_of_string(field_with_number) - return EpicsName( - prefix=prefix, block=block, block_number=block_number, field=field, field_number=field_number - ) - - def to_panda_name(self): - return PandaName.from_string(self._name.replace(EPICS_SEPERATOR, PANDA_SEPERATOR)) - - def to_pvi_name(self): - relevant_section = self._name.split(EPICS_SEPERATOR)[-1] - words = relevant_section.replace("-", "_").split("_") - capitalised_word = "".join(word.capitalize() for word in words) - - # We don't want to allow any non-alphanumeric characters. - formatted_word = re.search(r"[A-Za-z0-9]+", capitalised_word) - assert formatted_word - - return PviName(formatted_word.group()) - - def __add__(self, other: EpicsName): - def _merge_sub_pv( - sub_pv_1: T, sub_pv_2: T - ) -> T: - if sub_pv_1 is not None and sub_pv_2 is not None: - assert sub_pv_1 == sub_pv_2 - return sub_pv_2 or sub_pv_1 - - return EpicsName( - prefix = _merge_sub_pv(self.prefix, other.prefix), - block = _merge_sub_pv(self.block, other.block), - block_number = _merge_sub_pv(self.block_number, other.block_number), - field = _merge_sub_pv(self.field, other.field), - field_number = _merge_sub_pv(self.field_number, other.field_number) - ) - - def __contains__(self, other: EpicsName): - """Checks to see if a given epics name is a subset of another one. - - Examples - -------- - - (EpicsName(block="field1") in EpicsName("prefix:block1:field1")) == True - (EpicsName(block="field1") in EpicsName("prefix:block1:field2")) == False - """ - def _check_eq(sub_pv_1: T, sub_pv_2: T) -> bool: - if sub_pv_1 is not None and sub_pv_2 is not None: - return sub_pv_1 == sub_pv_2 - return True - - return ( - _check_eq(self.prefix, other.prefix) and - _check_eq(self.block, other.block) and - _check_eq(self.block_number, other.block_number) and - _check_eq(self.field, other.field) and - _check_eq(self.field_number, other.field_number) - ) - - - - - -class PviName(_Name): - ... - - -ResponseType = Union[ - BitMuxFieldInfo, - BitOutFieldInfo, - EnumFieldInfo, - ExtOutBitsFieldInfo, - ExtOutFieldInfo, - FieldInfo, - PosMuxFieldInfo, - PosOutFieldInfo, - ScalarFieldInfo, - SubtypeTimeFieldInfo, - TableFieldInfo, - TimeFieldInfo, - UintFieldInfo, -] diff --git a/src/fastcs_pandablocks/types/__init__.py b/src/fastcs_pandablocks/types/__init__.py new file mode 100644 index 0000000..08e3f6f --- /dev/null +++ b/src/fastcs_pandablocks/types/__init__.py @@ -0,0 +1,17 @@ +from .annotations import ResponseType +from .string_types import ( + EPICS_SEPERATOR, + PANDA_SEPERATOR, + EpicsName, + PandaName, + PviName, +) + +__all__ = [ + "EPICS_SEPERATOR", + "EpicsName", + "PANDA_SEPERATOR", + "PandaName", + "PviName", + "ResponseType" +] diff --git a/src/fastcs_pandablocks/types/annotations.py b/src/fastcs_pandablocks/types/annotations.py new file mode 100644 index 0000000..4643a53 --- /dev/null +++ b/src/fastcs_pandablocks/types/annotations.py @@ -0,0 +1,32 @@ + +from pandablocks.responses import ( + BitMuxFieldInfo, + BitOutFieldInfo, + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, +) + +ResponseType = ( + BitMuxFieldInfo | + BitOutFieldInfo | + EnumFieldInfo | + ExtOutBitsFieldInfo | + ExtOutFieldInfo | + FieldInfo | + PosMuxFieldInfo | + PosOutFieldInfo | + ScalarFieldInfo | + SubtypeTimeFieldInfo | + TableFieldInfo | + TimeFieldInfo | + UintFieldInfo +) diff --git a/src/fastcs_pandablocks/types/string_types.py b/src/fastcs_pandablocks/types/string_types.py new file mode 100644 index 0000000..9a00ab0 --- /dev/null +++ b/src/fastcs_pandablocks/types/string_types.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import re +from dataclasses import dataclass +from typing import TypeVar + +T = TypeVar("T") + +EPICS_SEPERATOR = ":" +PANDA_SEPERATOR = "." + +def _extract_number_at_of_string(string: str) -> tuple[str, int | None]: + pattern = r"(\D+)(\d+)$" + match = re.match(pattern, string) + if match: + return (match.group(1), int(match.group(2))) + return string, None + + +def _format_with_seperator( + seperator: str, + *sections: tuple[str | None, int | None] | str | None +) -> str: + result = "" + for section in sections: + if isinstance(section, tuple): + section_string, section_number = section + if section_string is not None: + result += f"{seperator}{section_string}" + if section_number is not None: + result += f"{section_number}" + elif section is not None: + result += f"{seperator}{section}" + + return result.lstrip(seperator) + +@dataclass(frozen=True) +class _Name: + _name: str + + def __str__(self): + return str(self._name) + def __repr__(self): + return str(self) + +class PandaName(_Name): + def __init__( + self, + block: str | None = None, + block_number: int | None = None, + field: str | None = None, + sub_field: str | None = None, + ): + self.block=block + self.block_number=block_number + self.field=field + self.sub_field=sub_field + + super().__init__( + _format_with_seperator( + PANDA_SEPERATOR, + (block, block_number), + field, + sub_field + ) + ) + + @classmethod + def from_string(cls, name: str): + split_name = name.split(PANDA_SEPERATOR) + + block, block_number = ( + _extract_number_at_of_string(split_name[0]) + ) + field = split_name[1] + sub_field = split_name[2] if len(split_name) == 3 else None + + return PandaName( + block=block, block_number=block_number, field=field, sub_field=sub_field + ) + + def to_epics_name(self): + return EpicsName( + block=self.block, + block_number=self.block_number, + field=self.field, + sub_field=self.sub_field + ) + +class EpicsName(_Name): + def __init__( + self, + *, + prefix: str | None = None, + block: str | None = None, + block_number: int | None = None, + field: str | None = None, + sub_field: str | None = None, + ): + assert block_number != 0 + + self.prefix = prefix + self.block = block + self.block_number = block_number + self.field = field + self.sub_field = sub_field + + super().__init__( + _format_with_seperator( + EPICS_SEPERATOR, + prefix, + (block, block_number), + field + ) + ) + + @classmethod + def from_string(cls, name: str) -> EpicsName: + """Converts a string to an EPICS name, must contain a prefix.""" + split_name = name.split(EPICS_SEPERATOR) + if len(split_name) < 3: + raise ValueError( + f"Received a a pv string `{name}` which isn't of the form " + "`PREFIX:BLOCK:FIELD` or `PREFIX:BLOCK:FIELD:SUB_FIELD`." + ) + split_name = name.split(EPICS_SEPERATOR) + prefix, block_with_number, field = split_name[:3] + block, block_number = _extract_number_at_of_string(block_with_number) + sub_field = split_name[3] if len(split_name) == 4 else None + + return EpicsName( + prefix=prefix, + block=block, + block_number=block_number, + field=field, + sub_field=sub_field + ) + + def to_panda_name(self) -> PandaName: + return PandaName( + block=self.block, + block_number=self.block_number, + field=self.field, + sub_field=self.sub_field + ) + + def to_pvi_name(self) -> PviName: + assert self.field + words = self.field.replace("-", "_").split("_") + capitalised_word = "".join(word.capitalize() for word in words) + + # We don't want to allow any non-alphanumeric characters. + formatted_word = re.search(r"[A-Za-z0-9]+", capitalised_word) + assert formatted_word + + return PviName(formatted_word.group()) + + def __add__(self, other: EpicsName) -> EpicsName: + """ + Returns the sum of PVs: + + EpicsName(prefix="PREFIX", block="BLOCK") + EpicsName(field="FIELD") + == EpicsName.from_string("PREFIX:BLOCK:FIELD") + """ + + def _choose_sub_pv( + sub_pv_1: T, sub_pv_2: T + ) -> T: + if sub_pv_1 is not None and sub_pv_2 is not None: + if sub_pv_1 != sub_pv_2: + raise TypeError( + "Ambiguous pv elements on `EpicsName` add " + f"{sub_pv_1} and {sub_pv_2}" + ) + return sub_pv_2 or sub_pv_1 + + return EpicsName( + prefix = _choose_sub_pv(self.prefix, other.prefix), + block = _choose_sub_pv(self.block, other.block), + block_number = _choose_sub_pv(self.block_number, other.block_number), + field = _choose_sub_pv(self.field, other.field), + sub_field=_choose_sub_pv(self.sub_field, other.sub_field) + ) + + def __contains__(self, other: EpicsName) -> bool: + """Checks to see if a given epics name is a subset of another one. + + Examples + -------- + + (EpicsName(block="field1") in EpicsName("prefix:block1:field1")) == True + (EpicsName(block="field1") in EpicsName("prefix:block1:field2")) == False + """ + def _check_eq(sub_pv_1: T, sub_pv_2: T) -> bool: + if sub_pv_1 is not None and sub_pv_2 is not None: + return sub_pv_1 == sub_pv_2 + return True + + return ( + _check_eq(self.prefix, other.prefix) and + _check_eq(self.block, other.block) and + _check_eq(self.block_number, other.block_number) and + _check_eq(self.field, other.field) and + _check_eq(self.sub_field, other.sub_field) + ) + +class PviName(_Name): + pass diff --git a/tests/test_types.py b/tests/test_types.py index 89a1834..087da52 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,29 +1,33 @@ -import pytest from fastcs_pandablocks.types import EpicsName def test_epics_name(): - name1 = EpicsName.from_string("prefix:block1:field1") + name1 = EpicsName.from_string("prefix:block1:field") assert name1.prefix == "prefix" assert name1.block == "block" assert name1.block_number == 1 assert name1.field == "field" - assert name1.field_number == 1 + def test_epics_name_add(): assert ( - (EpicsName.from_string("prefix:block1:field1") + EpicsName.from_string("prefix:block1:field1")) - == EpicsName.from_string("prefix:block1:field1") + EpicsName.from_string("prefix:block1:field") + + EpicsName.from_string("prefix:block1:field") + ) == EpicsName.from_string("prefix:block1:field") + assert EpicsName(block="block") + EpicsName(block_number=1) == EpicsName( + block="block", block_number=1 ) - assert EpicsName(block="block") + EpicsName(block_number=1) == EpicsName(block="block", block_number=1) + def test_malformed_epics_name_add(): pass + def test_epics_name_contains(): - parent_name = EpicsName.from_string("prefix:block1:field1") + parent_name = EpicsName.from_string("prefix:block1:field") assert EpicsName(block="block") in parent_name - assert EpicsName(block="block", field_number=1) in parent_name + assert EpicsName(block="block", field="field") in parent_name + def test_malformed_epics_name_contains(): pass