From 6073cfc2b2f6d5339cc27ea2eb815521c747e826 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 9 Oct 2024 11:50:41 +0100 Subject: [PATCH 01/10] started the structure and async iterator parsing the panda --- .pyproject.toml.swp | Bin 16384 -> 0 bytes pyproject.toml | 10 +- src/fastcs_pandablocks/__main__.py | 47 +++++- src/fastcs_pandablocks/fastcs/__init__.py | 34 +++++ src/fastcs_pandablocks/fastcs/controller.py | 14 ++ src/fastcs_pandablocks/fastcs/gui.py | 4 + src/fastcs_pandablocks/panda/__init__.py | 1 + .../panda/client_wrapper.py | 99 +++++++++++++ src/fastcs_pandablocks/panda/panda.py | 0 src/fastcs_pandablocks/types.py | 134 ++++++++++++++++++ 10 files changed, 336 insertions(+), 7 deletions(-) delete mode 100644 .pyproject.toml.swp create mode 100644 src/fastcs_pandablocks/fastcs/__init__.py create mode 100644 src/fastcs_pandablocks/fastcs/controller.py create mode 100644 src/fastcs_pandablocks/fastcs/gui.py create mode 100644 src/fastcs_pandablocks/panda/__init__.py create mode 100644 src/fastcs_pandablocks/panda/client_wrapper.py create mode 100644 src/fastcs_pandablocks/panda/panda.py create mode 100644 src/fastcs_pandablocks/types.py diff --git a/.pyproject.toml.swp b/.pyproject.toml.swp deleted file mode 100644 index 6a2f34f2479ff514e7dbb0ac979e9ab0da0ec73c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHOJ&Ytr6)p_+9X2)=1Ox`DJR4X#P50~{*4;{b_TJr|S!oYew|AV-dNtH^*UXf+ zyQ{CNdUi+Fi2xzYKLG&&0Ra*ufP}=K35W=Q01FWiK+Fk50FiYO->dGPnfno}B?6FY zq)$CvUG?hK_g=kMQ(ZGy{^ZUzem-7i_&m$luby~(^!!uLJo~~VX72d4^|h7t7wOTz z`;^J@%G!lEv2m(Iu8TAlNn9O0)TCzwSs$lMvsihb5)FbLt$NFqdifiTbDAO z9NAtuEscInCN6e5&mNK&3IYRxfp@~d(`;>Z#ZUF6&hz|(AGrBW8i%@pfxtjuATSUZ z2n+-U0t118z`*}G1Fn3GeFkHAtQp7c=K8J&uJ3N{+pBrtd2<^sfq}q4U?4CM7zhjm z1_A?tfxtjuATSUZ2n_rWG9XgMu0gMeHURMczy1CHkM}e7GvMpM*MQFf3iuST3cURc zW4{5u1r$IAtO0kQX6*OC&w=j%UjW_!#=xh6>%dDu1Uv`admm$e1O5#B7WgIbUEr(0 z6c_6k|UHegceutH3(22CM=f2JSw|*k6D@0&fBaxDDI_81T>c zGWKKO`@pNfW#I2;8G8%(9q9km^FzV-YtA7wISU0u@4!dwkt%HpMK`@32!7{@SqTb6R8Ge$@+`HplAfXyG$Huw8Y8|bP?BaDb* zBxW0^Gy>@indq2~N=ER^s1@zKzl}IihR0~iU2cNeHL@cH1V==iG->M`O%y?|4aGC}f^i?=UA=x&FD|F846HM%k`^$w zZ<8FNSRr!X+xA58UG2cB%n|iS0XY$IhN746I|&O>2kl8##Jw@n>)|2BdbKi$I#@c@ zWtcU7%ECez=KxEss>~EksbYj#gq4*=JOhnb1}gWlYCt`$kyRc(eM`b0&OUL!kFgg0{svtZzISU6*H)allq>m!$srEN)* z3$QH0O7MZgM3>V+<(z)-!7OvM4QM&oey?4QQs@08^E;pP=C>4dqDBuGBbOI9m*bVy zS%q2Ro0X_Cm8COMwk7!#v#u1zO4AlWdFAg?NxA4?&UZZ#L>57aEK*q{$}fQ1j1x6c z2<%_nT#HwhJD4#dtv&Ag_STha+i?yDhht!6CF3?;?(SUP{`mfOhow{ar>d7OjW;85 zFZMDH6LsV2xG%TpJFblwB_4QHhr6M`$6_?5h=+P1tRCoK=CWoS%|v0 z`64aC7g|x!!nz{Q4liz=r{!zigN4sZL^JyRzmDJYuLAV@A3rDk=SR4oH01~MEPMh3 zfq}q4U?4CM7zhjm1_A?tfxtjuATSUZcq9Y$E2ADZ($95h4}jioE-$b6ee~-x$5unV zt4D91X!8Vny#qyCGLL+(+HN=dHv_Ys<%Mm(EA^l6cj=|hzTeKrALwbD8@pk&8x`RP Qf0^OkNBi1f<)iEDUvGg$8vp2", + "h5py", +] dynamic = ["version"] license.file = "LICENSE" readme = "README.md" @@ -37,7 +43,7 @@ dev = [ ] [project.scripts] -fastcs-PandABlocks = "fastcs_pandablocks.__main__:main" +fastcs-pandablocks = "fastcs_pandablocks.__main__:main" [project.urls] GitHub = "https://github.com/PandABlocks-ioc/fastcs-PandABlocks" diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index d6291de..f07eaa8 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -1,23 +1,60 @@ """Interface for ``python -m fastcs_pandablocks``.""" -from argparse import ArgumentParser -from collections.abc import Sequence +import argparse +import logging + +from fastcs_pandablocks.fastcs import ioc from . import __version__ __all__ = ["main"] -def main(args: Sequence[str] | None = None) -> None: +def main(): """Argument parser for the CLI.""" - parser = ArgumentParser() + 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("prefix", type=str, help="The prefix for the IOC.") + parser.add_argument( + "--screens-dir", + type=str, + help=( + "Provide an existing directory to export generated bobfiles to, if no " + "directory is provided then bobfiles will not be generated." + ), + ) + parser.add_argument( + "--clear-bobfiles", + action="store_true", + help="Clear bobfiles from the given `screens-dir` before generating new ones.", + ) parser.add_argument( "-v", "--version", action="version", version=__version__, ) - parser.parse_args(args) + parser.add_argument( + "--log-level", + default="INFO", + choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], + help="Set the logging level.", + ) + + parsed_args = parser.parse_args() + + # Set the logging level + level = getattr(logging, parsed_args.log_level.upper(), None) + logging.basicConfig(format="%(levelname)s:%(message)s", level=level) + + ioc( + parsed_args.host, + parsed_args.prefix, + parsed_args.screens_dir, + parsed_args.clear_bobfiles, + ) if __name__ == "__main__": diff --git a/src/fastcs_pandablocks/fastcs/__init__.py b/src/fastcs_pandablocks/fastcs/__init__.py new file mode 100644 index 0000000..276d44d --- /dev/null +++ b/src/fastcs_pandablocks/fastcs/__init__.py @@ -0,0 +1,34 @@ +"""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 new file mode 100644 index 0000000..bd564b5 --- /dev/null +++ b/src/fastcs_pandablocks/fastcs/controller.py @@ -0,0 +1,14 @@ +# 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/fastcs/gui.py new file mode 100644 index 0000000..7334017 --- /dev/null +++ b/src/fastcs_pandablocks/fastcs/gui.py @@ -0,0 +1,4 @@ +from fastcs.backends.epics.gui import EpicsGUIOptions + +class PandaGUIOptions(EpicsGUIOptions): + ... diff --git a/src/fastcs_pandablocks/panda/__init__.py b/src/fastcs_pandablocks/panda/__init__.py new file mode 100644 index 0000000..f0a9692 --- /dev/null +++ b/src/fastcs_pandablocks/panda/__init__.py @@ -0,0 +1 @@ +"""Contains the logic relevant to the Panda's operation.""" diff --git a/src/fastcs_pandablocks/panda/client_wrapper.py b/src/fastcs_pandablocks/panda/client_wrapper.py new file mode 100644 index 0000000..ae30c76 --- /dev/null +++ b/src/fastcs_pandablocks/panda/client_wrapper.py @@ -0,0 +1,99 @@ +""" +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 pandablocks.asyncio import AsyncioClient +from pandablocks.commands import ( + ChangeGroup, + Changes, + GetBlockInfo, + GetChanges, + GetFieldInfo, +) +from pandablocks.responses import ( + BitMuxFieldInfo, + BitOutFieldInfo, + BlockInfo, + Changes, + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, +) +from typing import Union + +ResponseType = Union[ + BitMuxFieldInfo, + BitOutFieldInfo, + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, +] + +class RawPanda: + _blocks: dict[str, BlockInfo] | None = None + _metadata: tuple[Changes] | None = None + + _responses: list[dict[str, ResponseType]] | None = None + _changes: Changes | None = None + + def __init__(self, host: str): + self._client = AsyncioClient(host) + + async def connect(self): await self._client.connect() + + async def disconnect(self): await self._client.close() + + async def introspect(self): + self._blocks = await self._client.send(GetBlockInfo()) + self._responses = await asyncio.gather( + *[self._client.send(GetFieldInfo(block)) for block in self._blocks], + ) + self._metadata = await self._client.send(GetChanges(ChangeGroup.ALL, True)), + + async def get_changes(self): + self._changes = await self._client.send(GetChanges(ChangeGroup.ALL, False)) + + + async def _sync_with_panda(self): + if not self._client.is_connected(): + await self.connect() + await self.introspect() + + async def _ensure_connected(self): + if not self._blocks: + await self._sync_with_panda() + + async def __aenter__(self): + await self._sync_with_panda() + return self + + async def __aexit__(self, exc_type, exc, tb): + await self._ensure_connected() + await self.disconnect() + + def __aiter__(self): + return self + + async def __anext__(self): + await self._ensure_connected() + return await self.get_changes() + diff --git a/src/fastcs_pandablocks/panda/panda.py b/src/fastcs_pandablocks/panda/panda.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastcs_pandablocks/types.py b/src/fastcs_pandablocks/types.py new file mode 100644 index 0000000..aa39194 --- /dev/null +++ b/src/fastcs_pandablocks/types.py @@ -0,0 +1,134 @@ +from dataclasses import dataclass +import re +from pydantic import BaseModel +from typing import Literal + +from pydantic import BaseModel + + +# Dataclasses for names + +@dataclass +class _Name: + _name: str + + def __str__(self): + return str(self._name) + +class PandaName(_Name): + def to_epics_name(self): + return EpicsName(self._name.replace(".", ":")) + +class EpicsName(_Name): + def to_panda_name(self): + return PandaName(self._name.replace(":", ".")) + + def to_pvi_name(self): + relevant_section = self._name.split(":")[-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()) + +class PviName(_Name): + ... + + + +Field_T = Literal[ + "time", + "bit_out", + "pos_out", + "ext_out", + "bit_mux", + "pos_mux", + "param", + "read", + "write", +] + +FieldSubtype_T = Literal[ + "timestamp", + "samples", + "bits", + "uint", + "int", + "scalar", + "bit", + "action", + "lut", + "enum", + "time", +] + + +class PandaField(BaseModel, frozen=True): + """Validates fields from the client.""" + + field_type: Field_T + field_subtype: FieldSubtype_T | None + + +TIME_FIELDS = { + PandaField(field_type="time", field_subtype=None), +} + +BIT_OUT_FIELDS = { + PandaField(field_type="bit_out", field_subtype=None), +} + +POS_OUT_FIELDS = { + PandaField(field_type="pos_out", field_subtype=None), +} + +EXT_OUT_FIELDS = { + PandaField(field_type="ext_out", field_subtype="timestamp"), + PandaField(field_type="ext_out", field_subtype="samples"), +} + +EXT_OUT_BITS_FIELDS = { + PandaField(field_type="ext_out", field_subtype="bits"), +} + +BIT_MUX_FIELDS = { + PandaField(field_type="bit_mux", field_subtype=None), +} + +POS_MUX_FIELDS = { + PandaField(field_type="pos_mux", field_subtype=None), +} + +UINT_FIELDS = { + PandaField(field_type="param", field_subtype="uint"), + PandaField(field_type="read", field_subtype="uint"), + PandaField(field_type="write", field_subtype="uint"), +} + +INT_FIELDS = { + PandaField(field_type="param", field_subtype="int"), + PandaField(field_type="read", field_subtype="int"), + PandaField(field_type="write", field_subtype="int"), +} + +SCALAR_FIELDS = { + PandaField(field_type="param", field_subtype="scalar"), + PandaField(field_type="read", field_subtype="scalar"), + PandaField(field_type="write", field_subtype="scalar"), +} + +BIT_FIELDS = { + PandaField(field_type="param", field_subtype="bit"), + PandaField(field_type="read", field_subtype="bit"), + PandaField(field_type="write", field_subtype="bit"), +} + +ACTION_FIELDS = { + PandaField(field_type="param", field_subtype="action"), + PandaField(field_type="read", field_subtype="action"), + PandaField(field_type="write", field_subtype="action"), +} + From 07cf0dfb27883151c1192699dd58137ad4ac1d16 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 9 Oct 2024 17:29:27 +0100 Subject: [PATCH 02/10] added types for the field elements Also tried to make an `EpicsString` type capable of being used as a hash. --- src/fastcs_pandablocks/__main__.py | 12 +- src/fastcs_pandablocks/panda/blocks.py | 227 ++++++++++++++++++ .../panda/client_wrapper.py | 32 +-- src/fastcs_pandablocks/panda/panda.py | 51 ++++ src/fastcs_pandablocks/types.py | 194 +++++++-------- 5 files changed, 380 insertions(+), 136 deletions(-) create mode 100644 src/fastcs_pandablocks/panda/blocks.py diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index f07eaa8..e89fe3b 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -2,8 +2,11 @@ import argparse import logging +import asyncio + +#from fastcs_pandablocks.fastcs import ioc +from fastcs_pandablocks.panda.panda import Panda -from fastcs_pandablocks.fastcs import ioc from . import __version__ @@ -49,12 +52,19 @@ 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, parsed_args.clear_bobfiles, ) + """ if __name__ == "__main__": diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py new file mode 100644 index 0000000..c03f4ae --- /dev/null +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -0,0 +1,227 @@ +import itertools +from pprint import pprint +from typing import Type +from fastcs_pandablocks.types import EpicsName, ResponseType + +class Field: + def __init__(self, name: EpicsName, field_info: ResponseType): + self.name = name + self.field_info = field_info + +def change_value(self, new_field_value): + print("setting value", new_field_value) + self.value = new_field_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 BitWriteField(Field): + ... + +class BitReadField(Field): + ... + +class ActionWriteField(Field): + ... + +class ActionReadField(Field): + ... + +class LutParamField(Field): + ... + +class LutWriteField(Field): + ... + +class LutReadField(Field): + ... + +class EnumParamField(Field): + ... + +class EnumWriteField(Field): + ... + +class EnumReadField(Field): + ... + +class TimeSubTypeParamField(Field): + ... + +class TimeSubTypeReadField(Field): + ... + +class TimeSubTypeWriteField(Field): + ... + +FieldType = ( + TableField + | BitParamField + | BitWriteField + | BitReadField + | ActionWriteField + | ActionReadField + | LutParamField + | LutWriteField + | LutReadField + | EnumParamField + | EnumWriteField + | EnumReadField + | TimeSubTypeParamField + | TimeSubTypeReadField + | TimeSubTypeWriteField + | TimeField + | BitOutField + | PosOutField + | ExtOutField + | ExtOutBitsField + | BitMuxField + | PosMuxField + | UintParamField + | UintReadField + | UintWriteField + | IntParamField + | IntReadField + | IntWriteField + | ScalarParamField + | ScalarReadField + | ScalarWriteField +) + +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: + _sub_blocks: dict[int, dict[EpicsName, FieldType]] + + def __init__(self, name: EpicsName, number: int, description: str | None, raw_fields: dict[str, ResponseType]): + self.name = name + self.number = number + self.description = description + self._sub_blocks = {} + + for number in range(1, number + 1): + numbered_block = name + EpicsName(str(number)) + single_block = self._sub_blocks[number] = {} + + for field_suffix, field_info in ( + raw_fields.items() + ): + field_name = ( + numbered_block + EpicsName(field_suffix) + ) + single_block[EpicsName(field_suffix)] = ( + FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( + field_name, field_info + ) + ) + def change_value(self, new_field_value, block_number, field_name): + self._sub_blocks[block_number][field_name].change_value(new_field_value) diff --git a/src/fastcs_pandablocks/panda/client_wrapper.py b/src/fastcs_pandablocks/panda/client_wrapper.py index ae30c76..bf05f26 100644 --- a/src/fastcs_pandablocks/panda/client_wrapper.py +++ b/src/fastcs_pandablocks/panda/client_wrapper.py @@ -14,39 +14,12 @@ GetFieldInfo, ) from pandablocks.responses import ( - BitMuxFieldInfo, - BitOutFieldInfo, BlockInfo, Changes, - EnumFieldInfo, - ExtOutBitsFieldInfo, - ExtOutFieldInfo, - FieldInfo, - PosMuxFieldInfo, - PosOutFieldInfo, - ScalarFieldInfo, - SubtypeTimeFieldInfo, - TableFieldInfo, - TimeFieldInfo, - UintFieldInfo, ) -from typing import Union -ResponseType = Union[ - BitMuxFieldInfo, - BitOutFieldInfo, - EnumFieldInfo, - ExtOutBitsFieldInfo, - ExtOutFieldInfo, - FieldInfo, - PosMuxFieldInfo, - PosOutFieldInfo, - ScalarFieldInfo, - SubtypeTimeFieldInfo, - TableFieldInfo, - TimeFieldInfo, - UintFieldInfo, -] +from fastcs_pandablocks.types import ResponseType + class RawPanda: _blocks: dict[str, BlockInfo] | None = None @@ -96,4 +69,3 @@ def __aiter__(self): async def __anext__(self): await self._ensure_connected() return await self.get_changes() - diff --git a/src/fastcs_pandablocks/panda/panda.py b/src/fastcs_pandablocks/panda/panda.py index e69de29..d7b4222 100644 --- a/src/fastcs_pandablocks/panda/panda.py +++ b/src/fastcs_pandablocks/panda/panda.py @@ -0,0 +1,51 @@ +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 + + + +class Panda: + _raw_panda: RawPanda + _blocks: dict[EpicsName, Block] + + def __init__(self, host: str): + self._raw_panda = RawPanda(host) + self._blocks = {} + + async def connect(self): + await self._raw_panda._sync_with_panda() + self._parse_introspected_data() + + def _parse_introspected_data(self): + self._blocks = {} + if ( + self._raw_panda._blocks is None or self._raw_panda._responses is None + ): + raise ValueError("Panda not introspected.") + + for (block_name, block_info), raw_fields in zip( + self._raw_panda._blocks.items(), self._raw_panda._responses + ): + self._blocks[EpicsName(block=block_name)] = Block( + EpicsName(block_name), + block_info.number, + block_info.description, + raw_fields + ) + + + def _parse_values(self, changes: Changes): + for panda_name, field_value in changes.values.items(): + epics_name = PandaName(panda_name).to_epics_name() + self._blocks[epics_name.block].change_value( + field_value, epics_name.block_number, epics_name.field + ) + + + + + async def disconnect(self): await self._raw_panda.disconnect() diff --git a/src/fastcs_pandablocks/types.py b/src/fastcs_pandablocks/types.py index aa39194..de2c67b 100644 --- a/src/fastcs_pandablocks/types.py +++ b/src/fastcs_pandablocks/types.py @@ -1,30 +1,87 @@ +from __future__ import annotations + from dataclasses import dataclass import re -from pydantic import BaseModel -from typing import Literal - -from pydantic import BaseModel - - -# Dataclasses for names - -@dataclass +from pandablocks.responses import ( + BitMuxFieldInfo, + BitOutFieldInfo, + BlockInfo, + Changes, + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, +) +from typing import Union + + +EPICS_SEPERATOR = ":" +PANDA_SEPERATOR = "." + +def _extract_number_at_of_string(string: str) -> tuple[str, int | None]: + pattern = r"(\D+)(\d+)$" + print("===================================================") + print(string) + 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): def to_epics_name(self): - return EpicsName(self._name.replace(".", ":")) + return EpicsName(self._name.replace(PANDA_SEPERATOR, EPICS_SEPERATOR)) class EpicsName(_Name): + block: str | None = None + block_number: int | None = None + field: str | None = None + field_number: int | None = None + prefix: str | None = None + + def __init__( + self, + prefix=None, + block: str | None = None, + block_number: int | None = None, + field: str | None = None, + field_number: int | None = None + ): + 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_number_string = f"{self.block_number}" if self.block_number is not None else "" + block_with_number = f"{self.block}{block_number_string}{EPICS_SEPERATOR}" if self.block is not None else "" + field_number_string = f"{self.field_number}" if self.field_number is not None else "" + field_with_number = f"{self.field}{field_number_string}" if self.field is not None else "" + + super().__init__(f"{prefix_string}{block_with_number}{field_with_number}") + def to_panda_name(self): - return PandaName(self._name.replace(":", ".")) + return PandaName(self._name.replace(EPICS_SEPERATOR, PANDA_SEPERATOR)) def to_pvi_name(self): - relevant_section = self._name.split(":")[-1] + relevant_section = self._name.split(EPICS_SEPERATOR)[-1] words = relevant_section.replace("-", "_").split("_") capitalised_word = "".join(word.capitalize() for word in words) @@ -33,102 +90,29 @@ def to_pvi_name(self): assert formatted_word return PviName(formatted_word.group()) - -class PviName(_Name): - ... - - - -Field_T = Literal[ - "time", - "bit_out", - "pos_out", - "ext_out", - "bit_mux", - "pos_mux", - "param", - "read", - "write", -] - -FieldSubtype_T = Literal[ - "timestamp", - "samples", - "bits", - "uint", - "int", - "scalar", - "bit", - "action", - "lut", - "enum", - "time", -] + + def __add__(self, suffix: "EpicsName"): + return EpicsName(f"{str(self)}{EPICS_SEPERATOR}{str(suffix)}") -class PandaField(BaseModel, frozen=True): - """Validates fields from the client.""" - field_type: Field_T - field_subtype: FieldSubtype_T | None +class PviName(_Name): + ... -TIME_FIELDS = { - PandaField(field_type="time", field_subtype=None), -} - -BIT_OUT_FIELDS = { - PandaField(field_type="bit_out", field_subtype=None), -} - -POS_OUT_FIELDS = { - PandaField(field_type="pos_out", field_subtype=None), -} - -EXT_OUT_FIELDS = { - PandaField(field_type="ext_out", field_subtype="timestamp"), - PandaField(field_type="ext_out", field_subtype="samples"), -} - -EXT_OUT_BITS_FIELDS = { - PandaField(field_type="ext_out", field_subtype="bits"), -} - -BIT_MUX_FIELDS = { - PandaField(field_type="bit_mux", field_subtype=None), -} - -POS_MUX_FIELDS = { - PandaField(field_type="pos_mux", field_subtype=None), -} - -UINT_FIELDS = { - PandaField(field_type="param", field_subtype="uint"), - PandaField(field_type="read", field_subtype="uint"), - PandaField(field_type="write", field_subtype="uint"), -} - -INT_FIELDS = { - PandaField(field_type="param", field_subtype="int"), - PandaField(field_type="read", field_subtype="int"), - PandaField(field_type="write", field_subtype="int"), -} - -SCALAR_FIELDS = { - PandaField(field_type="param", field_subtype="scalar"), - PandaField(field_type="read", field_subtype="scalar"), - PandaField(field_type="write", field_subtype="scalar"), -} - -BIT_FIELDS = { - PandaField(field_type="param", field_subtype="bit"), - PandaField(field_type="read", field_subtype="bit"), - PandaField(field_type="write", field_subtype="bit"), -} - -ACTION_FIELDS = { - PandaField(field_type="param", field_subtype="action"), - PandaField(field_type="read", field_subtype="action"), - PandaField(field_type="write", field_subtype="action"), -} +ResponseType = Union[ + BitMuxFieldInfo, + BitOutFieldInfo, + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, +] From 9ef976c12bac45292fabec7c716f76992097541f Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 10 Oct 2024 13:48:34 +0100 Subject: [PATCH 03/10] playing around with the datastructures --- src/fastcs_pandablocks/panda/blocks.py | 204 +++++++++--------- .../panda/client_wrapper.py | 63 +++--- src/fastcs_pandablocks/panda/panda.py | 44 ++-- src/fastcs_pandablocks/types.py | 106 +++++++-- tests/test_types.py | 29 +++ 5 files changed, 298 insertions(+), 148 deletions(-) create mode 100644 tests/test_types.py diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py index c03f4ae..afbb11b 100644 --- a/src/fastcs_pandablocks/panda/blocks.py +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -1,142 +1,146 @@ import itertools from pprint import pprint from typing import Type -from fastcs_pandablocks.types import EpicsName, ResponseType +from fastcs_pandablocks.types import EpicsName, PandaName, ResponseType + +panda_name_to_field = {} class Field: - def __init__(self, name: EpicsName, field_info: ResponseType): - self.name = name - self.field_info = field_info + 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 change_value(self, new_field_value): - print("setting value", new_field_value) - self.value = new_field_value + 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 BitWriteField(Field): - ... + ... class BitReadField(Field): - ... + ... -class ActionWriteField(Field): - ... +class BitWriteField(Field): + ... class ActionReadField(Field): - ... + ... -class LutParamField(Field): - ... +class ActionWriteField(Field): + ... -class LutWriteField(Field): - ... +class LutParamField(Field): + ... class LutReadField(Field): - ... + ... -class EnumParamField(Field): - ... +class LutWriteField(Field): + ... -class EnumWriteField(Field): - ... +class EnumParamField(Field): + ... class EnumReadField(Field): - ... + ... -class TimeSubTypeParamField(Field): - ... +class EnumWriteField(Field): + ... -class TimeSubTypeReadField(Field): - ... +class TimeSubTypeParamField(TimeField): + ... -class TimeSubTypeWriteField(Field): - ... +class TimeSubTypeReadField(TimeField): + ... + +class TimeSubTypeWriteField(TimeField): + ... FieldType = ( - TableField - | BitParamField - | BitWriteField - | BitReadField - | ActionWriteField - | ActionReadField - | LutParamField - | LutWriteField - | LutReadField - | EnumParamField - | EnumWriteField - | EnumReadField - | TimeSubTypeParamField - | TimeSubTypeReadField - | TimeSubTypeWriteField - | TimeField - | BitOutField - | PosOutField - | ExtOutField - | ExtOutBitsField - | BitMuxField - | PosMuxField - | UintParamField - | UintReadField - | UintWriteField - | IntParamField - | IntReadField - | IntWriteField - | ScalarParamField - | ScalarReadField - | ScalarWriteField + 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]]] = { @@ -198,30 +202,38 @@ class TimeSubTypeWriteField(Field): }, } - class Block: - _sub_blocks: dict[int, dict[EpicsName, FieldType]] - - def __init__(self, name: EpicsName, number: int, description: str | None, raw_fields: dict[str, ResponseType]): - self.name = name + _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._sub_blocks = {} + self._fields = {} for number in range(1, number + 1): - numbered_block = name + EpicsName(str(number)) - single_block = self._sub_blocks[number] = {} + numbered_block_name = epics_name + EpicsName(block_number=number) + self._fields[number] = {} - for field_suffix, field_info in ( + for field_raw_name, field_info in ( raw_fields.items() ): - field_name = ( - numbered_block + EpicsName(field_suffix) + 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 ) - single_block[EpicsName(field_suffix)] = ( - FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( - field_name, field_info - ) + 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 ) - def change_value(self, new_field_value, block_number, field_name): - self._sub_blocks[block_number][field_name].change_value(new_field_value) + 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/client_wrapper.py b/src/fastcs_pandablocks/panda/client_wrapper.py index bf05f26..4eb3a50 100644 --- a/src/fastcs_pandablocks/panda/client_wrapper.py +++ b/src/fastcs_pandablocks/panda/client_wrapper.py @@ -5,6 +5,9 @@ """ import asyncio +from dataclasses import dataclass +from pprint import pprint +from typing import TypedDict from pandablocks.asyncio import AsyncioClient from pandablocks.commands import ( ChangeGroup, @@ -18,54 +21,60 @@ Changes, ) -from fastcs_pandablocks.types import ResponseType - +from fastcs_pandablocks.types import PandaName, ResponseType class RawPanda: - _blocks: dict[str, BlockInfo] | None = None - _metadata: tuple[Changes] | None = None - - _responses: list[dict[str, ResponseType]] | None = None - _changes: Changes | None = None + blocks: dict[str, BlockInfo] | None = None + fields: list[dict[str, ResponseType]] | None = None + metadata: dict[str, str] | None = None + changes: dict[str, str] | None = None def __init__(self, host: str): self._client = AsyncioClient(host) - async def connect(self): await self._client.connect() - - async def disconnect(self): await self._client.close() + async def connect(self): + await self._client.connect() + await self.introspect() + async def disconnect(self): + await self._client.close() + self.blocks = None + self.fields = None + self.metadata = None + self.changes = None + async def introspect(self): - self._blocks = await self._client.send(GetBlockInfo()) - self._responses = await asyncio.gather( - *[self._client.send(GetFieldInfo(block)) for block in self._blocks], + 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], ) - self._metadata = await self._client.send(GetChanges(ChangeGroup.ALL, True)), - + initial_values = (await self._client.send(GetChanges(ChangeGroup.ALL, True))).values + + for field_name, value in initial_values.items(): + if field_name.startswith("*METADATA"): + self.metadata[field_name] = value + else: + self.changes[field_name] = value + async def get_changes(self): - self._changes = await self._client.send(GetChanges(ChangeGroup.ALL, False)) + if not self.changes: + raise RuntimeError("Panda not introspected.") + self.changes = (await self._client.send(GetChanges(ChangeGroup.ALL, False))).values - - async def _sync_with_panda(self): - if not self._client.is_connected(): - await self.connect() - await self.introspect() - async def _ensure_connected(self): - if not self._blocks: - await self._sync_with_panda() + if not self.blocks: + await self.connect() async def __aenter__(self): - await self._sync_with_panda() + await self._ensure_connected() return self async def __aexit__(self, exc_type, exc, tb): - await self._ensure_connected() await self.disconnect() def __aiter__(self): return self async def __anext__(self): - await self._ensure_connected() return await self.get_changes() diff --git a/src/fastcs_pandablocks/panda/panda.py b/src/fastcs_pandablocks/panda/panda.py index d7b4222..322b760 100644 --- a/src/fastcs_pandablocks/panda/panda.py +++ b/src/fastcs_pandablocks/panda/panda.py @@ -1,3 +1,4 @@ +import asyncio from pprint import pprint from typing import Callable from dataclasses import dataclass @@ -5,47 +6,66 @@ 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): - await self._raw_panda._sync_with_panda() + 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._responses is None + 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._responses + self._raw_panda.blocks.items(), self._raw_panda.fields ): self._blocks[EpicsName(block=block_name)] = Block( - EpicsName(block_name), + EpicsName(block=block_name), block_info.number, block_info.description, raw_fields ) + self._parse_changes() - - def _parse_values(self, changes: Changes): - for panda_name, field_value in changes.values.items(): - epics_name = PandaName(panda_name).to_epics_name() - self._blocks[epics_name.block].change_value( - field_value, epics_name.block_number, epics_name.field - ) + 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 index de2c67b..8a85325 100644 --- a/src/fastcs_pandablocks/types.py +++ b/src/fastcs_pandablocks/types.py @@ -1,4 +1,5 @@ from __future__ import annotations +from fastcs.attributes import AttrR from dataclasses import dataclass import re @@ -19,16 +20,15 @@ TimeFieldInfo, UintFieldInfo, ) -from typing import Union +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+)$" - print("===================================================") - print(string) match = re.match(pattern, string) if match: return (match.group(1), int(match.group(2))) @@ -45,24 +45,58 @@ 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): - return EpicsName(self._name.replace(PANDA_SEPERATOR, EPICS_SEPERATOR)) + 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 - prefix: str | None = None def __init__( self, - prefix=None, + *, + 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 @@ -70,15 +104,25 @@ def __init__( self.field_number = field_number prefix_string = f"{self.prefix}{EPICS_SEPERATOR}" if self.prefix is not None else "" - block_number_string = f"{self.block_number}" if self.block_number is not None else "" - block_with_number = f"{self.block}{block_number_string}{EPICS_SEPERATOR}" if self.block is not None else "" - field_number_string = f"{self.field_number}" if self.field_number is not None else "" - field_with_number = f"{self.field}{field_number_string}" if self.field 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(self._name.replace(EPICS_SEPERATOR, PANDA_SEPERATOR)) + return PandaName.from_string(self._name.replace(EPICS_SEPERATOR, PANDA_SEPERATOR)) def to_pvi_name(self): relevant_section = self._name.split(EPICS_SEPERATOR)[-1] @@ -91,8 +135,44 @@ def to_pvi_name(self): return PviName(formatted_word.group()) - def __add__(self, suffix: "EpicsName"): - return EpicsName(f"{str(self)}{EPICS_SEPERATOR}{str(suffix)}") + 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) + ) + diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 0000000..89a1834 --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,29 @@ +import pytest +from fastcs_pandablocks.types import EpicsName + + +def test_epics_name(): + name1 = EpicsName.from_string("prefix:block1:field1") + 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") + ) + 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") + assert EpicsName(block="block") in parent_name + assert EpicsName(block="block", field_number=1) in parent_name + +def test_malformed_epics_name_contains(): + pass From dd73c61fb38069cfacde06dc4656300108efa5cd Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 14 Oct 2024 17:08:09 +0100 Subject: [PATCH 04/10] implemented panda controller --- src/fastcs_pandablocks/__init__.py | 44 +- src/fastcs_pandablocks/__main__.py | 48 +- src/fastcs_pandablocks/block.py | 412 ++++++++++++++++++ src/fastcs_pandablocks/blocks.py | 34 ++ .../{panda => }/client_wrapper.py | 33 +- src/fastcs_pandablocks/controller.py | 50 +++ src/fastcs_pandablocks/fastcs/__init__.py | 34 -- src/fastcs_pandablocks/fastcs/controller.py | 14 - src/fastcs_pandablocks/{fastcs => }/gui.py | 4 +- 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 | 31 ++ src/fastcs_pandablocks/types/string_types.py | 204 +++++++++ tests/test_cli.py | 2 +- tests/test_types.py | 20 +- 19 files changed, 859 insertions(+), 612 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 (53%) 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..ca1da80 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,9 +17,20 @@ 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("prefix", type=str, help="The prefix for the IOC.") parser.add_argument( + "-v", + "--version", + action="version", + version=__version__, + ) + + subparsers = parser.add_subparsers(dest="command", required=True) + run_parser = subparsers.add_parser( + "run", help="Run the IOC with the given HOST and PREFIX." + ) + run_parser.add_argument("hostname", type=str, help="The host to connect to.") + run_parser.add_argument("prefix", type=str, help="The prefix for the IOC.") + run_parser.add_argument( "--screens-dir", type=str, help=( @@ -28,18 +38,13 @@ def main(): "directory is provided then bobfiles will not be generated." ), ) - parser.add_argument( + run_parser.add_argument( "--clear-bobfiles", action="store_true", help="Clear bobfiles from the given `screens-dir` before generating new ones.", ) - parser.add_argument( - "-v", - "--version", - action="version", - version=__version__, - ) - parser.add_argument( + + run_parser.add_argument( "--log-level", default="INFO", choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], @@ -47,24 +52,19 @@ def main(): ) parsed_args = parser.parse_args() + if parsed_args.command != "run": + return # Set the logging level 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..10113b1 --- /dev/null +++ b/src/fastcs_pandablocks/block.py @@ -0,0 +1,412 @@ +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..c2b083b --- /dev/null +++ b/src/fastcs_pandablocks/blocks.py @@ -0,0 +1,34 @@ +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..e529c20 --- /dev/null +++ b/src/fastcs_pandablocks/controller.py @@ -0,0 +1,50 @@ +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 53% rename from src/fastcs_pandablocks/fastcs/gui.py rename to src/fastcs_pandablocks/gui.py index 7334017..c4189f0 100644 --- a/src/fastcs_pandablocks/fastcs/gui.py +++ b/src/fastcs_pandablocks/gui.py @@ -1,4 +1,4 @@ from fastcs.backends.epics.gui import EpicsGUIOptions -class PandaGUIOptions(EpicsGUIOptions): - ... + +class PandaGUIOptions(EpicsGUIOptions): ... diff --git a/src/fastcs_pandablocks/handler.py b/src/fastcs_pandablocks/handler.py new file mode 100644 index 0000000..5f6fba9 --- /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..ae2c343 --- /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..1f2b5c2 --- /dev/null +++ b/src/fastcs_pandablocks/types/annotations.py @@ -0,0 +1,31 @@ +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..96de1e4 --- /dev/null +++ b/src/fastcs_pandablocks/types/string_types.py @@ -0,0 +1,204 @@ +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_cli.py b/tests/test_cli.py index c5072bd..381237b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,4 +6,4 @@ def test_cli_version(): cmd = [sys.executable, "-m", "fastcs_pandablocks", "--version"] - assert subprocess.check_output(cmd).decode().strip() == __version__ + assert __version__ in subprocess.check_output(cmd).decode().strip() 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 From 648fc0ebe2a7a2f4175d559225b53fded95d420a Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 15 Oct 2024 08:19:10 +0100 Subject: [PATCH 05/10] bumped minimum python to 3.11 --- .github/workflows/ci.yml | 2 +- docs/tutorials/installation.md | 2 +- pyproject.toml | 3 +-- src/fastcs_pandablocks/blocks.py | 4 +--- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 485b82b..9be1759 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.11", "3.12"] include: # Include one that runs in the dev environment - runs-on: "ubuntu-latest" diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md index 5100480..dc834af 100644 --- a/docs/tutorials/installation.md +++ b/docs/tutorials/installation.md @@ -2,7 +2,7 @@ ## Check your version of python -You will need python 3.10 or later. You can check your version of python by +You will need python 3.11 or later. You can check your version of python by typing into a terminal: ``` diff --git a/pyproject.toml b/pyproject.toml index c812256..49e6c8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ name = "fastcs-pandablocks" classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] @@ -22,7 +21,7 @@ dependencies = [ dynamic = ["version"] license.file = "LICENSE" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" [project.optional-dependencies] dev = [ diff --git a/src/fastcs_pandablocks/blocks.py b/src/fastcs_pandablocks/blocks.py index c2b083b..58f5ed9 100644 --- a/src/fastcs_pandablocks/blocks.py +++ b/src/fastcs_pandablocks/blocks.py @@ -1,9 +1,7 @@ from pandablocks.responses import BlockInfo -from fastcs_pandablocks.types.string_types import EpicsName, PandaName - from .block import Block -from .types import ResponseType +from .types import ResponseType, EpicsName, PandaName class Blocks: From 02dafb40fbb6a1f0dbf5539ab931ae06f7e1252e Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 16 Oct 2024 08:27:01 +0100 Subject: [PATCH 06/10] began writing fields --- src/fastcs_pandablocks/__init__.py | 12 +- src/fastcs_pandablocks/__main__.py | 18 +- src/fastcs_pandablocks/block.py | 412 ------------------ src/fastcs_pandablocks/blocks.py | 32 -- src/fastcs_pandablocks/controller.py | 50 --- src/fastcs_pandablocks/gui.py | 3 +- src/fastcs_pandablocks/handler.py | 6 +- src/fastcs_pandablocks/panda/__init__.py | 0 src/fastcs_pandablocks/panda/blocks.py | 99 +++++ .../{ => panda}/client_wrapper.py | 0 src/fastcs_pandablocks/panda/controller.py | 46 ++ src/fastcs_pandablocks/panda/fields.py | 392 +++++++++++++++++ src/fastcs_pandablocks/types/__init__.py | 2 - src/fastcs_pandablocks/types/string_types.py | 121 +++-- tests/test_types.py | 59 ++- 15 files changed, 664 insertions(+), 588 deletions(-) delete mode 100644 src/fastcs_pandablocks/block.py delete mode 100644 src/fastcs_pandablocks/blocks.py delete mode 100644 src/fastcs_pandablocks/controller.py create mode 100644 src/fastcs_pandablocks/panda/__init__.py create mode 100644 src/fastcs_pandablocks/panda/blocks.py rename src/fastcs_pandablocks/{ => panda}/client_wrapper.py (100%) create mode 100644 src/fastcs_pandablocks/panda/controller.py create mode 100644 src/fastcs_pandablocks/panda/fields.py diff --git a/src/fastcs_pandablocks/__init__.py b/src/fastcs_pandablocks/__init__.py index 49fa8fb..2277088 100644 --- a/src/fastcs_pandablocks/__init__.py +++ b/src/fastcs_pandablocks/__init__.py @@ -6,20 +6,21 @@ from fastcs.backends.epics.gui import EpicsGUIFormat from ._version import __version__ -from .controller import PandaController from .gui import PandaGUIOptions +from .panda.controller import PandaController from .types import EpicsName -__all__ = ["__version__"] +DEFAULT_POLL_PERIOD = 0.1 def ioc( prefix: EpicsName, hostname: str, - screens_directory: Path | None, + screens_directory: Path | None = None, + poll_period: float = DEFAULT_POLL_PERIOD, clear_bobfiles: bool = False, ): - controller = PandaController(prefix, hostname) + controller = PandaController(hostname, poll_period) backend = EpicsBackend(controller, pv_prefix=str(prefix)) if clear_bobfiles and not screens_directory: @@ -43,3 +44,6 @@ def ioc( ) backend.run() + + +__all__ = ["__version__", "ioc", "DEFAULT_POLL_PERIOD"] diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index ca1da80..bf0f8e5 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from fastcs_pandablocks import ioc +from fastcs_pandablocks import DEFAULT_POLL_PERIOD, ioc from fastcs_pandablocks.types import EpicsName from . import __version__ @@ -41,7 +41,10 @@ def main(): run_parser.add_argument( "--clear-bobfiles", action="store_true", - help="Clear bobfiles from the given `screens-dir` before generating new ones.", + help=( + "Overwrite existing bobfiles from the given `screens-dir` " + "before generating new ones." + ), ) run_parser.add_argument( @@ -50,6 +53,12 @@ def main(): choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], help="Set the logging level.", ) + run_parser.add_argument( + "--poll-period", + default=DEFAULT_POLL_PERIOD, + type=float, + help="Period to poll", + ) parsed_args = parser.parse_args() if parsed_args.command != "run": @@ -62,8 +71,9 @@ def main(): ioc( EpicsName(prefix=parsed_args.prefix), parsed_args.hostname, - Path(parsed_args.screens_dir), - parsed_args.clear_bobfiles, + screens_directory=Path(parsed_args.screens_dir), + poll_period=parsed_args.poll_period, + clear_bobfiles=parsed_args.clear_bobfiles, ) diff --git a/src/fastcs_pandablocks/block.py b/src/fastcs_pandablocks/block.py deleted file mode 100644 index 10113b1..0000000 --- a/src/fastcs_pandablocks/block.py +++ /dev/null @@ -1,412 +0,0 @@ -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 deleted file mode 100644 index 58f5ed9..0000000 --- a/src/fastcs_pandablocks/blocks.py +++ /dev/null @@ -1,32 +0,0 @@ -from pandablocks.responses import BlockInfo - -from .block import Block -from .types import ResponseType, EpicsName, PandaName - - -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/controller.py b/src/fastcs_pandablocks/controller.py deleted file mode 100644 index e529c20..0000000 --- a/src/fastcs_pandablocks/controller.py +++ /dev/null @@ -1,50 +0,0 @@ -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/gui.py b/src/fastcs_pandablocks/gui.py index c4189f0..4285c76 100644 --- a/src/fastcs_pandablocks/gui.py +++ b/src/fastcs_pandablocks/gui.py @@ -1,4 +1,5 @@ from fastcs.backends.epics.gui import EpicsGUIOptions -class PandaGUIOptions(EpicsGUIOptions): ... +class PandaGUIOptions(EpicsGUIOptions): + ... diff --git a/src/fastcs_pandablocks/handler.py b/src/fastcs_pandablocks/handler.py index 5f6fba9..a2f686a 100644 --- a/src/fastcs_pandablocks/handler.py +++ b/src/fastcs_pandablocks/handler.py @@ -2,12 +2,10 @@ from fastcs.attributes import AttrW, Sender -from fastcs_pandablocks.types.string_types import PandaName +from fastcs_pandablocks.types import PandaName -# from fastcs_pandablocks.controller import PandaController - -class FieldSender(Sender): +class DefaultFieldSender(Sender): def __init__(self, panda_name: PandaName): self.panda_name = panda_name diff --git a/src/fastcs_pandablocks/panda/__init__.py b/src/fastcs_pandablocks/panda/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py new file mode 100644 index 0000000..19dc7df --- /dev/null +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -0,0 +1,99 @@ +from collections.abc import Generator + +from fastcs.controller import SubController +from pandablocks.responses import BlockInfo + +from fastcs_pandablocks.types import EpicsName, PandaName, ResponseType + +from .fields import FIELD_TYPE_TO_FASTCS_TYPE, FieldType + + +class Block(SubController): + fields: dict[str, FieldType] + + def __init__( + self, + panda_name: PandaName, + number: int | None, + description: str | None | None, + raw_fields: dict[str, ResponseType], + ): + super().__init__() + self.panda_name = panda_name + self.number = number + self.description = description + self.fields = {} + + for field_raw_name, field_info in raw_fields.items(): + field_panda_name = PandaName(field=field_raw_name) + print(field_raw_name) + + field = FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( + field_panda_name, field_info.description + ) + self.fields[field_raw_name] = field + self.register_sub_controller(field_panda_name.attribute_name, field) + +class Blocks: + _blocks: dict[str, dict[int | None, Block]] + epics_prefix: EpicsName + + def __init__(self): + 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 + ): + iterator = ( + range(1, block_info.number + 1) + if block_info.number > 1 else iter([None,]) + ) + self._blocks[block_name] = { + number: + Block( + PandaName(block=block_name, block_number=number), + block_info.number, + block_info.description, + raw_fields, + ) + for number in iterator + } + + async def update_field_value(self, panda_name: PandaName, value: str): + assert panda_name.block + assert panda_name.field + field = ( + self._blocks[panda_name.block][panda_name.block_number].fields[panda_name.field] + ) + if panda_name.sub_field: + field = field.sub_fields[panda_name.sub_field] + await field.update_value(value) + + def flattened_attribute_tree( + self + ) -> Generator[tuple[str, Block], None, None]: + for blocks in self._blocks.values(): + for block in blocks.values(): + yield (block.panda_name.attribute_name, block) + + def __getitem__( + self, + name: EpicsName | PandaName + ) -> dict[int | None, Block] | Block | FieldType: + if name.block is None: + raise ValueError(f"Cannot find block for name {name}.") + blocks = self._blocks[name.block] + if name.block_number is None: + return blocks + block = blocks[name.block_number] + if name.field is None: + return block + field = block.fields[name.field] + if name.sub_field is None: + return field + return field.sub_fields[name.sub_field] diff --git a/src/fastcs_pandablocks/client_wrapper.py b/src/fastcs_pandablocks/panda/client_wrapper.py similarity index 100% rename from src/fastcs_pandablocks/client_wrapper.py rename to src/fastcs_pandablocks/panda/client_wrapper.py diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py new file mode 100644 index 0000000..154b741 --- /dev/null +++ b/src/fastcs_pandablocks/panda/controller.py @@ -0,0 +1,46 @@ +import asyncio + +from fastcs.controller import Controller +from fastcs.wrappers import scan + +from fastcs_pandablocks import DEFAULT_POLL_PERIOD +from fastcs_pandablocks.types import PandaName + +from .blocks import Blocks +from .client_wrapper import RawPanda + + +class PandaController(Controller): + def __init__(self, hostname: str, poll_period: float) -> None: + super().__init__() + self._raw_panda = RawPanda(hostname) + self._blocks = Blocks() + + # TODO https://github.com/DiamondLightSource/FastCS/issues/62 + #self.fastcs_method = Scan(self.update(), poll_period) + + async def initialise(self) -> None: ... + + async def connect(self) -> 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 + ) + for attr_name, controller in self._blocks.flattened_attribute_tree(): + self.register_sub_controller(attr_name, controller) + + @scan(DEFAULT_POLL_PERIOD) # TODO https://github.com/DiamondLightSource/FastCS/issues/62 + async def update(self): + await self._raw_panda.get_changes() + assert self._raw_panda.changes + await asyncio.gather( + *[ + self._blocks.update_field_value( + PandaName.from_string(raw_panda_name), value + ) + for raw_panda_name, value in self._raw_panda.changes.items() + ] + ) diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py new file mode 100644 index 0000000..6bd79c3 --- /dev/null +++ b/src/fastcs_pandablocks/panda/fields.py @@ -0,0 +1,392 @@ +from __future__ import annotations + +from typing import Literal + +from fastcs.attributes import AttrR, AttrRW, AttrW +from fastcs.controller import SubController +from fastcs.datatypes import Bool, Float, Int, String + +from fastcs_pandablocks.types import PandaName + + +class PviGroup: + """Purposely not an enum since we only ever want the string.""" + PARAMETERS = "Parameters" + OUTPUTS = "Outputs" + INPUTS = "Inputs" + READBACKS = "Readbacks" + +PviGroupField = Literal["Parameters", "Outputs", "Inputs", "Readbacks"] + + +class Field(SubController): + def __init__( + self, + attribute_name: str | None, + attribute: AttrRW | AttrR | AttrW | None, + sub_fields: dict[str, FieldType] | None = None, + ): + """ + For controlling the field, sub fields can also be added. + attribute_name and attribute are optional since some fields + e.g won't contain a top level record, but only sub fields. + """ + super().__init__() + self.sub_fields = sub_fields or {} + self.attribute_name = attribute_name + + if attribute_name and attribute: + setattr(self, attribute_name, attribute) + + for sub_field_name, sub_field in self.sub_fields.items(): + self.register_sub_controller( + PandaName(sub_field=sub_field_name).attribute_name, + sub_field + ) + + async def update_value(self, value: str): + if self.attribute_name is None: + return + + attribute = getattr(self, self.attribute_name) + if isinstance(attribute, AttrW): + await attribute.process(value) + else: + await attribute.set(value) + + +class TableField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + +class TimeField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + + time_attr = AttrR(Float(), group=PviGroup.PARAMETERS) + # TODO: Find out how to add EGU and such + super().__init__(panda_name.attribute_name, time_attr) + + +class BitOutField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + bit_out_attr = AttrRW(Bool(znam="0", onam="1"), group=PviGroup.OUTPUTS) + super().__init__(panda_name.attribute_name, bit_out_attr) + + +class PosOutField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + # TODO add capture and dataset subfields + pos_out_attr = AttrR(Float(), group=PviGroup.OUTPUTS) + super().__init__(panda_name.attribute_name, pos_out_attr) + + +class ExtOutField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + # TODO add capture and dataset subfields + super().__init__(None, None) + + +class ExtOutBitsField(ExtOutField): + def __init__( + self, panda_name: PandaName, description: str | None + ): + # TODO add capture and dataset subfields + super().__init__(panda_name, description) + + +class BitMuxField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + bit_mux_attr = AttrRW(String(), group=PviGroup.INPUTS) + super().__init__(panda_name.attribute_name, bit_mux_attr) + + +class PosMuxField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + pos_mux_attr = AttrRW(String(), group=PviGroup.INPUTS) + super().__init__(panda_name.attribute_name, pos_mux_attr) + +class UintParamField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + uint_param_attr = AttrR(Float(prec=0), group=PviGroup.PARAMETERS) + super().__init__(panda_name.attribute_name, uint_param_attr) + +class UintReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + uint_read_attr = AttrR(Float(prec=0), group=PviGroup.READBACKS) + super().__init__(panda_name.attribute_name, uint_read_attr) + + +class UintWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + uint_write_attr = AttrW(Float(prec=0), group=PviGroup.OUTPUTS) + super().__init__(panda_name.attribute_name, uint_write_attr) + + +class IntParamField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + uint_param_attr = AttrRW(Float(prec=0), group=PviGroup.PARAMETERS) + super().__init__(panda_name.attribute_name, uint_param_attr) + + +class IntReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + int_read_attr = AttrR(Int(), group=PviGroup.READBACKS) + super().__init__(panda_name.attribute_name, int_read_attr) + + +class IntWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + int_write_attr = AttrW(Int(), group=PviGroup.PARAMETERS) + super().__init__(panda_name.attribute_name, int_write_attr) + + +class ScalarParamField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + scalar_param_attr = AttrRW(Float(), group=PviGroup.PARAMETERS) + super().__init__(panda_name.attribute_name, scalar_param_attr) + + +class ScalarReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + scalar_read_attr = AttrR(Float(), group=PviGroup.READBACKS) + super().__init__(panda_name.attribute_name, scalar_read_attr) + +class ScalarWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + scalar_read_attr = AttrR(Float(), group=PviGroup.PARAMETERS) + super().__init__(panda_name.attribute_name, scalar_read_attr) + + +class BitParamField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + bit_param_attr = AttrRW(Bool(znam="0", onam="1"), group=PviGroup.PARAMETERS) + super().__init__(panda_name.attribute_name, bit_param_attr) + + +class BitReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + bit_read_attr = AttrR(Bool(znam="0", onam="1"), group=PviGroup.READBACKS) + super().__init__(panda_name.attribute_name, bit_read_attr) + +class BitWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + bit_write_attr = AttrW(Bool(znam="0", onam="1"), group=PviGroup.OUTPUTS) + super().__init__(panda_name.attribute_name, bit_write_attr) + + +class ActionReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): + action_read_attr = AttrW(Bool(znam="0", onam="1"), group=PviGroup.READBACKS) + super().__init__(panda_name.attribute_name, action_read_attr) + + +class ActionWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class LutParamField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class LutReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class LutWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class EnumParamField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class EnumReadField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class EnumWriteField(Field): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class TimeSubTypeParamField(TimeField): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class TimeSubTypeReadField(TimeField): + def __init__( + self, panda_name: PandaName, description: str | None + ): ... + + ... + + +class TimeSubTypeWriteField(TimeField): + def __init__( + self, 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, + }, +} diff --git a/src/fastcs_pandablocks/types/__init__.py b/src/fastcs_pandablocks/types/__init__.py index ae2c343..f7eadaf 100644 --- a/src/fastcs_pandablocks/types/__init__.py +++ b/src/fastcs_pandablocks/types/__init__.py @@ -4,7 +4,6 @@ PANDA_SEPERATOR, EpicsName, PandaName, - PviName, ) __all__ = [ @@ -12,6 +11,5 @@ "EpicsName", "PANDA_SEPERATOR", "PandaName", - "PviName", "ResponseType", ] diff --git a/src/fastcs_pandablocks/types/string_types.py b/src/fastcs_pandablocks/types/string_types.py index 96de1e4..4d54ef7 100644 --- a/src/fastcs_pandablocks/types/string_types.py +++ b/src/fastcs_pandablocks/types/string_types.py @@ -2,6 +2,7 @@ import re from dataclasses import dataclass +from functools import cached_property from typing import TypeVar T = TypeVar("T") @@ -35,36 +36,26 @@ def _format_with_seperator( return result.lstrip(seperator) +def _to_python_attribute_name(string: str): + return string.replace("-", "_").lower() + + @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 - ) +class PandaName: + block: str | None = None + block_number: int | None = None + field: str | None = None + sub_field: str | None = None + + @cached_property + def _string_form(self) -> str: + return _format_with_seperator( + PANDA_SEPERATOR, (self.block, self.block_number), self.field, self.sub_field ) + def __str__(self) -> str: + return self._string_form + @classmethod def from_string(cls, name: str): split_name = name.split(PANDA_SEPERATOR) @@ -77,7 +68,8 @@ def from_string(cls, name: str): block=block, block_number=block_number, field=field, sub_field=sub_field ) - def to_epics_name(self): + @cached_property + def epics_name(self): return EpicsName( block=self.block, block_number=self.block_number, @@ -85,31 +77,40 @@ def to_epics_name(self): 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 + @cached_property + def attribute_name(self) -> str: + if self.sub_field: + return _to_python_attribute_name(self.sub_field) + if self.field: + return _to_python_attribute_name(self.field) + if self.block: + return _to_python_attribute_name(self.block) + ( + f"_{self.block_number}" if self.block_number is not None else "" ) + return "" + + +@dataclass(frozen=True) +class EpicsName: + prefix: str | None = None + block: str | None = None + block_number: int | None = None + field: str | None = None + sub_field: str | None = None + + @cached_property + def _string_form(self) -> str: + return _format_with_seperator( + EPICS_SEPERATOR, + self.prefix, + (self.block, self.block_number), + self.field, + self.sub_field, ) + def __str__(self) -> str: + return self._string_form + @classmethod def from_string(cls, name: str) -> EpicsName: """Converts a string to an EPICS name, must contain a prefix.""" @@ -132,7 +133,8 @@ def from_string(cls, name: str) -> EpicsName: sub_field=sub_field, ) - def to_panda_name(self) -> PandaName: + @cached_property + def panda_name(self) -> PandaName: return PandaName( block=self.block, block_number=self.block_number, @@ -140,17 +142,6 @@ def to_panda_name(self) -> PandaName: 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: @@ -189,6 +180,8 @@ def __contains__(self, other: EpicsName) -> bool: 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 + elif sub_pv_1 and sub_pv_2 is None: + return False return True return ( @@ -198,7 +191,3 @@ def _check_eq(sub_pv_1: T, sub_pv_2: T) -> bool: 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 087da52..84ea15c 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,12 +1,39 @@ -from fastcs_pandablocks.types import EpicsName +from dataclasses import FrozenInstanceError + +import pytest + +from fastcs_pandablocks.types import EpicsName, PandaName + + +@pytest.mark.parametrize( + "name_factory", + [ + lambda: EpicsName.from_string("PREFIX:BLOCK:FIELD"), + lambda: PandaName.from_string("BLOCK.FIELD"), + ], +) +def test_names_are_frozen(name_factory): + name = name_factory() + with pytest.raises(FrozenInstanceError): + name.block = "hello" def test_epics_name(): - name1 = EpicsName.from_string("prefix:block1:field") + string_form = "prefix:block1:field:sub_field" + name1 = EpicsName.from_string(string_form) assert name1.prefix == "prefix" assert name1.block == "block" assert name1.block_number == 1 assert name1.field == "field" + assert name1.sub_field == "sub_field" + assert str(name1) == string_form + assert name1 == EpicsName( + prefix="prefix", + block="block", + block_number=1, + field="field", + sub_field="sub_field", + ) def test_epics_name_add(): @@ -17,17 +44,23 @@ def test_epics_name_add(): assert EpicsName(block="block") + EpicsName(block_number=1) == EpicsName( block="block", block_number=1 ) - - -def test_malformed_epics_name_add(): - pass + with pytest.raises(TypeError) as error: + _ = EpicsName(block="block", block_number=1, field="field") + EpicsName( + block="block", block_number=2, field="field" + ) + assert str(error.value) == "Ambiguous pv elements on `EpicsName` add 1 and 2" def test_epics_name_contains(): - parent_name = EpicsName.from_string("prefix:block1:field") - assert EpicsName(block="block") in parent_name - assert EpicsName(block="block", field="field") in parent_name - - -def test_malformed_epics_name_contains(): - pass + parent_name = EpicsName(prefix="prefix", block="block") + assert parent_name in parent_name + assert EpicsName(prefix="prefix", block="block", block_number=1) in parent_name + assert ( + EpicsName(prefix="prefix", block="block", block_number=1, field="field") + in parent_name + ) + assert parent_name not in EpicsName(block="block", block_number=1) + assert parent_name not in EpicsName(prefix="prefix", block="block", block_number=2) + assert parent_name not in EpicsName( + prefix="prefix", block="block", block_number=1, field="field" + ) From 6000568536b29c65e5baf4ac28818a9af20309f6 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 16 Oct 2024 13:43:19 +0100 Subject: [PATCH 07/10] ioc now successfully runs --- pyproject.toml | 2 +- src/fastcs_pandablocks/__init__.py | 6 +- src/fastcs_pandablocks/gui.py | 3 +- src/fastcs_pandablocks/panda/blocks.py | 66 ++-- src/fastcs_pandablocks/panda/controller.py | 23 +- src/fastcs_pandablocks/panda/fields.py | 304 +++++++++---------- src/fastcs_pandablocks/types/__init__.py | 3 +- src/fastcs_pandablocks/types/annotations.py | 3 + src/fastcs_pandablocks/types/string_types.py | 45 +-- tests/test_introspection.py | 0 10 files changed, 233 insertions(+), 222 deletions(-) create mode 100644 tests/test_introspection.py diff --git a/pyproject.toml b/pyproject.toml index 49e6c8c..22903be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ ] description = "A softioc to control a PandABlocks-FPGA." dependencies = [ - "fastcs~=0.6.0", + "fastcs@git+https://github.com/DiamondLightSource/FastCS@panda-conversion-improvements", "pandablocks~=0.10.0", "numpy<2", # until https://github.com/mdavidsaver/p4p/issues/145 is fixed "pydantic>2", diff --git a/src/fastcs_pandablocks/__init__.py b/src/fastcs_pandablocks/__init__.py index 2277088..2dcf1a1 100644 --- a/src/fastcs_pandablocks/__init__.py +++ b/src/fastcs_pandablocks/__init__.py @@ -4,6 +4,7 @@ from fastcs.backends.epics.backend import EpicsBackend from fastcs.backends.epics.gui import EpicsGUIFormat +from fastcs.backends.epics.ioc import EpicsIOCOptions, PvNamingConvention from ._version import __version__ from .gui import PandaGUIOptions @@ -21,7 +22,10 @@ def ioc( clear_bobfiles: bool = False, ): controller = PandaController(hostname, poll_period) - backend = EpicsBackend(controller, pv_prefix=str(prefix)) + epics_ioc_options = EpicsIOCOptions( + terminal=True, pv_naming_convention=PvNamingConvention.CAPITALIZED + ) + backend = EpicsBackend(controller, pv_prefix=str(prefix), options=epics_ioc_options) if clear_bobfiles and not screens_directory: raise ValueError("`clear_bobfiles` is True with no `screens_directory`") diff --git a/src/fastcs_pandablocks/gui.py b/src/fastcs_pandablocks/gui.py index 4285c76..c4189f0 100644 --- a/src/fastcs_pandablocks/gui.py +++ b/src/fastcs_pandablocks/gui.py @@ -1,5 +1,4 @@ from fastcs.backends.epics.gui import EpicsGUIOptions -class PandaGUIOptions(EpicsGUIOptions): - ... +class PandaGUIOptions(EpicsGUIOptions): ... diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py index 19dc7df..67d8816 100644 --- a/src/fastcs_pandablocks/panda/blocks.py +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -1,14 +1,15 @@ from collections.abc import Generator +from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.controller import SubController from pandablocks.responses import BlockInfo -from fastcs_pandablocks.types import EpicsName, PandaName, ResponseType +from fastcs_pandablocks.types import AttrType, EpicsName, PandaName, ResponseType from .fields import FIELD_TYPE_TO_FASTCS_TYPE, FieldType -class Block(SubController): +class BlockController(SubController): fields: dict[str, FieldType] def __init__( @@ -25,17 +26,24 @@ def __init__( self.fields = {} for field_raw_name, field_info in raw_fields.items(): - field_panda_name = PandaName(field=field_raw_name) - print(field_raw_name) + field_panda_name = self.panda_name + PandaName(field=field_raw_name) field = FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( - field_panda_name, field_info.description + # TODO make type safe after match statment + field_panda_name, + field_info, # type: ignore ) self.fields[field_raw_name] = field - self.register_sub_controller(field_panda_name.attribute_name, field) + if field.block_attribute: + setattr(self, *field.block_attribute) + if field.sub_field_controller: + self.register_sub_controller( + field_panda_name.attribute_name, field.sub_field_controller + ) + class Blocks: - _blocks: dict[str, dict[int | None, Block]] + _blocks: dict[str, dict[int | None, BlockController]] epics_prefix: EpicsName def __init__(self): @@ -51,11 +59,15 @@ def parse_introspected_data( ): iterator = ( range(1, block_info.number + 1) - if block_info.number > 1 else iter([None,]) + if block_info.number > 1 + else iter( + [ + None, + ] + ) ) self._blocks[block_name] = { - number: - Block( + number: BlockController( PandaName(block=block_name, block_number=number), block_info.number, block_info.description, @@ -65,26 +77,25 @@ def parse_introspected_data( } async def update_field_value(self, panda_name: PandaName, value: str): - assert panda_name.block - assert panda_name.field - field = ( - self._blocks[panda_name.block][panda_name.block_number].fields[panda_name.field] - ) - if panda_name.sub_field: - field = field.sub_fields[panda_name.sub_field] - await field.update_value(value) + attribute = self[panda_name] + + if isinstance(attribute, AttrW): + await attribute.process(value) + elif isinstance(attribute, (AttrRW | AttrR)): + await attribute.set(value) + else: + raise RuntimeError(f"Couldn't find panda field for {panda_name}.") def flattened_attribute_tree( - self - ) -> Generator[tuple[str, Block], None, None]: + self, + ) -> Generator[tuple[str, BlockController], None, None]: for blocks in self._blocks.values(): for block in blocks.values(): yield (block.panda_name.attribute_name, block) def __getitem__( - self, - name: EpicsName | PandaName - ) -> dict[int | None, Block] | Block | FieldType: + self, name: EpicsName | PandaName + ) -> dict[int | None, BlockController] | BlockController | AttrType: if name.block is None: raise ValueError(f"Cannot find block for name {name}.") blocks = self._blocks[name.block] @@ -94,6 +105,9 @@ def __getitem__( if name.field is None: return block field = block.fields[name.field] - if name.sub_field is None: - return field - return field.sub_fields[name.sub_field] + if not name.sub_field: + assert field.block_attribute + return field.block_attribute.attribute + + sub_field = getattr(field.sub_field_controller, name.sub_field) + return sub_field diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py index 154b741..47fc216 100644 --- a/src/fastcs_pandablocks/panda/controller.py +++ b/src/fastcs_pandablocks/panda/controller.py @@ -3,7 +3,6 @@ from fastcs.controller import Controller from fastcs.wrappers import scan -from fastcs_pandablocks import DEFAULT_POLL_PERIOD from fastcs_pandablocks.types import PandaName from .blocks import Blocks @@ -12,16 +11,16 @@ class PandaController(Controller): def __init__(self, hostname: str, poll_period: float) -> None: - super().__init__() self._raw_panda = RawPanda(hostname) self._blocks = Blocks() + self.is_connected = False - # TODO https://github.com/DiamondLightSource/FastCS/issues/62 - #self.fastcs_method = Scan(self.update(), poll_period) - - async def initialise(self) -> None: ... + super().__init__() async def connect(self) -> None: + if self.is_connected: + return + await self._raw_panda.connect() assert self._raw_panda.blocks @@ -32,7 +31,17 @@ async def connect(self) -> None: for attr_name, controller in self._blocks.flattened_attribute_tree(): self.register_sub_controller(attr_name, controller) - @scan(DEFAULT_POLL_PERIOD) # TODO https://github.com/DiamondLightSource/FastCS/issues/62 + self.is_connected = True + + async def initialise(self) -> None: + """ + We connect in initialise since FastCS doesn't connect until + it's already parsed sub controllers. + """ + await self.connect() + + # TODO https://github.com/DiamondLightSource/FastCS/issues/62 + @scan(0.1) async def update(self): await self._raw_panda.get_changes() assert self._raw_panda.changes diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 6bd79c3..68e29e4 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -1,303 +1,274 @@ from __future__ import annotations -from typing import Literal +from collections import namedtuple +from enum import Enum from fastcs.attributes import AttrR, AttrRW, AttrW from fastcs.controller import SubController from fastcs.datatypes import Bool, Float, Int, String +from pandablocks.responses import ( + BitMuxFieldInfo, + BitOutFieldInfo, + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, +) -from fastcs_pandablocks.types import PandaName +from fastcs_pandablocks.types import AttrType, PandaName -class PviGroup: +class WidgetGroup(Enum): """Purposely not an enum since we only ever want the string.""" + + NONE = None PARAMETERS = "Parameters" OUTPUTS = "Outputs" INPUTS = "Inputs" READBACKS = "Readbacks" -PviGroupField = Literal["Parameters", "Outputs", "Inputs", "Readbacks"] +class NamedAttribute(namedtuple("NamedAttribute", "attribute_name attribute")): + attribute_name: str + attribute: AttrType -class Field(SubController): - def __init__( - self, - attribute_name: str | None, - attribute: AttrRW | AttrR | AttrW | None, - sub_fields: dict[str, FieldType] | None = None, - ): - """ - For controlling the field, sub fields can also be added. - attribute_name and attribute are optional since some fields - e.g won't contain a top level record, but only sub fields. - """ - super().__init__() - self.sub_fields = sub_fields or {} - self.attribute_name = attribute_name - - if attribute_name and attribute: + +class SubFieldController(SubController): + def __init__(self, attributes: list[NamedAttribute]): + for attribute_name, attribute in attributes: setattr(self, attribute_name, attribute) - for sub_field_name, sub_field in self.sub_fields.items(): - self.register_sub_controller( - PandaName(sub_field=sub_field_name).attribute_name, - sub_field - ) - async def update_value(self, value: str): - if self.attribute_name is None: - return +class Field: + def __init__( + self, + attribute_name: str, + attribute: AttrRW | AttrR | AttrW, + sub_field_controller: SubFieldController | None = None, + ): + self.sub_field_controller = sub_field_controller - attribute = getattr(self, self.attribute_name) - if isinstance(attribute, AttrW): - await attribute.process(value) - else: - await attribute.set(value) + self.block_attribute = ( + NamedAttribute(attribute_name=attribute_name, attribute=attribute) + if (attribute_name and attribute) + else None + ) class TableField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... + def __init__(self, panda_name: PandaName, table_field_info: TableFieldInfo): + # TODO: Make a table type. For now we'll leave this to an int. + table_field = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) + super().__init__(panda_name.attribute_name, table_field) class TimeField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - - time_attr = AttrR(Float(), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, time_field_info: TimeFieldInfo): + time_attr = AttrR(Float(), group=WidgetGroup.PARAMETERS.value) # TODO: Find out how to add EGU and such super().__init__(panda_name.attribute_name, time_attr) class BitOutField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - bit_out_attr = AttrRW(Bool(znam="0", onam="1"), group=PviGroup.OUTPUTS) + def __init__(self, panda_name: PandaName, bit_out_field_info: BitOutFieldInfo): + bit_out_attr = AttrRW(Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value) super().__init__(panda_name.attribute_name, bit_out_attr) class PosOutField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): + def __init__(self, panda_name: PandaName, pos_out_field_info: PosOutFieldInfo): # TODO add capture and dataset subfields - pos_out_attr = AttrR(Float(), group=PviGroup.OUTPUTS) + pos_out_attr = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) super().__init__(panda_name.attribute_name, pos_out_attr) class ExtOutField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): + def __init__(self, panda_name: PandaName, ext_out_field_info: ExtOutFieldInfo): # TODO add capture and dataset subfields - super().__init__(None, None) + ext_out_field = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) + super().__init__(panda_name.attribute_name, ext_out_field) class ExtOutBitsField(ExtOutField): def __init__( - self, panda_name: PandaName, description: str | None + self, panda_name: PandaName, ext_out_bits_field_info: ExtOutBitsFieldInfo ): # TODO add capture and dataset subfields - super().__init__(panda_name, description) + super().__init__(panda_name, ext_out_bits_field_info) class BitMuxField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - bit_mux_attr = AttrRW(String(), group=PviGroup.INPUTS) + def __init__(self, panda_name: PandaName, bit_mux_field_info: BitMuxFieldInfo): + bit_mux_attr = AttrRW(String(), group=WidgetGroup.INPUTS.value) super().__init__(panda_name.attribute_name, bit_mux_attr) class PosMuxField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - pos_mux_attr = AttrRW(String(), group=PviGroup.INPUTS) + def __init__(self, panda_name: PandaName, pos_mux_field_info: PosMuxFieldInfo): + pos_mux_attr = AttrRW(String(), group=WidgetGroup.INPUTS.value) super().__init__(panda_name.attribute_name, pos_mux_attr) + class UintParamField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - uint_param_attr = AttrR(Float(prec=0), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, uint_param_field_info: UintFieldInfo): + uint_param_attr = AttrR(Float(prec=0), group=WidgetGroup.PARAMETERS.value) super().__init__(panda_name.attribute_name, uint_param_attr) + class UintReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - uint_read_attr = AttrR(Float(prec=0), group=PviGroup.READBACKS) + def __init__(self, panda_name: PandaName, uint_read_field_info: UintFieldInfo): + uint_read_attr = AttrR(Float(prec=0), group=WidgetGroup.READBACKS.value) super().__init__(panda_name.attribute_name, uint_read_attr) class UintWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - uint_write_attr = AttrW(Float(prec=0), group=PviGroup.OUTPUTS) + def __init__(self, panda_name: PandaName, uint_write_field_info: UintFieldInfo): + uint_write_attr = AttrW(Float(prec=0), group=WidgetGroup.OUTPUTS.value) super().__init__(panda_name.attribute_name, uint_write_attr) class IntParamField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - uint_param_attr = AttrRW(Float(prec=0), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, int_param_field_info: FieldInfo): + uint_param_attr = AttrRW(Float(prec=0), group=WidgetGroup.PARAMETERS.value) super().__init__(panda_name.attribute_name, uint_param_attr) class IntReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - int_read_attr = AttrR(Int(), group=PviGroup.READBACKS) + def __init__(self, panda_name: PandaName, int_read_field_info: FieldInfo): + int_read_attr = AttrR(Int(), group=WidgetGroup.READBACKS.value) super().__init__(panda_name.attribute_name, int_read_attr) class IntWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - int_write_attr = AttrW(Int(), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, int_write_field_info: FieldInfo): + int_write_attr = AttrW(Int(), group=WidgetGroup.PARAMETERS.value) super().__init__(panda_name.attribute_name, int_write_attr) class ScalarParamField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - scalar_param_attr = AttrRW(Float(), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, scalar_param_field_info: ScalarFieldInfo): + scalar_param_attr = AttrRW(Float(), group=WidgetGroup.PARAMETERS.value) super().__init__(panda_name.attribute_name, scalar_param_attr) class ScalarReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - scalar_read_attr = AttrR(Float(), group=PviGroup.READBACKS) + def __init__(self, panda_name: PandaName, scalar_read_field_info: ScalarFieldInfo): + scalar_read_attr = AttrR(Float(), group=WidgetGroup.READBACKS.value) super().__init__(panda_name.attribute_name, scalar_read_attr) + class ScalarWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - scalar_read_attr = AttrR(Float(), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, scalar_write_field_info: ScalarFieldInfo): + scalar_read_attr = AttrR(Float(), group=WidgetGroup.PARAMETERS.value) super().__init__(panda_name.attribute_name, scalar_read_attr) class BitParamField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - bit_param_attr = AttrRW(Bool(znam="0", onam="1"), group=PviGroup.PARAMETERS) + def __init__(self, panda_name: PandaName, bit_param_field_info: FieldInfo): + bit_param_attr = AttrRW( + Bool(znam="0", onam="1"), group=WidgetGroup.PARAMETERS.value + ) super().__init__(panda_name.attribute_name, bit_param_attr) class BitReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - bit_read_attr = AttrR(Bool(znam="0", onam="1"), group=PviGroup.READBACKS) + def __init__(self, panda_name: PandaName, bit_read_field_info: FieldInfo): + bit_read_attr = AttrR( + Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value + ) super().__init__(panda_name.attribute_name, bit_read_attr) + class BitWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - bit_write_attr = AttrW(Bool(znam="0", onam="1"), group=PviGroup.OUTPUTS) + def __init__(self, panda_name: PandaName, bit_write_field_info: FieldInfo): + bit_write_attr = AttrW( + Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value + ) super().__init__(panda_name.attribute_name, bit_write_attr) class ActionReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): - action_read_attr = AttrW(Bool(znam="0", onam="1"), group=PviGroup.READBACKS) + def __init__(self, panda_name: PandaName, action_read_field_info: FieldInfo): + action_read_attr = AttrR( + Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value + ) super().__init__(panda_name.attribute_name, action_read_attr) class ActionWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, action_write_field_info: FieldInfo): + action_write_attr = AttrW( + Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value + ) + super().__init__(panda_name.attribute_name, action_write_attr) class LutParamField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, lut_param_field_info: FieldInfo): + lut_param_field = AttrRW(String(), group=WidgetGroup.PARAMETERS.value) + super().__init__(panda_name.attribute_name, lut_param_field) class LutReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, lut_read_field_info: FieldInfo): + lut_read_field = AttrR(String(), group=WidgetGroup.READBACKS.value) + super().__init__(panda_name.attribute_name, lut_read_field) class LutWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, lut_read_field_info: FieldInfo): + lut_write_field = AttrR(String(), group=WidgetGroup.OUTPUTS.value) + super().__init__(panda_name.attribute_name, lut_write_field) class EnumParamField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, enum_param_field_info: EnumFieldInfo): + self.allowed_values = enum_param_field_info.labels + enum_param_field = AttrRW(String(), group=WidgetGroup.PARAMETERS.value) + super().__init__(panda_name.attribute_name, enum_param_field) class EnumReadField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, enum_read_field_info: EnumFieldInfo): + enum_read_field = AttrR(String(), group=WidgetGroup.READBACKS.value) + super().__init__(panda_name.attribute_name, enum_read_field) class EnumWriteField(Field): - def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + def __init__(self, panda_name: PandaName, enum_write_field_info: EnumFieldInfo): + enum_write_field = AttrW(String(), group=WidgetGroup.OUTPUTS.value) + super().__init__(panda_name.attribute_name, enum_write_field) -class TimeSubTypeParamField(TimeField): +class TimeSubTypeParamField(Field): def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + self, panda_name: PandaName, time_subtype_param_field_info: SubtypeTimeFieldInfo + ): + time_subtype_param_field = AttrRW(Float(), group=WidgetGroup.PARAMETERS.value) + super().__init__(panda_name.attribute_name, time_subtype_param_field) -class TimeSubTypeReadField(TimeField): +class TimeSubTypeReadField(Field): def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + self, panda_name: PandaName, time_subtype_read_field_info: SubtypeTimeFieldInfo + ): + time_subtype_read_field = AttrR(Float(), group=WidgetGroup.READBACKS.value) + super().__init__(panda_name.attribute_name, time_subtype_read_field) -class TimeSubTypeWriteField(TimeField): +class TimeSubTypeWriteField(Field): def __init__( - self, panda_name: PandaName, description: str | None - ): ... - - ... + self, panda_name: PandaName, time_subtype_write_field_info: SubtypeTimeFieldInfo + ): + time_subtype_write_field = AttrW(Float(), group=WidgetGroup.OUTPUTS.value) + super().__init__(panda_name.attribute_name, time_subtype_write_field) FieldType = ( @@ -334,6 +305,7 @@ def __init__( | TimeSubTypeWriteField ) +# TODO: Change to a match statement so we can easily add a PCAP field type. FIELD_TYPE_TO_FASTCS_TYPE: dict[str, dict[str | None, type[FieldType]]] = { "table": {None: TableField}, "time": { diff --git a/src/fastcs_pandablocks/types/__init__.py b/src/fastcs_pandablocks/types/__init__.py index f7eadaf..2fe1977 100644 --- a/src/fastcs_pandablocks/types/__init__.py +++ b/src/fastcs_pandablocks/types/__init__.py @@ -1,4 +1,4 @@ -from .annotations import ResponseType +from .annotations import AttrType, ResponseType from .string_types import ( EPICS_SEPERATOR, PANDA_SEPERATOR, @@ -11,5 +11,6 @@ "EpicsName", "PANDA_SEPERATOR", "PandaName", + "AttrType", "ResponseType", ] diff --git a/src/fastcs_pandablocks/types/annotations.py b/src/fastcs_pandablocks/types/annotations.py index 1f2b5c2..35355e3 100644 --- a/src/fastcs_pandablocks/types/annotations.py +++ b/src/fastcs_pandablocks/types/annotations.py @@ -1,3 +1,4 @@ +from fastcs.attributes import AttrR, AttrRW, AttrW from pandablocks.responses import ( BitMuxFieldInfo, BitOutFieldInfo, @@ -29,3 +30,5 @@ | TimeFieldInfo | UintFieldInfo ) + +AttrType = AttrRW | AttrR | AttrW diff --git a/src/fastcs_pandablocks/types/string_types.py b/src/fastcs_pandablocks/types/string_types.py index 4d54ef7..e1fe0ac 100644 --- a/src/fastcs_pandablocks/types/string_types.py +++ b/src/fastcs_pandablocks/types/string_types.py @@ -37,7 +37,24 @@ def _format_with_seperator( def _to_python_attribute_name(string: str): - return string.replace("-", "_").lower() + return string.replace("-", "_") + + +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 add " f"{sub_pv_1} and {sub_pv_2}" + ) + return sub_pv_2 or sub_pv_1 + + +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 + elif sub_pv_1 and sub_pv_2 is None: + return False + return True @dataclass(frozen=True) @@ -77,6 +94,14 @@ def epics_name(self): sub_field=self.sub_field, ) + def __add__(self, other: PandaName) -> PandaName: + return PandaName( + 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), + ) + @cached_property def attribute_name(self) -> str: if self.sub_field: @@ -85,7 +110,7 @@ def attribute_name(self) -> str: return _to_python_attribute_name(self.field) if self.block: return _to_python_attribute_name(self.block) + ( - f"_{self.block_number}" if self.block_number is not None else "" + f"{self.block_number}" if self.block_number is not None else "" ) return "" @@ -150,15 +175,6 @@ def __add__(self, other: EpicsName) -> EpicsName: == 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), @@ -177,13 +193,6 @@ def __contains__(self, other: EpicsName) -> bool: (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 - elif sub_pv_1 and sub_pv_2 is None: - return False - return True - return ( _check_eq(self.prefix, other.prefix) and _check_eq(self.block, other.block) diff --git a/tests/test_introspection.py b/tests/test_introspection.py new file mode 100644 index 0000000..e69de29 From b7e0c8df270b2a1f2f4e577417a59538c7f1c2eb Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 17 Oct 2024 15:39:40 +0100 Subject: [PATCH 08/10] WIP --- pyproject.toml | 3 + src/fastcs_pandablocks/__init__.py | 19 +- src/fastcs_pandablocks/__main__.py | 20 +- src/fastcs_pandablocks/handler.py | 13 - src/fastcs_pandablocks/handlers.py | 23 ++ src/fastcs_pandablocks/panda/blocks.py | 13 +- src/fastcs_pandablocks/panda/controller.py | 11 +- src/fastcs_pandablocks/panda/fields.py | 275 ++++++++++++++----- src/fastcs_pandablocks/types/__init__.py | 8 +- src/fastcs_pandablocks/types/annotations.py | 37 +-- src/fastcs_pandablocks/types/string_types.py | 30 +- 11 files changed, 313 insertions(+), 139 deletions(-) delete mode 100644 src/fastcs_pandablocks/handler.py create mode 100644 src/fastcs_pandablocks/handlers.py diff --git a/pyproject.toml b/pyproject.toml index 22903be..ea2cbfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,3 +119,6 @@ lint.select = [ # See https://github.com/DiamondLightSource/python-copier-template/issues/154 # Remove this line to forbid private member access in tests "tests/**/*" = ["SLF001"] +# Ruff wants us to use `(|)` instead of `Union[,]` in types, but pyright warns against +# it. For now we'll use `Union` and turn the ruff error off. +"**" = ["UP007"] diff --git a/src/fastcs_pandablocks/__init__.py b/src/fastcs_pandablocks/__init__.py index 2dcf1a1..4f6a708 100644 --- a/src/fastcs_pandablocks/__init__.py +++ b/src/fastcs_pandablocks/__init__.py @@ -4,7 +4,8 @@ from fastcs.backends.epics.backend import EpicsBackend from fastcs.backends.epics.gui import EpicsGUIFormat -from fastcs.backends.epics.ioc import EpicsIOCOptions, PvNamingConvention +from fastcs.backends.epics.ioc import EpicsIOCOptions +from fastcs.backends.epics.util import EpicsNameOptions, PvNamingConvention from ._version import __version__ from .gui import PandaGUIOptions @@ -15,17 +16,23 @@ def ioc( - prefix: EpicsName, + epics_prefix: EpicsName, hostname: str, screens_directory: Path | None = None, - poll_period: float = DEFAULT_POLL_PERIOD, clear_bobfiles: bool = False, + poll_period: float = DEFAULT_POLL_PERIOD, + naming_convention: PvNamingConvention = PvNamingConvention.CAPITALIZED, + pv_separator: str = ":", ): + name_options = EpicsNameOptions( + pv_naming_convention=naming_convention, pv_separator=pv_separator + ) + epics_ioc_options = EpicsIOCOptions(terminal=True, name_options=name_options) + controller = PandaController(hostname, poll_period) - epics_ioc_options = EpicsIOCOptions( - terminal=True, pv_naming_convention=PvNamingConvention.CAPITALIZED + backend = EpicsBackend( + controller, pv_prefix=str(epics_prefix), ioc_options=epics_ioc_options ) - backend = EpicsBackend(controller, pv_prefix=str(prefix), options=epics_ioc_options) if clear_bobfiles and not screens_directory: raise ValueError("`clear_bobfiles` is True with no `screens_directory`") diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index bf0f8e5..a5d8e20 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -4,6 +4,8 @@ import logging from pathlib import Path +from fastcs.backends.epics.ioc import PvNamingConvention + from fastcs_pandablocks import DEFAULT_POLL_PERIOD, ioc from fastcs_pandablocks.types import EpicsName @@ -57,7 +59,19 @@ def main(): "--poll-period", default=DEFAULT_POLL_PERIOD, type=float, - help="Period to poll", + help="Period in seconds with which to poll the panda.", + ) + run_parser.add_argument( + "--pv-naming-convention", + default=PvNamingConvention.CAPITALIZED.name, + choices=[choice.name for choice in PvNamingConvention], + help="Naming convention of the EPICS PVs.", + ) + run_parser.add_argument( + "--pv-separator", + default=":", + type=str, + help="Separator to use between EPICS PV sections.", ) parsed_args = parser.parse_args() @@ -72,8 +86,10 @@ def main(): EpicsName(prefix=parsed_args.prefix), parsed_args.hostname, screens_directory=Path(parsed_args.screens_dir), - poll_period=parsed_args.poll_period, clear_bobfiles=parsed_args.clear_bobfiles, + poll_period=parsed_args.poll_period, + naming_convention=PvNamingConvention(parsed_args.pv_naming_convention), + pv_separator=parsed_args.pv_separator, ) diff --git a/src/fastcs_pandablocks/handler.py b/src/fastcs_pandablocks/handler.py deleted file mode 100644 index a2f686a..0000000 --- a/src/fastcs_pandablocks/handler.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Any - -from fastcs.attributes import AttrW, Sender - -from fastcs_pandablocks.types import PandaName - - -class DefaultFieldSender(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/handlers.py b/src/fastcs_pandablocks/handlers.py new file mode 100644 index 0000000..afe6652 --- /dev/null +++ b/src/fastcs_pandablocks/handlers.py @@ -0,0 +1,23 @@ +from typing import Any + +from fastcs.attributes import AttrW, Handler, Sender + +from fastcs_pandablocks.types import AttrType, PandaName + + +class DefaultFieldSender(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) + + +class UpdateEguSender(Sender): + def __init__(self, attr_to_update: AttrType): + """Update the attr""" + self.attr_to_update = attr_to_update + + async def put(self, controller: Any, attr: AttrW, value: str) -> None: + # TODO find out how to update attr_to_update's EGU with the value + ... diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py index 67d8816..4a5092d 100644 --- a/src/fastcs_pandablocks/panda/blocks.py +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -34,11 +34,14 @@ def __init__( field_info, # type: ignore ) self.fields[field_raw_name] = field - if field.block_attribute: - setattr(self, *field.block_attribute) + + def initialise(self): + for field_name, field in self.fields.items(): + if field.named_attribute: + setattr(self, *field.named_attribute) if field.sub_field_controller: self.register_sub_controller( - field_panda_name.attribute_name, field.sub_field_controller + field_name, sub_controller=field.sub_field_controller ) @@ -106,8 +109,8 @@ def __getitem__( return block field = block.fields[name.field] if not name.sub_field: - assert field.block_attribute - return field.block_attribute.attribute + assert field.named_attribute + return field.named_attribute.attribute sub_field = getattr(field.sub_field_controller, name.sub_field) return sub_field diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py index 47fc216..1d37db7 100644 --- a/src/fastcs_pandablocks/panda/controller.py +++ b/src/fastcs_pandablocks/panda/controller.py @@ -28,18 +28,15 @@ async def connect(self) -> None: self._blocks.parse_introspected_data( self._raw_panda.blocks, self._raw_panda.fields ) - for attr_name, controller in self._blocks.flattened_attribute_tree(): - self.register_sub_controller(attr_name, controller) - self.is_connected = True async def initialise(self) -> None: - """ - We connect in initialise since FastCS doesn't connect until - it's already parsed sub controllers. - """ await self.connect() + for attr_name, controller in self._blocks.flattened_attribute_tree(): + self.register_sub_controller(attr_name, controller) + controller.initialise() + # TODO https://github.com/DiamondLightSource/FastCS/issues/62 @scan(0.1) async def update(self): diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 68e29e4..690c84e 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections import namedtuple +from dataclasses import dataclass from enum import Enum from fastcs.attributes import AttrR, AttrRW, AttrW @@ -22,6 +23,7 @@ UintFieldInfo, ) +from fastcs_pandablocks.handlers import DefaultFieldSender, UpdateEguSender from fastcs_pandablocks.types import AttrType, PandaName @@ -41,59 +43,121 @@ class NamedAttribute(namedtuple("NamedAttribute", "attribute_name attribute")): class SubFieldController(SubController): - def __init__(self, attributes: list[NamedAttribute]): + def __init__(self, *attributes: NamedAttribute): + super().__init__() for attribute_name, attribute in attributes: setattr(self, attribute_name, attribute) +@dataclass class Field: - def __init__( - self, - attribute_name: str, - attribute: AttrRW | AttrR | AttrW, - sub_field_controller: SubFieldController | None = None, - ): - self.sub_field_controller = sub_field_controller - - self.block_attribute = ( - NamedAttribute(attribute_name=attribute_name, attribute=attribute) - if (attribute_name and attribute) - else None - ) + named_attribute: NamedAttribute | None + sub_field_controller: SubFieldController | None = None class TableField(Field): def __init__(self, panda_name: PandaName, table_field_info: TableFieldInfo): - # TODO: Make a table type. For now we'll leave this to an int. - table_field = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, table_field) + super().__init__( + NamedAttribute( + panda_name.attribute_name, + AttrR( + Float(), + # To be added once we have a pvxs backend + # description=table_field_info.description, + group=WidgetGroup.OUTPUTS.value, + ), + ) + ) class TimeField(Field): def __init__(self, panda_name: PandaName, time_field_info: TimeFieldInfo): - time_attr = AttrR(Float(), group=WidgetGroup.PARAMETERS.value) - # TODO: Find out how to add EGU and such - super().__init__(panda_name.attribute_name, time_attr) + time_attr = AttrRW( + Float(), + # To be added once we have a pvxs backend + # description=time_field_info.description, + group=WidgetGroup.PARAMETERS.value, + ) + sub_units = NamedAttribute( + attribute_name="units", + attribute=AttrW( + String(), + handler=UpdateEguSender(time_attr), + group=WidgetGroup.PARAMETERS.value, + ), + ) + super().__init__( + NamedAttribute(panda_name.attribute_name, time_attr), + SubFieldController(sub_units), + ) + + +class TimeSubTypeParamField(Field): + def __init__( + self, panda_name: PandaName, time_subtype_param_field_info: SubtypeTimeFieldInfo + ): + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, + attribute=AttrRW(Float(), group=WidgetGroup.PARAMETERS.value), + ) + ) + + +class TimeSubTypeReadField(Field): + def __init__( + self, panda_name: PandaName, time_subtype_read_field_info: SubtypeTimeFieldInfo + ): + time_subtype_read_field = AttrR(Float(), group=WidgetGroup.READBACKS.value) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, + attribute=time_subtype_read_field, + ) + ) + + +class TimeSubTypeWriteField(Field): + def __init__( + self, panda_name: PandaName, time_subtype_write_field_info: SubtypeTimeFieldInfo + ): + time_subtype_write_field = AttrW(Float(), group=WidgetGroup.OUTPUTS.value) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, + attribute=time_subtype_write_field, + ) + ) class BitOutField(Field): def __init__(self, panda_name: PandaName, bit_out_field_info: BitOutFieldInfo): - bit_out_attr = AttrRW(Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, bit_out_attr) + bit = AttrRW(Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value) + super().__init__( + NamedAttribute(attribute_name=panda_name.attribute_name, attribute=bit) + ) class PosOutField(Field): def __init__(self, panda_name: PandaName, pos_out_field_info: PosOutFieldInfo): # TODO add capture and dataset subfields pos_out_attr = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, pos_out_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=pos_out_attr + ) + ) class ExtOutField(Field): def __init__(self, panda_name: PandaName, ext_out_field_info: ExtOutFieldInfo): # TODO add capture and dataset subfields ext_out_field = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, ext_out_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=ext_out_field + ) + ) class ExtOutBitsField(ExtOutField): @@ -107,67 +171,116 @@ def __init__( class BitMuxField(Field): def __init__(self, panda_name: PandaName, bit_mux_field_info: BitMuxFieldInfo): bit_mux_attr = AttrRW(String(), group=WidgetGroup.INPUTS.value) - super().__init__(panda_name.attribute_name, bit_mux_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=bit_mux_attr + ) + ) class PosMuxField(Field): def __init__(self, panda_name: PandaName, pos_mux_field_info: PosMuxFieldInfo): pos_mux_attr = AttrRW(String(), group=WidgetGroup.INPUTS.value) - super().__init__(panda_name.attribute_name, pos_mux_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=pos_mux_attr + ) + ) class UintParamField(Field): def __init__(self, panda_name: PandaName, uint_param_field_info: UintFieldInfo): uint_param_attr = AttrR(Float(prec=0), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, uint_param_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=uint_param_attr + ) + ) class UintReadField(Field): def __init__(self, panda_name: PandaName, uint_read_field_info: UintFieldInfo): - uint_read_attr = AttrR(Float(prec=0), group=WidgetGroup.READBACKS.value) - super().__init__(panda_name.attribute_name, uint_read_attr) + uint_read_attr = AttrR( + Float(prec=0), + group=WidgetGroup.READBACKS.value, + # To be added once we have a pvxs backend + # description=uint_read_field_info.description, + ) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=uint_read_attr + ) + ) class UintWriteField(Field): def __init__(self, panda_name: PandaName, uint_write_field_info: UintFieldInfo): uint_write_attr = AttrW(Float(prec=0), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, uint_write_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=uint_write_attr + ) + ) class IntParamField(Field): def __init__(self, panda_name: PandaName, int_param_field_info: FieldInfo): uint_param_attr = AttrRW(Float(prec=0), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, uint_param_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=uint_param_attr + ) + ) class IntReadField(Field): def __init__(self, panda_name: PandaName, int_read_field_info: FieldInfo): int_read_attr = AttrR(Int(), group=WidgetGroup.READBACKS.value) - super().__init__(panda_name.attribute_name, int_read_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=int_read_attr + ) + ) class IntWriteField(Field): def __init__(self, panda_name: PandaName, int_write_field_info: FieldInfo): int_write_attr = AttrW(Int(), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, int_write_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=int_write_attr + ) + ) class ScalarParamField(Field): def __init__(self, panda_name: PandaName, scalar_param_field_info: ScalarFieldInfo): scalar_param_attr = AttrRW(Float(), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, scalar_param_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=scalar_param_attr + ) + ) class ScalarReadField(Field): def __init__(self, panda_name: PandaName, scalar_read_field_info: ScalarFieldInfo): scalar_read_attr = AttrR(Float(), group=WidgetGroup.READBACKS.value) - super().__init__(panda_name.attribute_name, scalar_read_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=scalar_read_attr + ) + ) class ScalarWriteField(Field): def __init__(self, panda_name: PandaName, scalar_write_field_info: ScalarFieldInfo): scalar_read_attr = AttrR(Float(), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, scalar_read_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=scalar_read_attr + ) + ) class BitParamField(Field): @@ -175,7 +288,11 @@ def __init__(self, panda_name: PandaName, bit_param_field_info: FieldInfo): bit_param_attr = AttrRW( Bool(znam="0", onam="1"), group=WidgetGroup.PARAMETERS.value ) - super().__init__(panda_name.attribute_name, bit_param_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=bit_param_attr + ) + ) class BitReadField(Field): @@ -183,7 +300,11 @@ def __init__(self, panda_name: PandaName, bit_read_field_info: FieldInfo): bit_read_attr = AttrR( Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value ) - super().__init__(panda_name.attribute_name, bit_read_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=bit_read_attr + ) + ) class BitWriteField(Field): @@ -191,7 +312,11 @@ def __init__(self, panda_name: PandaName, bit_write_field_info: FieldInfo): bit_write_attr = AttrW( Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value ) - super().__init__(panda_name.attribute_name, bit_write_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=bit_write_attr + ) + ) class ActionReadField(Field): @@ -199,7 +324,11 @@ def __init__(self, panda_name: PandaName, action_read_field_info: FieldInfo): action_read_attr = AttrR( Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value ) - super().__init__(panda_name.attribute_name, action_read_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=action_read_attr + ) + ) class ActionWriteField(Field): @@ -207,68 +336,72 @@ def __init__(self, panda_name: PandaName, action_write_field_info: FieldInfo): action_write_attr = AttrW( Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value ) - super().__init__(panda_name.attribute_name, action_write_attr) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=action_write_attr + ) + ) class LutParamField(Field): def __init__(self, panda_name: PandaName, lut_param_field_info: FieldInfo): lut_param_field = AttrRW(String(), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, lut_param_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=lut_param_field + ) + ) class LutReadField(Field): def __init__(self, panda_name: PandaName, lut_read_field_info: FieldInfo): lut_read_field = AttrR(String(), group=WidgetGroup.READBACKS.value) - super().__init__(panda_name.attribute_name, lut_read_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=lut_read_field + ) + ) class LutWriteField(Field): def __init__(self, panda_name: PandaName, lut_read_field_info: FieldInfo): lut_write_field = AttrR(String(), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, lut_write_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=lut_write_field + ) + ) class EnumParamField(Field): def __init__(self, panda_name: PandaName, enum_param_field_info: EnumFieldInfo): self.allowed_values = enum_param_field_info.labels enum_param_field = AttrRW(String(), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, enum_param_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=enum_param_field + ) + ) class EnumReadField(Field): def __init__(self, panda_name: PandaName, enum_read_field_info: EnumFieldInfo): enum_read_field = AttrR(String(), group=WidgetGroup.READBACKS.value) - super().__init__(panda_name.attribute_name, enum_read_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=enum_read_field + ) + ) class EnumWriteField(Field): def __init__(self, panda_name: PandaName, enum_write_field_info: EnumFieldInfo): enum_write_field = AttrW(String(), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, enum_write_field) - - -class TimeSubTypeParamField(Field): - def __init__( - self, panda_name: PandaName, time_subtype_param_field_info: SubtypeTimeFieldInfo - ): - time_subtype_param_field = AttrRW(Float(), group=WidgetGroup.PARAMETERS.value) - super().__init__(panda_name.attribute_name, time_subtype_param_field) - - -class TimeSubTypeReadField(Field): - def __init__( - self, panda_name: PandaName, time_subtype_read_field_info: SubtypeTimeFieldInfo - ): - time_subtype_read_field = AttrR(Float(), group=WidgetGroup.READBACKS.value) - super().__init__(panda_name.attribute_name, time_subtype_read_field) - - -class TimeSubTypeWriteField(Field): - def __init__( - self, panda_name: PandaName, time_subtype_write_field_info: SubtypeTimeFieldInfo - ): - time_subtype_write_field = AttrW(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__(panda_name.attribute_name, time_subtype_write_field) + super().__init__( + NamedAttribute( + attribute_name=panda_name.attribute_name, attribute=enum_write_field + ) + ) FieldType = ( diff --git a/src/fastcs_pandablocks/types/__init__.py b/src/fastcs_pandablocks/types/__init__.py index 2fe1977..dfe4411 100644 --- a/src/fastcs_pandablocks/types/__init__.py +++ b/src/fastcs_pandablocks/types/__init__.py @@ -1,15 +1,15 @@ from .annotations import AttrType, ResponseType from .string_types import ( - EPICS_SEPERATOR, - PANDA_SEPERATOR, + EPICS_SEPARATOR, + PANDA_SEPARATOR, EpicsName, PandaName, ) __all__ = [ - "EPICS_SEPERATOR", + "EPICS_SEPARATOR", "EpicsName", - "PANDA_SEPERATOR", + "PANDA_SEPARATOR", "PandaName", "AttrType", "ResponseType", diff --git a/src/fastcs_pandablocks/types/annotations.py b/src/fastcs_pandablocks/types/annotations.py index 35355e3..8684d03 100644 --- a/src/fastcs_pandablocks/types/annotations.py +++ b/src/fastcs_pandablocks/types/annotations.py @@ -1,4 +1,5 @@ from fastcs.attributes import AttrR, AttrRW, AttrW +from typing import Union from pandablocks.responses import ( BitMuxFieldInfo, BitOutFieldInfo, @@ -15,20 +16,24 @@ UintFieldInfo, ) -ResponseType = ( - BitMuxFieldInfo - | BitOutFieldInfo - | EnumFieldInfo - | ExtOutBitsFieldInfo - | ExtOutFieldInfo - | FieldInfo - | PosMuxFieldInfo - | PosOutFieldInfo - | ScalarFieldInfo - | SubtypeTimeFieldInfo - | TableFieldInfo - | TimeFieldInfo - | UintFieldInfo -) -AttrType = AttrRW | AttrR | AttrW +# Pyright gives us variable not allowed in type expression error +# if we try to use the new (|) syntax +ResponseType = Union[ + BitMuxFieldInfo, + BitOutFieldInfo, + EnumFieldInfo + , ExtOutBitsFieldInfo + , ExtOutFieldInfo + , FieldInfo + , PosMuxFieldInfo + , PosOutFieldInfo + , ScalarFieldInfo + , SubtypeTimeFieldInfo + , TableFieldInfo + , TimeFieldInfo + , UintFieldInfo +] + + +AttrType = Union[AttrRW, AttrR, AttrW] diff --git a/src/fastcs_pandablocks/types/string_types.py b/src/fastcs_pandablocks/types/string_types.py index e1fe0ac..20e6023 100644 --- a/src/fastcs_pandablocks/types/string_types.py +++ b/src/fastcs_pandablocks/types/string_types.py @@ -7,8 +7,8 @@ T = TypeVar("T") -EPICS_SEPERATOR = ":" -PANDA_SEPERATOR = "." +EPICS_SEPARATOR = ":" +PANDA_SEPARATOR = "." def _extract_number_at_of_string(string: str) -> tuple[str, int | None]: @@ -19,25 +19,25 @@ def _extract_number_at_of_string(string: str) -> tuple[str, int | None]: return string, None -def _format_with_seperator( - seperator: str, *sections: tuple[str | None, int | None] | str | None +def _format_with_separator( + separator: 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}" + result += f"{separator}{section_string}" if section_number is not None: result += f"{section_number}" elif section is not None: - result += f"{seperator}{section}" + result += f"{separator}{section}" - return result.lstrip(seperator) + return result.lstrip(separator) def _to_python_attribute_name(string: str): - return string.replace("-", "_") + return string.replace("-", "_").lower() def _choose_sub_pv(sub_pv_1: T, sub_pv_2: T) -> T: @@ -66,8 +66,8 @@ class PandaName: @cached_property def _string_form(self) -> str: - return _format_with_seperator( - PANDA_SEPERATOR, (self.block, self.block_number), self.field, self.sub_field + return _format_with_separator( + PANDA_SEPARATOR, (self.block, self.block_number), self.field, self.sub_field ) def __str__(self) -> str: @@ -75,7 +75,7 @@ def __str__(self) -> str: @classmethod def from_string(cls, name: str): - split_name = name.split(PANDA_SEPERATOR) + split_name = name.split(PANDA_SEPARATOR) block, block_number = _extract_number_at_of_string(split_name[0]) field = split_name[1] @@ -125,8 +125,8 @@ class EpicsName: @cached_property def _string_form(self) -> str: - return _format_with_seperator( - EPICS_SEPERATOR, + return _format_with_separator( + EPICS_SEPARATOR, self.prefix, (self.block, self.block_number), self.field, @@ -139,13 +139,13 @@ def __str__(self) -> str: @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) + split_name = name.split(EPICS_SEPARATOR) 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) + split_name = name.split(EPICS_SEPARATOR) 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 From 2424144de189397adad0624d7370b5e2773661fe Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 24 Oct 2024 10:03:08 +0100 Subject: [PATCH 09/10] fields are now `SubController`s Also made field generator a match statement. --- src/fastcs_pandablocks/__main__.py | 2 +- src/fastcs_pandablocks/handlers.py | 6 +- src/fastcs_pandablocks/panda/blocks.py | 48 +- src/fastcs_pandablocks/panda/fields.py | 691 ++++++++++---------- src/fastcs_pandablocks/types/__init__.py | 3 +- src/fastcs_pandablocks/types/annotations.py | 28 +- tests/test_types.py | 2 +- 7 files changed, 382 insertions(+), 398 deletions(-) diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index a5d8e20..a0c4ae3 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -4,7 +4,7 @@ import logging from pathlib import Path -from fastcs.backends.epics.ioc import PvNamingConvention +from fastcs.backends.epics.util import PvNamingConvention from fastcs_pandablocks import DEFAULT_POLL_PERIOD, ioc from fastcs_pandablocks.types import EpicsName diff --git a/src/fastcs_pandablocks/handlers.py b/src/fastcs_pandablocks/handlers.py index afe6652..75d46a5 100644 --- a/src/fastcs_pandablocks/handlers.py +++ b/src/fastcs_pandablocks/handlers.py @@ -1,8 +1,8 @@ from typing import Any -from fastcs.attributes import AttrW, Handler, Sender +from fastcs.attributes import Attribute, AttrW, Sender -from fastcs_pandablocks.types import AttrType, PandaName +from fastcs_pandablocks.types import PandaName class DefaultFieldSender(Sender): @@ -14,7 +14,7 @@ async def put(self, controller: Any, attr: AttrW, value: str) -> None: class UpdateEguSender(Sender): - def __init__(self, attr_to_update: AttrType): + def __init__(self, attr_to_update: Attribute): """Update the attr""" self.attr_to_update = attr_to_update diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py index 4a5092d..297c3d2 100644 --- a/src/fastcs_pandablocks/panda/blocks.py +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -1,16 +1,16 @@ from collections.abc import Generator -from fastcs.attributes import AttrR, AttrRW, AttrW +from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.controller import SubController from pandablocks.responses import BlockInfo -from fastcs_pandablocks.types import AttrType, EpicsName, PandaName, ResponseType +from fastcs_pandablocks.types import EpicsName, PandaName, ResponseType -from .fields import FIELD_TYPE_TO_FASTCS_TYPE, FieldType +from .fields import FieldControllerType, get_field_controller_from_field_info class BlockController(SubController): - fields: dict[str, FieldType] + fields: dict[str, FieldControllerType] def __init__( self, @@ -19,30 +19,31 @@ def __init__( description: str | None | None, raw_fields: dict[str, ResponseType], ): - super().__init__() + self._additional_attributes: dict[str, Attribute] = {} self.panda_name = panda_name self.number = number self.description = description self.fields = {} for field_raw_name, field_info in raw_fields.items(): - field_panda_name = self.panda_name + PandaName(field=field_raw_name) + field_panda_name = PandaName(field=field_raw_name) + field = get_field_controller_from_field_info(field_info) + self.fields[field_panda_name.attribute_name] = field - field = FIELD_TYPE_TO_FASTCS_TYPE[field_info.type][field_info.subtype]( - # TODO make type safe after match statment - field_panda_name, - field_info, # type: ignore - ) - self.fields[field_raw_name] = field + super().__init__() def initialise(self): for field_name, field in self.fields.items(): - if field.named_attribute: - setattr(self, *field.named_attribute) - if field.sub_field_controller: - self.register_sub_controller( - field_name, sub_controller=field.sub_field_controller - ) + if field.additional_attributes: + self.register_sub_controller(field_name, sub_controller=field) + if field.top_level_attribute: + self._additional_attributes[field_name] = field.top_level_attribute + + field.initialise() + + @property + def additional_attributes(self) -> dict[str, Attribute]: + return self._additional_attributes class Blocks: @@ -97,8 +98,8 @@ def flattened_attribute_tree( yield (block.panda_name.attribute_name, block) def __getitem__( - self, name: EpicsName | PandaName - ) -> dict[int | None, BlockController] | BlockController | AttrType: + self, name: PandaName + ) -> dict[int | None, BlockController] | BlockController | Attribute: if name.block is None: raise ValueError(f"Cannot find block for name {name}.") blocks = self._blocks[name.block] @@ -109,8 +110,7 @@ def __getitem__( return block field = block.fields[name.field] if not name.sub_field: - assert field.named_attribute - return field.named_attribute.attribute + assert field.top_level_attribute + return field.top_level_attribute - sub_field = getattr(field.sub_field_controller, name.sub_field) - return sub_field + return field.additional_attributes[name.sub_field] diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 690c84e..269b15f 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -1,10 +1,8 @@ from __future__ import annotations -from collections import namedtuple -from dataclasses import dataclass from enum import Enum -from fastcs.attributes import AttrR, AttrRW, AttrW +from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.controller import SubController from fastcs.datatypes import Bool, Float, Int, String from pandablocks.responses import ( @@ -23,8 +21,8 @@ UintFieldInfo, ) -from fastcs_pandablocks.handlers import DefaultFieldSender, UpdateEguSender -from fastcs_pandablocks.types import AttrType, PandaName +from fastcs_pandablocks.handlers import UpdateEguSender +from fastcs_pandablocks.types.annotations import ResponseType class WidgetGroup(Enum): @@ -37,461 +35,452 @@ class WidgetGroup(Enum): READBACKS = "Readbacks" -class NamedAttribute(namedtuple("NamedAttribute", "attribute_name attribute")): - attribute_name: str - attribute: AttrType +class FieldController(SubController): + def __init__(self): + """ + Since fields contain an attribute for the field itself + `PREFIX:BLOCK:FIELD`, but also subfields, `PREFIX:BLOCK:FIELD:SUB_FIELD`, + have a top level attribute set in the `BlockController`, and + further attributes which are used in the field as a `SubController`. + """ + self.top_level_attribute: Attribute | None = None + self._additional_attributes = {} + super().__init__(search_device_for_attributes=False) -class SubFieldController(SubController): - def __init__(self, *attributes: NamedAttribute): + @property + def additional_attributes(self) -> dict[str, Attribute]: + return self._additional_attributes + + def initialise(self): + pass + + +class TableFieldController(FieldController): + def __init__(self, table_field_info: TableFieldInfo): super().__init__() - for attribute_name, attribute in attributes: - setattr(self, attribute_name, attribute) - - -@dataclass -class Field: - named_attribute: NamedAttribute | None - sub_field_controller: SubFieldController | None = None - - -class TableField(Field): - def __init__(self, panda_name: PandaName, table_field_info: TableFieldInfo): - super().__init__( - NamedAttribute( - panda_name.attribute_name, - AttrR( - Float(), - # To be added once we have a pvxs backend - # description=table_field_info.description, - group=WidgetGroup.OUTPUTS.value, - ), - ) + self.top_level_attribute = AttrR( + Float(), + # To be added once we have a pvxs backend + # description=table_field_info.description, + group=WidgetGroup.OUTPUTS.value, ) -class TimeField(Field): - def __init__(self, panda_name: PandaName, time_field_info: TimeFieldInfo): - time_attr = AttrRW( +class TimeFieldController(FieldController): + def __init__(self, time_field_info: TimeFieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( Float(), # To be added once we have a pvxs backend # description=time_field_info.description, group=WidgetGroup.PARAMETERS.value, ) - sub_units = NamedAttribute( - attribute_name="units", - attribute=AttrW( - String(), - handler=UpdateEguSender(time_attr), - group=WidgetGroup.PARAMETERS.value, - ), - ) - super().__init__( - NamedAttribute(panda_name.attribute_name, time_attr), - SubFieldController(sub_units), + self._additional_attributes["units"] = AttrW( + String(), + handler=UpdateEguSender(self.top_level_attribute), + group=WidgetGroup.PARAMETERS.value, ) -class TimeSubTypeParamField(Field): +class TimeSubTypeParamFieldController(FieldController): def __init__( - self, panda_name: PandaName, time_subtype_param_field_info: SubtypeTimeFieldInfo + self, + time_subtype_param_field_info: SubtypeTimeFieldInfo, ): - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, - attribute=AttrRW(Float(), group=WidgetGroup.PARAMETERS.value), - ) + super().__init__() + self.top_level_attribute = AttrRW( + Float(), + group=WidgetGroup.PARAMETERS.value, ) -class TimeSubTypeReadField(Field): +class TimeSubTypeReadFieldController(FieldController): def __init__( - self, panda_name: PandaName, time_subtype_read_field_info: SubtypeTimeFieldInfo + self, + time_subtype_read_field_info: SubtypeTimeFieldInfo, ): - time_subtype_read_field = AttrR(Float(), group=WidgetGroup.READBACKS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, - attribute=time_subtype_read_field, - ) + super().__init__() + self.top_level_attribute = AttrR( + Float(), + group=WidgetGroup.READBACKS.value, ) -class TimeSubTypeWriteField(Field): +class TimeSubTypeWriteFieldController(FieldController): def __init__( - self, panda_name: PandaName, time_subtype_write_field_info: SubtypeTimeFieldInfo + self, + time_subtype_write_field_info: SubtypeTimeFieldInfo, ): - time_subtype_write_field = AttrW(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, - attribute=time_subtype_write_field, - ) + super().__init__() + self.top_level_attribute = AttrW( + Float(), + group=WidgetGroup.OUTPUTS.value, ) -class BitOutField(Field): - def __init__(self, panda_name: PandaName, bit_out_field_info: BitOutFieldInfo): - bit = AttrRW(Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute(attribute_name=panda_name.attribute_name, attribute=bit) +class BitOutFieldController(FieldController): + def __init__(self, bit_out_field_info: BitOutFieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + Bool(znam="0", onam="1"), + group=WidgetGroup.OUTPUTS.value, ) -class PosOutField(Field): - def __init__(self, panda_name: PandaName, pos_out_field_info: PosOutFieldInfo): - # TODO add capture and dataset subfields - pos_out_attr = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=pos_out_attr - ) +class PosOutFieldController(FieldController): + def __init__(self, pos_out_field_info: PosOutFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Float(), + group=WidgetGroup.OUTPUTS.value, ) -class ExtOutField(Field): - def __init__(self, panda_name: PandaName, ext_out_field_info: ExtOutFieldInfo): - # TODO add capture and dataset subfields - ext_out_field = AttrR(Float(), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=ext_out_field - ) +class ExtOutFieldController(FieldController): + def __init__(self, ext_out_field_info: ExtOutFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Float(), + group=WidgetGroup.OUTPUTS.value, ) -class ExtOutBitsField(ExtOutField): +class ExtOutBitsFieldController(ExtOutFieldController): def __init__( - self, panda_name: PandaName, ext_out_bits_field_info: ExtOutBitsFieldInfo + self, + ext_out_bits_field_info: ExtOutBitsFieldInfo, ): - # TODO add capture and dataset subfields - super().__init__(panda_name, ext_out_bits_field_info) + super().__init__(ext_out_bits_field_info) -class BitMuxField(Field): - def __init__(self, panda_name: PandaName, bit_mux_field_info: BitMuxFieldInfo): - bit_mux_attr = AttrRW(String(), group=WidgetGroup.INPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=bit_mux_attr - ) +class BitMuxFieldController(FieldController): + def __init__(self, bit_mux_field_info: BitMuxFieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + String(), + group=WidgetGroup.INPUTS.value, ) -class PosMuxField(Field): - def __init__(self, panda_name: PandaName, pos_mux_field_info: PosMuxFieldInfo): - pos_mux_attr = AttrRW(String(), group=WidgetGroup.INPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=pos_mux_attr - ) +class PosMuxFieldController(FieldController): + def __init__(self, pos_mux_field_info: PosMuxFieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + String(), + group=WidgetGroup.INPUTS.value, ) -class UintParamField(Field): - def __init__(self, panda_name: PandaName, uint_param_field_info: UintFieldInfo): - uint_param_attr = AttrR(Float(prec=0), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=uint_param_attr - ) +class UintParamFieldController(FieldController): + def __init__(self, uint_param_field_info: UintFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Float(prec=0), + group=WidgetGroup.PARAMETERS.value, ) -class UintReadField(Field): - def __init__(self, panda_name: PandaName, uint_read_field_info: UintFieldInfo): - uint_read_attr = AttrR( +class UintReadFieldController(FieldController): + def __init__(self, uint_read_field_info: UintFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( Float(prec=0), group=WidgetGroup.READBACKS.value, # To be added once we have a pvxs backend # description=uint_read_field_info.description, ) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=uint_read_attr - ) - ) -class UintWriteField(Field): - def __init__(self, panda_name: PandaName, uint_write_field_info: UintFieldInfo): - uint_write_attr = AttrW(Float(prec=0), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=uint_write_attr - ) +class UintWriteFieldController(FieldController): + def __init__(self, uint_write_field_info: UintFieldInfo): + super().__init__() + self.top_level_attribute = AttrW( + Float(prec=0), + group=WidgetGroup.OUTPUTS.value, ) -class IntParamField(Field): - def __init__(self, panda_name: PandaName, int_param_field_info: FieldInfo): - uint_param_attr = AttrRW(Float(prec=0), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=uint_param_attr - ) +class IntParamFieldController(FieldController): + def __init__(self, int_param_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + Float(prec=0), + group=WidgetGroup.PARAMETERS.value, ) -class IntReadField(Field): - def __init__(self, panda_name: PandaName, int_read_field_info: FieldInfo): - int_read_attr = AttrR(Int(), group=WidgetGroup.READBACKS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=int_read_attr - ) +class IntReadFieldController(FieldController): + def __init__(self, int_read_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Int(), + group=WidgetGroup.READBACKS.value, ) -class IntWriteField(Field): - def __init__(self, panda_name: PandaName, int_write_field_info: FieldInfo): - int_write_attr = AttrW(Int(), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=int_write_attr - ) +class IntWriteFieldController(FieldController): + def __init__(self, int_write_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrW( + Int(), + group=WidgetGroup.PARAMETERS.value, ) -class ScalarParamField(Field): - def __init__(self, panda_name: PandaName, scalar_param_field_info: ScalarFieldInfo): - scalar_param_attr = AttrRW(Float(), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=scalar_param_attr - ) +class ScalarParamFieldController(FieldController): + def __init__(self, scalar_param_field_info: ScalarFieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + Float(), + group=WidgetGroup.PARAMETERS.value, ) -class ScalarReadField(Field): - def __init__(self, panda_name: PandaName, scalar_read_field_info: ScalarFieldInfo): - scalar_read_attr = AttrR(Float(), group=WidgetGroup.READBACKS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=scalar_read_attr - ) +class ScalarReadFieldController(FieldController): + def __init__(self, scalar_read_field_info: ScalarFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Float(), + group=WidgetGroup.READBACKS.value, ) -class ScalarWriteField(Field): - def __init__(self, panda_name: PandaName, scalar_write_field_info: ScalarFieldInfo): - scalar_read_attr = AttrR(Float(), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=scalar_read_attr - ) +class ScalarWriteFieldController(FieldController): + def __init__(self, scalar_write_field_info: ScalarFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Float(), + group=WidgetGroup.PARAMETERS.value, ) -class BitParamField(Field): - def __init__(self, panda_name: PandaName, bit_param_field_info: FieldInfo): - bit_param_attr = AttrRW( - Bool(znam="0", onam="1"), group=WidgetGroup.PARAMETERS.value - ) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=bit_param_attr - ) +class BitParamFieldController(FieldController): + def __init__(self, bit_param_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + Bool(znam="0", onam="1"), + group=WidgetGroup.PARAMETERS.value, ) -class BitReadField(Field): - def __init__(self, panda_name: PandaName, bit_read_field_info: FieldInfo): - bit_read_attr = AttrR( - Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value - ) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=bit_read_attr - ) +class BitReadFieldController(FieldController): + def __init__(self, bit_read_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Bool(znam="0", onam="1"), + group=WidgetGroup.READBACKS.value, ) -class BitWriteField(Field): - def __init__(self, panda_name: PandaName, bit_write_field_info: FieldInfo): - bit_write_attr = AttrW( - Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value - ) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=bit_write_attr - ) +class BitWriteFieldController(FieldController): + def __init__(self, bit_write_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrW( + Bool(znam="0", onam="1"), + group=WidgetGroup.OUTPUTS.value, ) -class ActionReadField(Field): - def __init__(self, panda_name: PandaName, action_read_field_info: FieldInfo): - action_read_attr = AttrR( - Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value - ) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=action_read_attr - ) +class ActionReadFieldController(FieldController): + def __init__(self, action_read_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + Bool(znam="0", onam="1"), + group=WidgetGroup.READBACKS.value, ) -class ActionWriteField(Field): - def __init__(self, panda_name: PandaName, action_write_field_info: FieldInfo): - action_write_attr = AttrW( - Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value - ) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=action_write_attr - ) +class ActionWriteFieldController(FieldController): + def __init__(self, action_write_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrW( + Bool(znam="0", onam="1"), + group=WidgetGroup.OUTPUTS.value, ) -class LutParamField(Field): - def __init__(self, panda_name: PandaName, lut_param_field_info: FieldInfo): - lut_param_field = AttrRW(String(), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=lut_param_field - ) +class LutParamFieldController(FieldController): + def __init__(self, lut_param_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrRW( + String(), + group=WidgetGroup.PARAMETERS.value, ) -class LutReadField(Field): - def __init__(self, panda_name: PandaName, lut_read_field_info: FieldInfo): - lut_read_field = AttrR(String(), group=WidgetGroup.READBACKS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=lut_read_field - ) +class LutReadFieldController(FieldController): + def __init__(self, lut_read_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + String(), + group=WidgetGroup.READBACKS.value, ) -class LutWriteField(Field): - def __init__(self, panda_name: PandaName, lut_read_field_info: FieldInfo): - lut_write_field = AttrR(String(), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=lut_write_field - ) +class LutWriteFieldController(FieldController): + def __init__(self, lut_read_field_info: FieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + String(), + group=WidgetGroup.OUTPUTS.value, ) -class EnumParamField(Field): - def __init__(self, panda_name: PandaName, enum_param_field_info: EnumFieldInfo): +class EnumParamFieldController(FieldController): + def __init__(self, enum_param_field_info: EnumFieldInfo): + super().__init__() self.allowed_values = enum_param_field_info.labels - enum_param_field = AttrRW(String(), group=WidgetGroup.PARAMETERS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=enum_param_field - ) - ) - - -class EnumReadField(Field): - def __init__(self, panda_name: PandaName, enum_read_field_info: EnumFieldInfo): - enum_read_field = AttrR(String(), group=WidgetGroup.READBACKS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=enum_read_field - ) + self.top_level_attribute = AttrRW( + String(), + group=WidgetGroup.PARAMETERS.value, ) -class EnumWriteField(Field): - def __init__(self, panda_name: PandaName, enum_write_field_info: EnumFieldInfo): - enum_write_field = AttrW(String(), group=WidgetGroup.OUTPUTS.value) - super().__init__( - NamedAttribute( - attribute_name=panda_name.attribute_name, attribute=enum_write_field - ) +class EnumReadFieldController(FieldController): + def __init__(self, enum_read_field_info: EnumFieldInfo): + super().__init__() + self.top_level_attribute = AttrR( + String(), + group=WidgetGroup.READBACKS.value, ) -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 +class EnumWriteFieldController(FieldController): + def __init__(self, enum_write_field_info: EnumFieldInfo): + super().__init__() + self.top_level_attribute = AttrW( + String(), + group=WidgetGroup.OUTPUTS.value, + ) + + +FieldControllerType = ( + TableFieldController + | TimeFieldController + | BitOutFieldController + | PosOutFieldController + | ExtOutFieldController + | ExtOutBitsFieldController + | BitMuxFieldController + | PosMuxFieldController + | UintParamFieldController + | UintReadFieldController + | UintWriteFieldController + | IntParamFieldController + | IntReadFieldController + | IntWriteFieldController + | ScalarParamFieldController + | ScalarReadFieldController + | ScalarWriteFieldController + | BitParamFieldController + | BitReadFieldController + | BitWriteFieldController + | ActionReadFieldController + | ActionWriteFieldController + | LutParamFieldController + | LutReadFieldController + | LutWriteFieldController + | EnumParamFieldController + | EnumReadFieldController + | EnumWriteFieldController + | TimeSubTypeParamFieldController + | TimeSubTypeReadFieldController + | TimeSubTypeWriteFieldController ) -# TODO: Change to a match statement so we can easily add a PCAP field type. -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, - }, -} + +def get_field_controller_from_field_info( + field_info: ResponseType, +) -> FieldControllerType: + match field_info: + case TableFieldInfo(): + return TableFieldController(field_info) + # Time types + case TimeFieldInfo(subtype=None): + return TimeFieldController(field_info) + case SubtypeTimeFieldInfo(type="param"): + return TimeSubTypeParamFieldController(field_info) + case SubtypeTimeFieldInfo(subtype="read"): + return TimeSubTypeReadFieldController(field_info) + case SubtypeTimeFieldInfo(subtype="write"): + return TimeSubTypeWriteFieldController(field_info) + + # Bit types + case BitOutFieldInfo(): + return BitOutFieldController(field_info) + case ExtOutBitsFieldInfo(subtype="timestamp"): + return ExtOutFieldController(field_info) + case ExtOutBitsFieldInfo(): + return ExtOutBitsFieldController(field_info) + case ExtOutFieldInfo(): + return ExtOutFieldController(field_info) + case BitMuxFieldInfo(): + return BitMuxFieldController(field_info) + case FieldInfo(type="param", subtype="bit"): + return BitParamFieldController(field_info) + case FieldInfo(type="read", subtype="bit"): + return BitReadFieldController(field_info) + case FieldInfo(type="write", subtype="bit"): + return BitWriteFieldController(field_info) + + # Pos types + case PosOutFieldInfo(): + return PosOutFieldController(field_info) + case PosMuxFieldInfo(): + return PosMuxFieldController(field_info) + + # Uint types + case UintFieldInfo(type="param"): + return UintParamFieldController(field_info) + case UintFieldInfo(type="read"): + return UintReadFieldController(field_info) + case UintFieldInfo(type="write"): + return UintWriteFieldController(field_info) + + # Scalar types + case ScalarFieldInfo(subtype="param"): + return ScalarParamFieldController(field_info) + case ScalarFieldInfo(type="read"): + return ScalarReadFieldController(field_info) + case ScalarFieldInfo(type="write"): + return ScalarWriteFieldController(field_info) + + # Int types + case FieldInfo(type="param", subtype="int"): + return IntParamFieldController(field_info) + case FieldInfo(type="read", subtype="int"): + return IntReadFieldController(field_info) + case FieldInfo(type="write", subtype="int"): + return IntWriteFieldController(field_info) + + # Action types + case FieldInfo( + type="read", + subtype="action", + ): + return ActionReadFieldController(field_info) + case FieldInfo( + type="write", + subtype="action", + ): + return ActionWriteFieldController(field_info) + + # Lut types + case FieldInfo(type="param", subtype="lut"): + return LutParamFieldController(field_info) + case FieldInfo(type="read", subtype="lut"): + return LutReadFieldController(field_info) + case FieldInfo(type="write", subtype="lut"): + return LutWriteFieldController(field_info) + + # Enum types + case EnumFieldInfo(type="param"): + return EnumParamFieldController(field_info) + case EnumFieldInfo(type="read"): + return EnumReadFieldController(field_info) + case EnumFieldInfo(type="write"): + return EnumWriteFieldController(field_info) + + case _: + raise ValueError(f"Unknown field type: {type(field_info).__name__}.") diff --git a/src/fastcs_pandablocks/types/__init__.py b/src/fastcs_pandablocks/types/__init__.py index dfe4411..8a1da7a 100644 --- a/src/fastcs_pandablocks/types/__init__.py +++ b/src/fastcs_pandablocks/types/__init__.py @@ -1,4 +1,4 @@ -from .annotations import AttrType, ResponseType +from .annotations import ResponseType from .string_types import ( EPICS_SEPARATOR, PANDA_SEPARATOR, @@ -11,6 +11,5 @@ "EpicsName", "PANDA_SEPARATOR", "PandaName", - "AttrType", "ResponseType", ] diff --git a/src/fastcs_pandablocks/types/annotations.py b/src/fastcs_pandablocks/types/annotations.py index 8684d03..2bc5e14 100644 --- a/src/fastcs_pandablocks/types/annotations.py +++ b/src/fastcs_pandablocks/types/annotations.py @@ -1,5 +1,5 @@ -from fastcs.attributes import AttrR, AttrRW, AttrW from typing import Union + from pandablocks.responses import ( BitMuxFieldInfo, BitOutFieldInfo, @@ -16,24 +16,20 @@ UintFieldInfo, ) - # Pyright gives us variable not allowed in type expression error # if we try to use the new (|) syntax ResponseType = Union[ BitMuxFieldInfo, BitOutFieldInfo, - EnumFieldInfo - , ExtOutBitsFieldInfo - , ExtOutFieldInfo - , FieldInfo - , PosMuxFieldInfo - , PosOutFieldInfo - , ScalarFieldInfo - , SubtypeTimeFieldInfo - , TableFieldInfo - , TimeFieldInfo - , UintFieldInfo + EnumFieldInfo, + ExtOutBitsFieldInfo, + ExtOutFieldInfo, + FieldInfo, + PosMuxFieldInfo, + PosOutFieldInfo, + ScalarFieldInfo, + SubtypeTimeFieldInfo, + TableFieldInfo, + TimeFieldInfo, + UintFieldInfo, ] - - -AttrType = Union[AttrRW, AttrR, AttrW] diff --git a/tests/test_types.py b/tests/test_types.py index 84ea15c..3a68023 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -48,7 +48,7 @@ def test_epics_name_add(): _ = EpicsName(block="block", block_number=1, field="field") + EpicsName( block="block", block_number=2, field="field" ) - assert str(error.value) == "Ambiguous pv elements on `EpicsName` add 1 and 2" + assert str(error.value) == "Ambiguous pv elements on add 1 and 2" def test_epics_name_contains(): From c419d08f80995a1b9b50b26a62f8a65483ec0240 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 23:21:06 +0000 Subject: [PATCH 10/10] Update numpy requirement Updates the requirements on [numpy](https://github.com/numpy/numpy) to permit the latest version. Updates `numpy` to 2.1.2 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v0.2.0...v2.1.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ea2cbfe..71f748f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ description = "A softioc to control a PandABlocks-FPGA." dependencies = [ "fastcs@git+https://github.com/DiamondLightSource/FastCS@panda-conversion-improvements", "pandablocks~=0.10.0", - "numpy<2", # until https://github.com/mdavidsaver/p4p/issues/145 is fixed + "numpy<3", # until https://github.com/mdavidsaver/p4p/issues/145 is fixed "pydantic>2", "h5py", ]