From 6073cfc2b2f6d5339cc27ea2eb815521c747e826 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Wed, 9 Oct 2024 11:50:41 +0100 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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 85ab10fd7b17e80afc8cd737690dbaf449ec05f7 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Tue, 29 Oct 2024 07:51:19 +0000 Subject: [PATCH 10/18] changed field object getter to a match statement --- src/fastcs_pandablocks/handlers.py | 38 ++++- src/fastcs_pandablocks/panda/blocks.py | 4 +- src/fastcs_pandablocks/panda/fields.py | 202 +++++++++++++++++++------ 3 files changed, 196 insertions(+), 48 deletions(-) diff --git a/src/fastcs_pandablocks/handlers.py b/src/fastcs_pandablocks/handlers.py index 75d46a5..62e1ca2 100644 --- a/src/fastcs_pandablocks/handlers.py +++ b/src/fastcs_pandablocks/handlers.py @@ -1,6 +1,6 @@ from typing import Any -from fastcs.attributes import Attribute, AttrW, Sender +from fastcs.attributes import Attribute, AttrR, AttrW, Handler, Sender, Updater from fastcs_pandablocks.types import PandaName @@ -13,7 +13,23 @@ async def put(self, controller: Any, attr: AttrW, value: str) -> None: await controller.put_value_to_panda(self.panda_name, value) -class UpdateEguSender(Sender): +class DefaultFieldUpdater(Updater): + #: We update the fields from the top level + update_period = float("inf") + + def __init__(self, panda_name: PandaName): + self.panda_name = panda_name + + async def update(self, controller: Any, attr: AttrR) -> None: + pass # TODO: update the attr with the value from the panda + + +class DefaultFieldHandler(DefaultFieldSender, DefaultFieldUpdater, Handler): + def __init__(self, panda_name: PandaName): + super().__init__(panda_name) + + +class EguSender(Sender): def __init__(self, attr_to_update: Attribute): """Update the attr""" self.attr_to_update = attr_to_update @@ -21,3 +37,21 @@ def __init__(self, attr_to_update: Attribute): 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 ... + + +class CaptureHandler(Handler): + update_period = float("inf") + + def __init__(self, *args, **kwargs): + pass + + async def update(self, controller: Any, attr: AttrR) -> None: ... + async def put(self, controller: Any, attr: AttrW, value: Any) -> None: ... + + +class DatasetHandler(Handler): + update_period = float("inf") + + def __init__(self, *args, **kwargs): ... # TODO: work dataset + async def update(self, controller: Any, attr: AttrR) -> None: ... + async def put(self, controller: Any, attr: AttrW, value: Any) -> None: ... diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py index 297c3d2..c582c4f 100644 --- a/src/fastcs_pandablocks/panda/blocks.py +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -26,8 +26,8 @@ def __init__( self.fields = {} for field_raw_name, field_info in raw_fields.items(): - field_panda_name = PandaName(field=field_raw_name) - field = get_field_controller_from_field_info(field_info) + field_panda_name = self.panda_name + PandaName(field=field_raw_name) + field = get_field_controller_from_field_info(field_panda_name, field_info) self.fields[field_panda_name.attribute_name] = field super().__init__() diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 269b15f..ee6f6e1 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -21,8 +21,16 @@ UintFieldInfo, ) -from fastcs_pandablocks.handlers import UpdateEguSender +from fastcs_pandablocks.handlers import ( + CaptureHandler, + DatasetHandler, + DefaultFieldHandler, + DefaultFieldSender, + DefaultFieldUpdater, + EguSender, +) from fastcs_pandablocks.types.annotations import ResponseType +from fastcs_pandablocks.types.string_types import PandaName class WidgetGroup(Enum): @@ -33,6 +41,17 @@ class WidgetGroup(Enum): OUTPUTS = "Outputs" INPUTS = "Inputs" READBACKS = "Readbacks" + CAPTURE = "Capture" + + +# EPICS hardcoded. TODO: remove once we switch to pvxs. +MAXIMUM_DESCRIPTION_LENGTH = 40 + + +def _strip_description(description: str | None) -> str: + if description is None: + return "" + return description[:MAXIMUM_DESCRIPTION_LENGTH] class FieldController(SubController): @@ -57,111 +76,206 @@ def initialise(self): class TableFieldController(FieldController): - def __init__(self, table_field_info: TableFieldInfo): + def __init__(self, panda_name: PandaName, field_info: TableFieldInfo): super().__init__() + self.top_level_attribute = AttrR( Float(), - # To be added once we have a pvxs backend - # description=table_field_info.description, + description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, ) -class TimeFieldController(FieldController): - def __init__(self, time_field_info: TimeFieldInfo): +class TimeParamFieldController(FieldController): + # TODO: these `FieldInfo` are the exact same in pandablocks-client. + def __init__( + self, + panda_name: PandaName, + field_info: SubtypeTimeFieldInfo | TimeFieldInfo, + ): super().__init__() self.top_level_attribute = AttrRW( Float(), - # To be added once we have a pvxs backend - # description=time_field_info.description, + handler=DefaultFieldHandler(panda_name), + description=_strip_description(field_info.description), group=WidgetGroup.PARAMETERS.value, ) self._additional_attributes["units"] = AttrW( String(), - handler=UpdateEguSender(self.top_level_attribute), + handler=EguSender(self.top_level_attribute), group=WidgetGroup.PARAMETERS.value, + allowed_values=field_info.units_labels, ) -class TimeSubTypeParamFieldController(FieldController): +class TimeReadFieldController(FieldController): def __init__( self, - time_subtype_param_field_info: SubtypeTimeFieldInfo, - ): - super().__init__() - self.top_level_attribute = AttrRW( - Float(), - group=WidgetGroup.PARAMETERS.value, - ) - - -class TimeSubTypeReadFieldController(FieldController): - def __init__( - self, - time_subtype_read_field_info: SubtypeTimeFieldInfo, + panda_name: PandaName, + field_info: SubtypeTimeFieldInfo, ): super().__init__() self.top_level_attribute = AttrR( Float(), - group=WidgetGroup.READBACKS.value, + handler=DefaultFieldUpdater( + panda_name=panda_name, + ), + description=_strip_description(field_info.description), + group=WidgetGroup.OUTPUTS.value, + ) + self._additional_attributes["units"] = AttrW( + String(), + handler=EguSender(self.top_level_attribute), + group=WidgetGroup.OUTPUTS.value, + allowed_values=field_info.units_labels, ) -class TimeSubTypeWriteFieldController(FieldController): +class TimeWriteFieldController(FieldController): def __init__( self, - time_subtype_write_field_info: SubtypeTimeFieldInfo, + panda_name: PandaName, + field_info: SubtypeTimeFieldInfo, ): super().__init__() self.top_level_attribute = AttrW( Float(), + handler=DefaultFieldSender(panda_name), + description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, ) + self._additional_attributes["units"] = AttrW( + String(), + handler=EguSender(self.top_level_attribute), + group=WidgetGroup.READBACKS.value, + allowed_values=field_info.units_labels, + ) class BitOutFieldController(FieldController): - def __init__(self, bit_out_field_info: BitOutFieldInfo): + def __init__(self, field_info: BitOutFieldInfo): super().__init__() - self.top_level_attribute = AttrRW( + self.top_level_attribute = AttrR( Bool(znam="0", onam="1"), + description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, ) class PosOutFieldController(FieldController): - def __init__(self, pos_out_field_info: PosOutFieldInfo): + def __init__(self, panda_name: PandaName, field_info: PosOutFieldInfo): super().__init__() - self.top_level_attribute = AttrR( + top_level_attribute = AttrR( Float(), + description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, ) + scaled = AttrR( + Float(), + group=WidgetGroup.CAPTURE.value, + description="Value with scaling applied.", + ) + + scale = AttrRW( + Float(), + group=WidgetGroup.CAPTURE.value, + handler=DefaultFieldHandler(panda_name), + ) + offset = AttrRW( + Float(), + group=WidgetGroup.CAPTURE.value, + handler=DefaultFieldHandler(panda_name), + ) + + async def updated_scaled_on_offset_change(*_): + await scaled.set(scale.get() * top_level_attribute.get() + offset.get()) + + offset.set_update_callback(updated_scaled_on_offset_change) + + self._additional_attributes.update( + {"scaled": scaled, "scale": scale, "offset": offset} + ) + + self.top_level_attribute = top_level_attribute + self._additional_attributes["capture"] = AttrRW( + String(), + group=WidgetGroup.CAPTURE.value, + handler=CaptureHandler(), + allowed_values=field_info.capture_labels, + ) + self._additional_attributes["dataset"] = AttrRW( + String(), + group=WidgetGroup.CAPTURE.value, + handler=DatasetHandler(), + allowed_values=field_info.capture_labels, + ) + class ExtOutFieldController(FieldController): - def __init__(self, ext_out_field_info: ExtOutFieldInfo): + def __init__(self, field_info: ExtOutFieldInfo): super().__init__() self.top_level_attribute = AttrR( Float(), + description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, ) + self._additional_attributes["capture"] = AttrRW( + String(), + group=WidgetGroup.CAPTURE.value, + handler=CaptureHandler(), + allowed_values=field_info.capture_labels, + ) + self._additional_attributes["dataset"] = AttrRW( + String(), + group=WidgetGroup.CAPTURE.value, + handler=DatasetHandler(), + allowed_values=field_info.capture_labels, + ) class ExtOutBitsFieldController(ExtOutFieldController): def __init__( self, - ext_out_bits_field_info: ExtOutBitsFieldInfo, + field_info: ExtOutBitsFieldInfo, ): - super().__init__(ext_out_bits_field_info) + super().__init__(field_info) + + for bit_number, label in enumerate(field_info.bits, start=1): + if label == "": + continue # Some rows are empty, do not create records. + + self._additional_attributes[f"val{bit_number}"] = AttrR( + Bool(znam="0", onam="1"), + description="Value of the field connected to this bit.", + group=WidgetGroup.OUTPUTS.value, + ) + self._additional_attributes[f"name{bit_number}"] = AttrR( + Bool(znam="0", onam="1"), + description="Value of the field connected to this bit.", + group=WidgetGroup.OUTPUTS.value, + ) class BitMuxFieldController(FieldController): - def __init__(self, bit_mux_field_info: BitMuxFieldInfo): + def __init__(self, panda_name: PandaName, bit_mux_field_info: BitMuxFieldInfo): super().__init__() self.top_level_attribute = AttrRW( String(), + description=_strip_description(bit_mux_field_info.description), + handler=DefaultFieldHandler(panda_name), group=WidgetGroup.INPUTS.value, ) + self._additional_attributes["delay"] = AttrRW( + Float(), + description="Clock delay on input.", + handler=DefaultFieldHandler(panda_name), + group=WidgetGroup.INPUTS.value, + ) + + # TODO: Add DRVL DRVH to `delay`. + class PosMuxFieldController(FieldController): def __init__(self, pos_mux_field_info: PosMuxFieldInfo): @@ -357,7 +471,6 @@ def __init__(self, enum_write_field_info: EnumFieldInfo): FieldControllerType = ( TableFieldController - | TimeFieldController | BitOutFieldController | PosOutFieldController | ExtOutFieldController @@ -384,27 +497,28 @@ def __init__(self, enum_write_field_info: EnumFieldInfo): | EnumParamFieldController | EnumReadFieldController | EnumWriteFieldController - | TimeSubTypeParamFieldController - | TimeSubTypeReadFieldController - | TimeSubTypeWriteFieldController + | TimeParamFieldController + | TimeReadFieldController + | TimeWriteFieldController ) def get_field_controller_from_field_info( + panda_name: PandaName, field_info: ResponseType, ) -> FieldControllerType: match field_info: case TableFieldInfo(): - return TableFieldController(field_info) + return TableFieldController(panda_name, field_info) # Time types case TimeFieldInfo(subtype=None): - return TimeFieldController(field_info) + return TimeParamFieldController(panda_name, field_info) case SubtypeTimeFieldInfo(type="param"): - return TimeSubTypeParamFieldController(field_info) + return TimeParamFieldController(panda_name, field_info) case SubtypeTimeFieldInfo(subtype="read"): - return TimeSubTypeReadFieldController(field_info) + return TimeReadFieldController(panda_name, field_info) case SubtypeTimeFieldInfo(subtype="write"): - return TimeSubTypeWriteFieldController(field_info) + return TimeWriteFieldController(panda_name, field_info) # Bit types case BitOutFieldInfo(): @@ -426,7 +540,7 @@ def get_field_controller_from_field_info( # Pos types case PosOutFieldInfo(): - return PosOutFieldController(field_info) + return PosOutFieldController(panda_name, field_info) case PosMuxFieldInfo(): return PosMuxFieldController(field_info) From 139b705cac3c8323ca953d0f73f75dec0e01fe2c Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 31 Oct 2024 10:25:54 +0000 Subject: [PATCH 11/18] parsed initial values and metadata Using them in fields is still TODO, but we'll fill out the rest of the fields first. --- src/fastcs_pandablocks/panda/blocks.py | 136 +++++++++++------- .../panda/client_wrapper.py | 92 ++++++------ src/fastcs_pandablocks/panda/controller.py | 7 +- src/fastcs_pandablocks/panda/fields.py | 41 ++++-- src/fastcs_pandablocks/types/__init__.py | 10 +- src/fastcs_pandablocks/types/annotations.py | 7 + src/fastcs_pandablocks/types/string_types.py | 14 +- 7 files changed, 190 insertions(+), 117 deletions(-) diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py index c582c4f..2ce23e9 100644 --- a/src/fastcs_pandablocks/panda/blocks.py +++ b/src/fastcs_pandablocks/panda/blocks.py @@ -2,44 +2,76 @@ from fastcs.attributes import Attribute, 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 ( + EpicsName, + PandaName, + RawBlocksType, + RawFieldsType, + RawInitialValuesType, +) +from fastcs_pandablocks.types.annotations import ResponseType -from .fields import FieldControllerType, get_field_controller_from_field_info +from .fields import ( + FieldControllerType, + get_field_controller_from_field_info, +) + + +def _def_pop_up_to_block_or_field(name: PandaName, dictionary: RawInitialValuesType): + extracted_members = {} + resolution_method = ( + PandaName.up_to_block if name.field is None else PandaName.up_to_field + ) + + # So the dictionary can be changed during iteration. + for sub_name in list(dictionary): + if resolution_method(sub_name) == name: + extracted_members[sub_name] = dictionary.pop(sub_name) + return extracted_members class BlockController(SubController): - fields: dict[str, FieldControllerType] + fields: dict[PandaName, FieldControllerType] def __init__( self, panda_name: PandaName, - number: int | None, description: str | None | None, - raw_fields: dict[str, ResponseType], + field_infos: dict[PandaName, ResponseType], + initial_values: RawInitialValuesType, + label: str | None, ): - self._additional_attributes: dict[str, Attribute] = {} self.panda_name = panda_name - self.number = number self.description = description - self.fields = {} + self.label = label - for field_raw_name, field_info in raw_fields.items(): - field_panda_name = self.panda_name + PandaName(field=field_raw_name) - field = get_field_controller_from_field_info(field_panda_name, field_info) - self.fields[field_panda_name.attribute_name] = field + self._additional_attributes: dict[str, Attribute] = {} + self.fields: dict[PandaName, FieldControllerType] = {} + + for field_name, field_info in field_infos.items(): + field_name = panda_name + field_name + field_initial_values = _def_pop_up_to_block_or_field( + field_name, initial_values + ) + self.fields[field_name] = get_field_controller_from_field_info( + field_name, field_info, field_initial_values, label + ) super().__init__() def initialise(self): for field_name, field in self.fields.items(): if field.additional_attributes: - self.register_sub_controller(field_name, sub_controller=field) + self.register_sub_controller( + field_name.attribute_name, sub_controller=field + ) + field.initialise() # Registers `field.sub_contollers`. if field.top_level_attribute: - self._additional_attributes[field_name] = field.top_level_attribute - - field.initialise() + assert field_name.field is not None + self._additional_attributes[field_name.field] = ( + field.top_level_attribute + ) @property def additional_attributes(self) -> dict[str, Attribute]: @@ -47,38 +79,46 @@ def additional_attributes(self) -> dict[str, Attribute]: class Blocks: - _blocks: dict[str, dict[int | None, BlockController]] + _blocks: dict[PandaName, BlockController] epics_prefix: EpicsName def __init__(self): self._blocks = {} def parse_introspected_data( - self, blocks: dict[str, BlockInfo], fields: list[dict[str, ResponseType]] + self, + blocks: RawBlocksType, + field_infos: RawFieldsType, + labels: RawInitialValuesType, + initial_values: RawInitialValuesType, ): self._blocks = {} - for (block_name, block_info), raw_fields in zip( - blocks.items(), fields, strict=True + for (block_name, block_info), field_info in zip( + blocks.items(), field_infos, strict=True ): - iterator = ( - range(1, block_info.number + 1) - if block_info.number > 1 - else iter( - [ - None, - ] - ) + numbered_block_names = ( + [block_name] + if block_info.number in (None, 1) + else [ + block_name + PandaName(block_number=number) + for number in range(1, block_info.number + 1) + ] ) - self._blocks[block_name] = { - number: BlockController( - PandaName(block=block_name, block_number=number), - block_info.number, + + for numbered_block_name in numbered_block_names: + block_initial_values = _def_pop_up_to_block_or_field( + numbered_block_name, initial_values + ) + label = labels.get(numbered_block_name, None) + + self._blocks[numbered_block_name] = BlockController( + numbered_block_name, block_info.description, - raw_fields, + field_info, + block_initial_values, + label, ) - for number in iterator - } async def update_field_value(self, panda_name: PandaName, value: str): attribute = self[panda_name] @@ -93,24 +133,14 @@ async def update_field_value(self, panda_name: PandaName, value: str): def flattened_attribute_tree( 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: 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] - if name.block_number is None: - return blocks - block = blocks[name.block_number] + for block in self._blocks.values(): + yield (block.panda_name.attribute_name, block) + + def __getitem__(self, name: PandaName) -> BlockController | Attribute | None: + block = self._blocks[name.up_to_block()] if name.field is None: return block - field = block.fields[name.field] - if not name.sub_field: - assert field.top_level_attribute + field = block.fields[name.up_to_field()] + if name.sub_field is None: return field.top_level_attribute - return field.additional_attributes[name.sub_field] diff --git a/src/fastcs_pandablocks/panda/client_wrapper.py b/src/fastcs_pandablocks/panda/client_wrapper.py index 331ce60..067cf76 100644 --- a/src/fastcs_pandablocks/panda/client_wrapper.py +++ b/src/fastcs_pandablocks/panda/client_wrapper.py @@ -12,17 +12,16 @@ GetFieldInfo, Put, ) -from pandablocks.responses import ( - BlockInfo, -) -from fastcs_pandablocks.types import ResponseType +from fastcs_pandablocks.types import ( + RawBlocksType, + RawFieldsType, + RawInitialValuesType, +) +from fastcs_pandablocks.types.string_types import PandaName class RawPanda: - 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, hostname: str): @@ -30,54 +29,61 @@ def __init__(self, hostname: str): 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, 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 - - for field_name, value in initial_values.items(): + async def introspect( + self, + ) -> tuple[ + RawBlocksType, RawFieldsType, RawInitialValuesType, RawInitialValuesType + ]: + self.changes = {} + blocks, fields, labels, initial_values = {}, [], {}, {} + + blocks = { + PandaName.from_string(name): block_info + for name, block_info in (await self._client.send(GetBlockInfo())).items() + } + fields = [ + { + PandaName(field=name): field_info + for name, field_info in block_values.items() + } + for block_values in await asyncio.gather( + *[self._client.send(GetFieldInfo(str(block))) for block in blocks] + ) + ] + + field_data = (await self._client.send(GetChanges(ChangeGroup.ALL, True))).values + + for field_name, value in field_data.items(): if field_name.startswith("*METADATA"): - self.metadata[field_name] = value - else: - self.changes[field_name] = value + field_name_without_prefix = field_name.removeprefix("*METADATA.") + if field_name_without_prefix == "DESIGN": + continue # TODO: Handle design. + elif not field_name_without_prefix.startswith("LABEL_"): + raise TypeError( + "Received metadata not corresponding to a `LABEL_`: " + f"{field_name} = {value}." + ) + labels[ + PandaName.from_string( + field_name_without_prefix.removeprefix("LABEL_") + ) + ] = value + else: # Field is a default value + initial_values[PandaName.from_string(field_name)] = value + + return blocks, fields, labels, initial_values async def send(self, name: str, value: str): await self._client.send(Put(name, value)) async def get_changes(self): - if not self.changes: + if self.changes is None: raise RuntimeError("Panda not introspected.") self.changes = ( await self._client.send(GetChanges(ChangeGroup.ALL, False)) ).values - - async def _ensure_connected(self): - if not self.blocks: - await self.connect() - - async def __aenter__(self): - await self._ensure_connected() - return self - - async def __aexit__(self, exc_type, exc, tb): - await self.disconnect() - - def __aiter__(self): - return self - - async def __anext__(self): - return await self.get_changes() diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py index 1d37db7..b0092da 100644 --- a/src/fastcs_pandablocks/panda/controller.py +++ b/src/fastcs_pandablocks/panda/controller.py @@ -22,12 +22,9 @@ async def connect(self) -> None: return await self._raw_panda.connect() + blocks, fields, labels, initial_values = await self._raw_panda.introspect() - assert self._raw_panda.blocks - assert self._raw_panda.fields - self._blocks.parse_introspected_data( - self._raw_panda.blocks, self._raw_panda.fields - ) + self._blocks.parse_introspected_data(blocks, fields, labels, initial_values) self.is_connected = True async def initialise(self) -> None: diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index ee6f6e1..99c20bb 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -29,7 +29,7 @@ DefaultFieldUpdater, EguSender, ) -from fastcs_pandablocks.types.annotations import ResponseType +from fastcs_pandablocks.types.annotations import RawInitialValuesType, ResponseType from fastcs_pandablocks.types.string_types import PandaName @@ -65,6 +65,7 @@ def __init__(self): self.top_level_attribute: Attribute | None = None self._additional_attributes = {} + self.sub_controllers: dict[str, FieldController] = {} super().__init__(search_device_for_attributes=False) @property @@ -72,7 +73,12 @@ def additional_attributes(self) -> dict[str, Attribute]: return self._additional_attributes def initialise(self): - pass + for sub_field_name, sub_field_controller in self.sub_controllers.items(): + self.register_sub_controller(sub_field_name, sub_field_controller) + sub_field_controller.initialise() + self._additional_attributes[sub_field_name] = ( + sub_field_controller.top_level_attribute + ) class TableFieldController(FieldController): @@ -234,6 +240,22 @@ def __init__(self, field_info: ExtOutFieldInfo): ) +class _BitsSubFieldController(FieldController): + def __init__(self, label: str): + super().__init__() + + self.top_level_attribute = AttrR( + Bool(znam="0", onam="1"), + description=_strip_description("Value of the field connected to this bit."), + group=WidgetGroup.OUTPUTS.value, + ) + self._additional_attributes["NAME"] = AttrR( + String(), + description="Name of the field connected to this bit.", + initial_value=label, + ) + + class ExtOutBitsFieldController(ExtOutFieldController): def __init__( self, @@ -245,16 +267,7 @@ def __init__( if label == "": continue # Some rows are empty, do not create records. - self._additional_attributes[f"val{bit_number}"] = AttrR( - Bool(znam="0", onam="1"), - description="Value of the field connected to this bit.", - group=WidgetGroup.OUTPUTS.value, - ) - self._additional_attributes[f"name{bit_number}"] = AttrR( - Bool(znam="0", onam="1"), - description="Value of the field connected to this bit.", - group=WidgetGroup.OUTPUTS.value, - ) + self.sub_controllers[f"BIT{bit_number}"] = _BitsSubFieldController(label) class BitMuxFieldController(FieldController): @@ -506,6 +519,8 @@ def __init__(self, enum_write_field_info: EnumFieldInfo): def get_field_controller_from_field_info( panda_name: PandaName, field_info: ResponseType, + initial_values: RawInitialValuesType, + label: str | None, ) -> FieldControllerType: match field_info: case TableFieldInfo(): @@ -530,7 +545,7 @@ def get_field_controller_from_field_info( case ExtOutFieldInfo(): return ExtOutFieldController(field_info) case BitMuxFieldInfo(): - return BitMuxFieldController(field_info) + return BitMuxFieldController(panda_name, field_info) case FieldInfo(type="param", subtype="bit"): return BitParamFieldController(field_info) case FieldInfo(type="read", subtype="bit"): diff --git a/src/fastcs_pandablocks/types/__init__.py b/src/fastcs_pandablocks/types/__init__.py index 8a1da7a..b124a72 100644 --- a/src/fastcs_pandablocks/types/__init__.py +++ b/src/fastcs_pandablocks/types/__init__.py @@ -1,4 +1,9 @@ -from .annotations import ResponseType +from .annotations import ( + RawBlocksType, + RawFieldsType, + RawInitialValuesType, + ResponseType, +) from .string_types import ( EPICS_SEPARATOR, PANDA_SEPARATOR, @@ -12,4 +17,7 @@ "PANDA_SEPARATOR", "PandaName", "ResponseType", + "RawBlocksType", + "RawFieldsType", + "RawInitialValuesType", ] diff --git a/src/fastcs_pandablocks/types/annotations.py b/src/fastcs_pandablocks/types/annotations.py index 2bc5e14..aca1cab 100644 --- a/src/fastcs_pandablocks/types/annotations.py +++ b/src/fastcs_pandablocks/types/annotations.py @@ -3,6 +3,7 @@ from pandablocks.responses import ( BitMuxFieldInfo, BitOutFieldInfo, + BlockInfo, EnumFieldInfo, ExtOutBitsFieldInfo, ExtOutFieldInfo, @@ -16,6 +17,8 @@ UintFieldInfo, ) +from .string_types import PandaName + # Pyright gives us variable not allowed in type expression error # if we try to use the new (|) syntax ResponseType = Union[ @@ -33,3 +36,7 @@ TimeFieldInfo, UintFieldInfo, ] + +RawBlocksType = dict[PandaName, BlockInfo] +RawFieldsType = list[dict[PandaName, ResponseType]] +RawInitialValuesType = dict[PandaName, str] diff --git a/src/fastcs_pandablocks/types/string_types.py b/src/fastcs_pandablocks/types/string_types.py index 20e6023..1a2482b 100644 --- a/src/fastcs_pandablocks/types/string_types.py +++ b/src/fastcs_pandablocks/types/string_types.py @@ -64,6 +64,12 @@ class PandaName: field: str | None = None sub_field: str | None = None + def up_to_block(self) -> PandaName: + return PandaName(block=self.block, block_number=self.block_number) + + def up_to_field(self) -> PandaName: + return self.up_to_block() + PandaName(field=self.field) + @cached_property def _string_form(self) -> str: return _format_with_separator( @@ -77,9 +83,13 @@ def __str__(self) -> str: def from_string(cls, name: str): split_name = name.split(PANDA_SEPARATOR) + if split_name == [""]: + return PandaName() + + block, block_number, field, sub_field = None, None, None, None 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 + field = split_name[1] if len(split_name) > 1 else None + sub_field = split_name[2] if len(split_name) > 2 else None return PandaName( block=block, block_number=block_number, field=field, sub_field=sub_field From 030a2e14c23e0df3cce3a2acee09f82341ae8caf Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Thu, 31 Oct 2024 15:20:22 +0000 Subject: [PATCH 12/18] merged blocks and fields into a single recursive controller --- src/fastcs_pandablocks/__init__.py | 5 +- src/fastcs_pandablocks/__main__.py | 3 +- src/fastcs_pandablocks/panda/blocks.py | 146 ------- .../panda/client_wrapper.py | 14 +- src/fastcs_pandablocks/panda/controller.py | 109 ++++- src/fastcs_pandablocks/panda/fields.py | 408 ++++++++++++------ src/fastcs_pandablocks/types/__init__.py | 19 +- .../types/{annotations.py => _annotations.py} | 2 +- .../{string_types.py => _string_types.py} | 98 +---- tests/test_types.py | 66 --- 10 files changed, 395 insertions(+), 475 deletions(-) delete mode 100644 src/fastcs_pandablocks/panda/blocks.py rename src/fastcs_pandablocks/types/{annotations.py => _annotations.py} (95%) rename src/fastcs_pandablocks/types/{string_types.py => _string_types.py} (54%) delete mode 100644 tests/test_types.py diff --git a/src/fastcs_pandablocks/__init__.py b/src/fastcs_pandablocks/__init__.py index 4f6a708..2c02802 100644 --- a/src/fastcs_pandablocks/__init__.py +++ b/src/fastcs_pandablocks/__init__.py @@ -10,13 +10,12 @@ from ._version import __version__ from .gui import PandaGUIOptions from .panda.controller import PandaController -from .types import EpicsName DEFAULT_POLL_PERIOD = 0.1 def ioc( - epics_prefix: EpicsName, + epics_prefix: str, hostname: str, screens_directory: Path | None = None, clear_bobfiles: bool = False, @@ -31,7 +30,7 @@ def ioc( controller = PandaController(hostname, poll_period) backend = EpicsBackend( - controller, pv_prefix=str(epics_prefix), ioc_options=epics_ioc_options + controller, pv_prefix=epics_prefix, ioc_options=epics_ioc_options ) if clear_bobfiles and not screens_directory: diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index a0c4ae3..4a5fd3d 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -7,7 +7,6 @@ from fastcs.backends.epics.util import PvNamingConvention from fastcs_pandablocks import DEFAULT_POLL_PERIOD, ioc -from fastcs_pandablocks.types import EpicsName from . import __version__ @@ -83,7 +82,7 @@ def main(): logging.basicConfig(format="%(levelname)s:%(message)s", level=level) ioc( - EpicsName(prefix=parsed_args.prefix), + parsed_args.prefix, parsed_args.hostname, screens_directory=Path(parsed_args.screens_dir), clear_bobfiles=parsed_args.clear_bobfiles, diff --git a/src/fastcs_pandablocks/panda/blocks.py b/src/fastcs_pandablocks/panda/blocks.py deleted file mode 100644 index 2ce23e9..0000000 --- a/src/fastcs_pandablocks/panda/blocks.py +++ /dev/null @@ -1,146 +0,0 @@ -from collections.abc import Generator - -from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW -from fastcs.controller import SubController - -from fastcs_pandablocks.types import ( - EpicsName, - PandaName, - RawBlocksType, - RawFieldsType, - RawInitialValuesType, -) -from fastcs_pandablocks.types.annotations import ResponseType - -from .fields import ( - FieldControllerType, - get_field_controller_from_field_info, -) - - -def _def_pop_up_to_block_or_field(name: PandaName, dictionary: RawInitialValuesType): - extracted_members = {} - resolution_method = ( - PandaName.up_to_block if name.field is None else PandaName.up_to_field - ) - - # So the dictionary can be changed during iteration. - for sub_name in list(dictionary): - if resolution_method(sub_name) == name: - extracted_members[sub_name] = dictionary.pop(sub_name) - return extracted_members - - -class BlockController(SubController): - fields: dict[PandaName, FieldControllerType] - - def __init__( - self, - panda_name: PandaName, - description: str | None | None, - field_infos: dict[PandaName, ResponseType], - initial_values: RawInitialValuesType, - label: str | None, - ): - self.panda_name = panda_name - self.description = description - self.label = label - - self._additional_attributes: dict[str, Attribute] = {} - self.fields: dict[PandaName, FieldControllerType] = {} - - for field_name, field_info in field_infos.items(): - field_name = panda_name + field_name - field_initial_values = _def_pop_up_to_block_or_field( - field_name, initial_values - ) - self.fields[field_name] = get_field_controller_from_field_info( - field_name, field_info, field_initial_values, label - ) - - super().__init__() - - def initialise(self): - for field_name, field in self.fields.items(): - if field.additional_attributes: - self.register_sub_controller( - field_name.attribute_name, sub_controller=field - ) - field.initialise() # Registers `field.sub_contollers`. - if field.top_level_attribute: - assert field_name.field is not None - self._additional_attributes[field_name.field] = ( - field.top_level_attribute - ) - - @property - def additional_attributes(self) -> dict[str, Attribute]: - return self._additional_attributes - - -class Blocks: - _blocks: dict[PandaName, BlockController] - epics_prefix: EpicsName - - def __init__(self): - self._blocks = {} - - def parse_introspected_data( - self, - blocks: RawBlocksType, - field_infos: RawFieldsType, - labels: RawInitialValuesType, - initial_values: RawInitialValuesType, - ): - self._blocks = {} - - for (block_name, block_info), field_info in zip( - blocks.items(), field_infos, strict=True - ): - numbered_block_names = ( - [block_name] - if block_info.number in (None, 1) - else [ - block_name + PandaName(block_number=number) - for number in range(1, block_info.number + 1) - ] - ) - - for numbered_block_name in numbered_block_names: - block_initial_values = _def_pop_up_to_block_or_field( - numbered_block_name, initial_values - ) - label = labels.get(numbered_block_name, None) - - self._blocks[numbered_block_name] = BlockController( - numbered_block_name, - block_info.description, - field_info, - block_initial_values, - label, - ) - - async def update_field_value(self, panda_name: PandaName, value: str): - 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, BlockController], None, None]: - for block in self._blocks.values(): - yield (block.panda_name.attribute_name, block) - - def __getitem__(self, name: PandaName) -> BlockController | Attribute | None: - block = self._blocks[name.up_to_block()] - if name.field is None: - return block - field = block.fields[name.up_to_field()] - if name.sub_field is None: - return field.top_level_attribute - return field.additional_attributes[name.sub_field] diff --git a/src/fastcs_pandablocks/panda/client_wrapper.py b/src/fastcs_pandablocks/panda/client_wrapper.py index 067cf76..6f39755 100644 --- a/src/fastcs_pandablocks/panda/client_wrapper.py +++ b/src/fastcs_pandablocks/panda/client_wrapper.py @@ -14,16 +14,14 @@ ) from fastcs_pandablocks.types import ( + PandaName, RawBlocksType, RawFieldsType, RawInitialValuesType, ) -from fastcs_pandablocks.types.string_types import PandaName class RawPanda: - changes: dict[str, str] | None = None - def __init__(self, hostname: str): self._client = AsyncioClient(host=hostname) @@ -32,14 +30,12 @@ async def connect(self): async def disconnect(self): await self._client.close() - self.changes = None async def introspect( self, ) -> tuple[ RawBlocksType, RawFieldsType, RawInitialValuesType, RawInitialValuesType ]: - self.changes = {} blocks, fields, labels, initial_values = {}, [], {}, {} blocks = { @@ -81,9 +77,5 @@ async def introspect( async def send(self, name: str, value: str): await self._client.send(Put(name, value)) - async def get_changes(self): - if self.changes is None: - raise RuntimeError("Panda not introspected.") - self.changes = ( - await self._client.send(GetChanges(ChangeGroup.ALL, False)) - ).values + async def get_changes(self) -> dict[str, str]: + return (await self._client.send(GetChanges(ChangeGroup.ALL, False))).values diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py index b0092da..b717400 100644 --- a/src/fastcs_pandablocks/panda/controller.py +++ b/src/fastcs_pandablocks/panda/controller.py @@ -1,49 +1,118 @@ import asyncio +from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.controller import Controller from fastcs.wrappers import scan -from fastcs_pandablocks.types import PandaName +from fastcs_pandablocks.types import ( + PandaName, + RawBlocksType, + RawFieldsType, + RawInitialValuesType, +) -from .blocks import Blocks from .client_wrapper import RawPanda +from .fields import FieldController + + +def _parse_introspected_data( + raw_blocks: RawBlocksType, + raw_field_infos: RawFieldsType, + raw_labels: RawInitialValuesType, + raw_initial_values: RawInitialValuesType, +): + block_controllers: dict[PandaName, FieldController] = {} + for (block_name, block_info), field_info in zip( + raw_blocks.items(), raw_field_infos, strict=True + ): + numbered_block_names = ( + [block_name] + if block_info.number in (None, 1) + else [ + block_name + PandaName(block_number=number) + for number in range(1, block_info.number + 1) + ] + ) + for numbered_block_name in numbered_block_names: + block_initial_values = { + key: value + for key, value in raw_initial_values.items() + if key in numbered_block_name + } + label = raw_labels.get(numbered_block_name, None) + block = FieldController( + numbered_block_name, + label=block_info.description or label, + ) + block.make_sub_fields(field_info, block_initial_values) + block_controllers[numbered_block_name] = block + + return block_controllers class PandaController(Controller): def __init__(self, hostname: str, poll_period: float) -> None: + # TODO https://github.com/DiamondLightSource/FastCS/issues/62 + self.poll_period = poll_period + + self._additional_attributes: dict[str, Attribute] = {} self._raw_panda = RawPanda(hostname) - self._blocks = Blocks() - self.is_connected = False + self._blocks: dict[PandaName, FieldController] = {} super().__init__() - async def connect(self) -> None: - if self.is_connected: - return + @property + def additional_attributes(self): + return self._additional_attributes + async def connect(self) -> None: await self._raw_panda.connect() blocks, fields, labels, initial_values = await self._raw_panda.introspect() - - self._blocks.parse_introspected_data(blocks, fields, labels, initial_values) - self.is_connected = True + self._blocks = _parse_introspected_data(blocks, fields, labels, initial_values) async def initialise(self) -> None: await self.connect() + for block_name, block in self._blocks.items(): + if block.top_level_attribute is not None: + self._additional_attributes[block_name.attribute_name] = ( + block.top_level_attribute + ) + if block.additional_attributes or block.sub_fields: + self.register_sub_controller(block_name.attribute_name, block) + await block.initialise() + + def get_attribute(self, panda_name: PandaName) -> Attribute: + assert panda_name.block + block_controller = self._blocks[panda_name.up_to_block()] + if panda_name.field is None: + assert block_controller.top_level_attribute is not None + return block_controller.top_level_attribute + + field_controller = block_controller.sub_fields[panda_name.up_to_field()] + if panda_name.sub_field is None: + assert field_controller.top_level_attribute is not None + return field_controller.top_level_attribute - for attr_name, controller in self._blocks.flattened_attribute_tree(): - self.register_sub_controller(attr_name, controller) - controller.initialise() + sub_field_controller = field_controller.sub_fields[panda_name] + assert sub_field_controller.top_level_attribute is not None + return sub_field_controller.top_level_attribute + + async def update_field_value(self, panda_name: PandaName, value: str): + attribute = self.get_attribute(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}.") - # 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 + changes = await self._raw_panda.get_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() + self.update_field_value(PandaName.from_string(raw_panda_name), value) + for raw_panda_name, value in changes.items() ] ) diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 99c20bb..d1406f2 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -1,7 +1,5 @@ from __future__ import annotations -from enum import Enum - from fastcs.attributes import Attribute, AttrR, AttrRW, AttrW from fastcs.controller import SubController from fastcs.datatypes import Bool, Float, Int, String @@ -29,61 +27,83 @@ DefaultFieldUpdater, EguSender, ) -from fastcs_pandablocks.types.annotations import RawInitialValuesType, ResponseType -from fastcs_pandablocks.types.string_types import PandaName - - -class WidgetGroup(Enum): - """Purposely not an enum since we only ever want the string.""" - - NONE = None - PARAMETERS = "Parameters" - OUTPUTS = "Outputs" - INPUTS = "Inputs" - READBACKS = "Readbacks" - CAPTURE = "Capture" - +from fastcs_pandablocks.types import ( + PandaName, + RawInitialValuesType, + ResponseType, + WidgetGroup, +) # EPICS hardcoded. TODO: remove once we switch to pvxs. MAXIMUM_DESCRIPTION_LENGTH = 40 -def _strip_description(description: str | None) -> str: +def _strip_description(description: str | None) -> str | None: if description is None: - return "" + return description return description[:MAXIMUM_DESCRIPTION_LENGTH] 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`. - """ - + def __init__( + self, + panda_name: PandaName, + label: str | None = None, + ): + self.panda_name = panda_name self.top_level_attribute: Attribute | None = None - self._additional_attributes = {} - self.sub_controllers: dict[str, FieldController] = {} + + # Sub fields eg `PGEN.OUT` and `PGEN.TRIGGER` + self.sub_fields: dict[PandaName, FieldController] = {} + + self._additional_attributes: dict[str, Attribute] = {} + super().__init__(search_device_for_attributes=False) + def make_sub_fields( + self, + field_infos: dict[PandaName, ResponseType], + initial_values: RawInitialValuesType, + ): + for sub_field_name, field_info in field_infos.items(): + full_sub_field_name = self.panda_name + sub_field_name + field_initial_values = { + key: value + for key, value in initial_values.items() + if key in sub_field_name + } + self.sub_fields[full_sub_field_name] = get_field_controller_from_field_info( + full_sub_field_name, field_info, field_initial_values + ) + + async def initialise(self): + for field_name, field in self.sub_fields.items(): + self.register_sub_controller( + field_name.attribute_name, sub_controller=field + ) + await field.initialise() + if field.top_level_attribute: + self._additional_attributes[field_name.attribute_name] = ( + field.top_level_attribute + ) + @property def additional_attributes(self) -> dict[str, Attribute]: + """ + Used by the FastCS mapping parser to get attributes since + we're not searching for device attributes. + """ return self._additional_attributes - def initialise(self): - for sub_field_name, sub_field_controller in self.sub_controllers.items(): - self.register_sub_controller(sub_field_name, sub_field_controller) - sub_field_controller.initialise() - self._additional_attributes[sub_field_name] = ( - sub_field_controller.top_level_attribute - ) - class TableFieldController(FieldController): - def __init__(self, panda_name: PandaName, field_info: TableFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + field_info: TableFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name, field_info.description) self.top_level_attribute = AttrR( Float(), @@ -98,8 +118,9 @@ def __init__( self, panda_name: PandaName, field_info: SubtypeTimeFieldInfo | TimeFieldInfo, + initial_values: RawInitialValuesType, ): - super().__init__() + super().__init__(panda_name) self.top_level_attribute = AttrRW( Float(), handler=DefaultFieldHandler(panda_name), @@ -120,7 +141,7 @@ def __init__( panda_name: PandaName, field_info: SubtypeTimeFieldInfo, ): - super().__init__() + super().__init__(panda_name) self.top_level_attribute = AttrR( Float(), handler=DefaultFieldUpdater( @@ -143,7 +164,7 @@ def __init__( panda_name: PandaName, field_info: SubtypeTimeFieldInfo, ): - super().__init__() + super().__init__(panda_name) self.top_level_attribute = AttrW( Float(), handler=DefaultFieldSender(panda_name), @@ -159,8 +180,13 @@ def __init__( class BitOutFieldController(FieldController): - def __init__(self, field_info: BitOutFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + field_info: BitOutFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Bool(znam="0", onam="1"), description=_strip_description(field_info.description), @@ -169,8 +195,13 @@ def __init__(self, field_info: BitOutFieldInfo): class PosOutFieldController(FieldController): - def __init__(self, panda_name: PandaName, field_info: PosOutFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + field_info: PosOutFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) top_level_attribute = AttrR( Float(), description=_strip_description(field_info.description), @@ -219,8 +250,14 @@ async def updated_scaled_on_offset_change(*_): class ExtOutFieldController(FieldController): - def __init__(self, field_info: ExtOutFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + field_info: ExtOutFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) + self.top_level_attribute = AttrR( Float(), description=_strip_description(field_info.description), @@ -241,8 +278,8 @@ def __init__(self, field_info: ExtOutFieldInfo): class _BitsSubFieldController(FieldController): - def __init__(self, label: str): - super().__init__() + def __init__(self, panda_name: PandaName, label: str): + super().__init__(panda_name, label=label) self.top_level_attribute = AttrR( Bool(znam="0", onam="1"), @@ -259,20 +296,30 @@ def __init__(self, label: str): class ExtOutBitsFieldController(ExtOutFieldController): def __init__( self, + panda_name: PandaName, field_info: ExtOutBitsFieldInfo, + initial_values: RawInitialValuesType, ): - super().__init__(field_info) + super().__init__(panda_name, field_info, initial_values) for bit_number, label in enumerate(field_info.bits, start=1): if label == "": continue # Some rows are empty, do not create records. - self.sub_controllers[f"BIT{bit_number}"] = _BitsSubFieldController(label) + sub_field_panda_name = panda_name + PandaName(sub_field=f"bit{bit_number}") + self.sub_fields[sub_field_panda_name] = _BitsSubFieldController( + sub_field_panda_name, label=label + ) class BitMuxFieldController(FieldController): - def __init__(self, panda_name: PandaName, bit_mux_field_info: BitMuxFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + bit_mux_field_info: BitMuxFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrRW( String(), description=_strip_description(bit_mux_field_info.description), @@ -291,8 +338,13 @@ def __init__(self, panda_name: PandaName, bit_mux_field_info: BitMuxFieldInfo): class PosMuxFieldController(FieldController): - def __init__(self, pos_mux_field_info: PosMuxFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + pos_mux_field_info: PosMuxFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrRW( String(), group=WidgetGroup.INPUTS.value, @@ -300,8 +352,13 @@ def __init__(self, pos_mux_field_info: PosMuxFieldInfo): class UintParamFieldController(FieldController): - def __init__(self, uint_param_field_info: UintFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + uint_param_field_info: UintFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Float(prec=0), group=WidgetGroup.PARAMETERS.value, @@ -309,8 +366,13 @@ def __init__(self, uint_param_field_info: UintFieldInfo): class UintReadFieldController(FieldController): - def __init__(self, uint_read_field_info: UintFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + uint_read_field_info: UintFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Float(prec=0), group=WidgetGroup.READBACKS.value, @@ -320,8 +382,13 @@ def __init__(self, uint_read_field_info: UintFieldInfo): class UintWriteFieldController(FieldController): - def __init__(self, uint_write_field_info: UintFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + uint_write_field_info: UintFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrW( Float(prec=0), group=WidgetGroup.OUTPUTS.value, @@ -329,8 +396,13 @@ def __init__(self, uint_write_field_info: UintFieldInfo): class IntParamFieldController(FieldController): - def __init__(self, int_param_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + int_param_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrRW( Float(prec=0), group=WidgetGroup.PARAMETERS.value, @@ -338,8 +410,13 @@ def __init__(self, int_param_field_info: FieldInfo): class IntReadFieldController(FieldController): - def __init__(self, int_read_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + int_read_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Int(), group=WidgetGroup.READBACKS.value, @@ -347,8 +424,13 @@ def __init__(self, int_read_field_info: FieldInfo): class IntWriteFieldController(FieldController): - def __init__(self, int_write_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + int_write_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrW( Int(), group=WidgetGroup.PARAMETERS.value, @@ -356,8 +438,13 @@ def __init__(self, int_write_field_info: FieldInfo): class ScalarParamFieldController(FieldController): - def __init__(self, scalar_param_field_info: ScalarFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + scalar_param_field_info: ScalarFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrRW( Float(), group=WidgetGroup.PARAMETERS.value, @@ -365,8 +452,13 @@ def __init__(self, scalar_param_field_info: ScalarFieldInfo): class ScalarReadFieldController(FieldController): - def __init__(self, scalar_read_field_info: ScalarFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + scalar_read_field_info: ScalarFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Float(), group=WidgetGroup.READBACKS.value, @@ -374,8 +466,13 @@ def __init__(self, scalar_read_field_info: ScalarFieldInfo): class ScalarWriteFieldController(FieldController): - def __init__(self, scalar_write_field_info: ScalarFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + scalar_write_field_info: ScalarFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Float(), group=WidgetGroup.PARAMETERS.value, @@ -383,8 +480,13 @@ def __init__(self, scalar_write_field_info: ScalarFieldInfo): class BitParamFieldController(FieldController): - def __init__(self, bit_param_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + bit_param_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrRW( Bool(znam="0", onam="1"), group=WidgetGroup.PARAMETERS.value, @@ -392,8 +494,13 @@ def __init__(self, bit_param_field_info: FieldInfo): class BitReadFieldController(FieldController): - def __init__(self, bit_read_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + bit_read_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value, @@ -401,8 +508,13 @@ def __init__(self, bit_read_field_info: FieldInfo): class BitWriteFieldController(FieldController): - def __init__(self, bit_write_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + bit_write_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrW( Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value, @@ -410,8 +522,13 @@ def __init__(self, bit_write_field_info: FieldInfo): class ActionReadFieldController(FieldController): - def __init__(self, action_read_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + action_read_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( Bool(znam="0", onam="1"), group=WidgetGroup.READBACKS.value, @@ -419,8 +536,13 @@ def __init__(self, action_read_field_info: FieldInfo): class ActionWriteFieldController(FieldController): - def __init__(self, action_write_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + action_write_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrW( Bool(znam="0", onam="1"), group=WidgetGroup.OUTPUTS.value, @@ -428,8 +550,13 @@ def __init__(self, action_write_field_info: FieldInfo): class LutParamFieldController(FieldController): - def __init__(self, lut_param_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + lut_param_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrRW( String(), group=WidgetGroup.PARAMETERS.value, @@ -437,8 +564,13 @@ def __init__(self, lut_param_field_info: FieldInfo): class LutReadFieldController(FieldController): - def __init__(self, lut_read_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + lut_read_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( String(), group=WidgetGroup.READBACKS.value, @@ -446,8 +578,13 @@ def __init__(self, lut_read_field_info: FieldInfo): class LutWriteFieldController(FieldController): - def __init__(self, lut_read_field_info: FieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + lut_read_field_info: FieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( String(), group=WidgetGroup.OUTPUTS.value, @@ -455,8 +592,13 @@ def __init__(self, lut_read_field_info: FieldInfo): class EnumParamFieldController(FieldController): - def __init__(self, enum_param_field_info: EnumFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + enum_param_field_info: EnumFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.allowed_values = enum_param_field_info.labels self.top_level_attribute = AttrRW( String(), @@ -465,8 +607,13 @@ def __init__(self, enum_param_field_info: EnumFieldInfo): class EnumReadFieldController(FieldController): - def __init__(self, enum_read_field_info: EnumFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + enum_read_field_info: EnumFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrR( String(), group=WidgetGroup.READBACKS.value, @@ -474,8 +621,13 @@ def __init__(self, enum_read_field_info: EnumFieldInfo): class EnumWriteFieldController(FieldController): - def __init__(self, enum_write_field_info: EnumFieldInfo): - super().__init__() + def __init__( + self, + panda_name: PandaName, + enum_write_field_info: EnumFieldInfo, + initial_values: RawInitialValuesType, + ): + super().__init__(panda_name) self.top_level_attribute = AttrW( String(), group=WidgetGroup.OUTPUTS.value, @@ -520,96 +672,94 @@ def get_field_controller_from_field_info( panda_name: PandaName, field_info: ResponseType, initial_values: RawInitialValuesType, - label: str | None, ) -> FieldControllerType: match field_info: case TableFieldInfo(): - return TableFieldController(panda_name, field_info) + return TableFieldController(panda_name, field_info, initial_values) # Time types case TimeFieldInfo(subtype=None): - return TimeParamFieldController(panda_name, field_info) + return TimeParamFieldController(panda_name, field_info, initial_values) case SubtypeTimeFieldInfo(type="param"): - return TimeParamFieldController(panda_name, field_info) + return TimeParamFieldController(panda_name, field_info, initial_values) case SubtypeTimeFieldInfo(subtype="read"): - return TimeReadFieldController(panda_name, field_info) + return TimeReadFieldController(panda_name, field_info, initial_values) case SubtypeTimeFieldInfo(subtype="write"): - return TimeWriteFieldController(panda_name, field_info) + return TimeWriteFieldController(panda_name, field_info, initial_values) # Bit types case BitOutFieldInfo(): - return BitOutFieldController(field_info) + return BitOutFieldController(panda_name, field_info, initial_values) case ExtOutBitsFieldInfo(subtype="timestamp"): - return ExtOutFieldController(field_info) + return ExtOutFieldController(panda_name, field_info, initial_values) case ExtOutBitsFieldInfo(): - return ExtOutBitsFieldController(field_info) + return ExtOutBitsFieldController(panda_name, field_info, initial_values) case ExtOutFieldInfo(): - return ExtOutFieldController(field_info) + return ExtOutFieldController(panda_name, field_info, initial_values) case BitMuxFieldInfo(): - return BitMuxFieldController(panda_name, field_info) + return BitMuxFieldController(panda_name, field_info, initial_values) case FieldInfo(type="param", subtype="bit"): - return BitParamFieldController(field_info) + return BitParamFieldController(panda_name, field_info, initial_values) case FieldInfo(type="read", subtype="bit"): - return BitReadFieldController(field_info) + return BitReadFieldController(panda_name, field_info, initial_values) case FieldInfo(type="write", subtype="bit"): - return BitWriteFieldController(field_info) + return BitWriteFieldController(panda_name, field_info, initial_values) # Pos types case PosOutFieldInfo(): - return PosOutFieldController(panda_name, field_info) + return PosOutFieldController(panda_name, field_info, initial_values) case PosMuxFieldInfo(): - return PosMuxFieldController(field_info) + return PosMuxFieldController(panda_name, field_info, initial_values) # Uint types case UintFieldInfo(type="param"): - return UintParamFieldController(field_info) + return UintParamFieldController(panda_name, field_info, initial_values) case UintFieldInfo(type="read"): - return UintReadFieldController(field_info) + return UintReadFieldController(panda_name, field_info, initial_values) case UintFieldInfo(type="write"): - return UintWriteFieldController(field_info) + return UintWriteFieldController(panda_name, field_info, initial_values) # Scalar types case ScalarFieldInfo(subtype="param"): - return ScalarParamFieldController(field_info) + return ScalarParamFieldController(panda_name, field_info, initial_values) case ScalarFieldInfo(type="read"): - return ScalarReadFieldController(field_info) + return ScalarReadFieldController(panda_name, field_info, initial_values) case ScalarFieldInfo(type="write"): - return ScalarWriteFieldController(field_info) + return ScalarWriteFieldController(panda_name, field_info, initial_values) # Int types case FieldInfo(type="param", subtype="int"): - return IntParamFieldController(field_info) + return IntParamFieldController(panda_name, field_info, initial_values) case FieldInfo(type="read", subtype="int"): - return IntReadFieldController(field_info) + return IntReadFieldController(panda_name, field_info, initial_values) case FieldInfo(type="write", subtype="int"): - return IntWriteFieldController(field_info) + return IntWriteFieldController(panda_name, field_info, initial_values) # Action types case FieldInfo( type="read", subtype="action", ): - return ActionReadFieldController(field_info) + return ActionReadFieldController(panda_name, field_info, initial_values) case FieldInfo( type="write", subtype="action", ): - return ActionWriteFieldController(field_info) + return ActionWriteFieldController(panda_name, field_info, initial_values) # Lut types case FieldInfo(type="param", subtype="lut"): - return LutParamFieldController(field_info) + return LutParamFieldController(panda_name, field_info, initial_values) case FieldInfo(type="read", subtype="lut"): - return LutReadFieldController(field_info) + return LutReadFieldController(panda_name, field_info, initial_values) case FieldInfo(type="write", subtype="lut"): - return LutWriteFieldController(field_info) + return LutWriteFieldController(panda_name, field_info, initial_values) # Enum types case EnumFieldInfo(type="param"): - return EnumParamFieldController(field_info) + return EnumParamFieldController(panda_name, field_info, initial_values) case EnumFieldInfo(type="read"): - return EnumReadFieldController(field_info) + return EnumReadFieldController(panda_name, field_info, initial_values) case EnumFieldInfo(type="write"): - return EnumWriteFieldController(field_info) - + return EnumWriteFieldController(panda_name, field_info, initial_values) 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 b124a72..efaf40d 100644 --- a/src/fastcs_pandablocks/types/__init__.py +++ b/src/fastcs_pandablocks/types/__init__.py @@ -1,23 +1,34 @@ -from .annotations import ( +from enum import Enum + +from ._annotations import ( RawBlocksType, RawFieldsType, RawInitialValuesType, ResponseType, ) -from .string_types import ( +from ._string_types import ( EPICS_SEPARATOR, PANDA_SEPARATOR, - EpicsName, PandaName, ) + +class WidgetGroup(Enum): + NONE = None + PARAMETERS = "Parameters" + OUTPUTS = "Outputs" + INPUTS = "Inputs" + READBACKS = "Readbacks" + CAPTURE = "Capture" + + __all__ = [ "EPICS_SEPARATOR", - "EpicsName", "PANDA_SEPARATOR", "PandaName", "ResponseType", "RawBlocksType", "RawFieldsType", "RawInitialValuesType", + "WidgetGroup", ] diff --git a/src/fastcs_pandablocks/types/annotations.py b/src/fastcs_pandablocks/types/_annotations.py similarity index 95% rename from src/fastcs_pandablocks/types/annotations.py rename to src/fastcs_pandablocks/types/_annotations.py index aca1cab..4d50a33 100644 --- a/src/fastcs_pandablocks/types/annotations.py +++ b/src/fastcs_pandablocks/types/_annotations.py @@ -17,7 +17,7 @@ UintFieldInfo, ) -from .string_types import PandaName +from ._string_types import PandaName # Pyright gives us variable not allowed in type expression error # if we try to use the new (|) syntax diff --git a/src/fastcs_pandablocks/types/string_types.py b/src/fastcs_pandablocks/types/_string_types.py similarity index 54% rename from src/fastcs_pandablocks/types/string_types.py rename to src/fastcs_pandablocks/types/_string_types.py index 1a2482b..1b3107e 100644 --- a/src/fastcs_pandablocks/types/string_types.py +++ b/src/fastcs_pandablocks/types/_string_types.py @@ -95,15 +95,6 @@ def from_string(cls, name: str): block=block, block_number=block_number, field=field, sub_field=sub_field ) - @cached_property - def epics_name(self): - return EpicsName( - block=self.block, - block_number=self.block_number, - field=self.field, - sub_field=self.sub_field, - ) - def __add__(self, other: PandaName) -> PandaName: return PandaName( block=_choose_sub_pv(self.block, other.block), @@ -124,89 +115,10 @@ def attribute_name(self) -> str: ) 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_separator( - EPICS_SEPARATOR, - 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.""" - 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_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 - - return EpicsName( - prefix=prefix, - block=block, - block_number=block_number, - field=field, - sub_field=sub_field, - ) - - @cached_property - def panda_name(self) -> PandaName: - return PandaName( - block=self.block, - block_number=self.block_number, - field=self.field, - sub_field=self.sub_field, - ) - - 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") - """ - - 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 __contains__(self, other: PandaName) -> bool: 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) + _check_eq(other.block, self.block) + and _check_eq(other.block_number, self.block_number) + and _check_eq(other.field, self.field) + and _check_eq(other.sub_field, self.sub_field) ) diff --git a/tests/test_types.py b/tests/test_types.py deleted file mode 100644 index 3a68023..0000000 --- a/tests/test_types.py +++ /dev/null @@ -1,66 +0,0 @@ -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(): - 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(): - assert ( - 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 - ) - 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 add 1 and 2" - - -def test_epics_name_contains(): - 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 2a4f5c23ac9bb4fe8e75bb0211f69f841b7b3931 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Fri, 1 Nov 2024 15:33:49 +0000 Subject: [PATCH 13/18] added initial values --- src/fastcs_pandablocks/__main__.py | 6 +- src/fastcs_pandablocks/panda/fields.py | 85 ++++++++++++------- src/fastcs_pandablocks/types/_string_types.py | 35 +++----- 3 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/fastcs_pandablocks/__main__.py b/src/fastcs_pandablocks/__main__.py index 4a5fd3d..3c13d95 100644 --- a/src/fastcs_pandablocks/__main__.py +++ b/src/fastcs_pandablocks/__main__.py @@ -81,10 +81,14 @@ def main(): level = getattr(logging, parsed_args.log_level.upper(), None) logging.basicConfig(format="%(levelname)s:%(message)s", level=level) + screens_directory = ( + Path(parsed_args.screens_dir) if parsed_args.screens_dir else None + ) + ioc( parsed_args.prefix, parsed_args.hostname, - screens_directory=Path(parsed_args.screens_dir), + screens_directory=screens_directory, clear_bobfiles=parsed_args.clear_bobfiles, poll_period=parsed_args.poll_period, naming_convention=PvNamingConvention(parsed_args.pv_naming_convention), diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index d1406f2..9f1a795 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -39,9 +39,7 @@ def _strip_description(description: str | None) -> str | None: - if description is None: - return description - return description[:MAXIMUM_DESCRIPTION_LENGTH] + return None if description is None else description[:MAXIMUM_DESCRIPTION_LENGTH] class FieldController(SubController): @@ -126,6 +124,7 @@ def __init__( handler=DefaultFieldHandler(panda_name), description=_strip_description(field_info.description), group=WidgetGroup.PARAMETERS.value, + initial_value=float(initial_values[panda_name]), ) self._additional_attributes["units"] = AttrW( String(), @@ -140,6 +139,7 @@ def __init__( self, panda_name: PandaName, field_info: SubtypeTimeFieldInfo, + initial_values: RawInitialValuesType, ): super().__init__(panda_name) self.top_level_attribute = AttrR( @@ -149,6 +149,7 @@ def __init__( ), description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, + initial_value=float(initial_values[panda_name]), ) self._additional_attributes["units"] = AttrW( String(), @@ -163,6 +164,7 @@ def __init__( self, panda_name: PandaName, field_info: SubtypeTimeFieldInfo, + initial_value: RawInitialValuesType, ): super().__init__(panda_name) self.top_level_attribute = AttrW( @@ -191,6 +193,7 @@ def __init__( Bool(znam="0", onam="1"), description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, + initial_value=bool(int(initial_values[panda_name])), ) @@ -206,6 +209,7 @@ def __init__( Float(), description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, + initial_value=bool(int(initial_values[panda_name])), ) scaled = AttrR( @@ -320,15 +324,17 @@ def __init__( initial_values: RawInitialValuesType, ): super().__init__(panda_name) + self.top_level_attribute = AttrRW( String(), description=_strip_description(bit_mux_field_info.description), handler=DefaultFieldHandler(panda_name), group=WidgetGroup.INPUTS.value, + initial_value=initial_values[panda_name], ) self._additional_attributes["delay"] = AttrRW( - Float(), + Int(), description="Clock delay on input.", handler=DefaultFieldHandler(panda_name), group=WidgetGroup.INPUTS.value, @@ -347,7 +353,10 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrRW( String(), + description=_strip_description(pos_mux_field_info.description), group=WidgetGroup.INPUTS.value, + allowed_values=pos_mux_field_info.labels, + initial_value=initial_values[panda_name], ) @@ -361,9 +370,13 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( Float(prec=0), + description=_strip_description(uint_param_field_info.description), group=WidgetGroup.PARAMETERS.value, + initial_value=float(initial_values[panda_name]), ) + # TODO: set DRVL, DRVH, HOPR (new fastcs feature) + class UintReadFieldController(FieldController): def __init__( @@ -375,9 +388,9 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( Float(prec=0), + description=_strip_description(uint_read_field_info.description), group=WidgetGroup.READBACKS.value, - # To be added once we have a pvxs backend - # description=uint_read_field_info.description, + initial_value=float(initial_values[panda_name]), ) @@ -391,6 +404,7 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrW( Float(prec=0), + description=_strip_description(uint_write_field_info.description), group=WidgetGroup.OUTPUTS.value, ) @@ -404,8 +418,10 @@ def __init__( ): super().__init__(panda_name) self.top_level_attribute = AttrRW( - Float(prec=0), + Int(), + description=_strip_description(int_param_field_info.description), group=WidgetGroup.PARAMETERS.value, + initial_value=int(initial_values[panda_name]), ) @@ -419,7 +435,9 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( Int(), + description=_strip_description(int_read_field_info.description), group=WidgetGroup.READBACKS.value, + initial_value=int(initial_values[panda_name]), ) @@ -433,6 +451,7 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrW( Int(), + description=_strip_description(int_write_field_info.description), group=WidgetGroup.PARAMETERS.value, ) @@ -446,8 +465,10 @@ def __init__( ): super().__init__(panda_name) self.top_level_attribute = AttrRW( - Float(), + Float(units=scalar_param_field_info.units), + description=_strip_description(scalar_param_field_info.description), group=WidgetGroup.PARAMETERS.value, + initial_value=float(initial_values[panda_name]), ) @@ -461,7 +482,9 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( Float(), + description=_strip_description(scalar_read_field_info.description), group=WidgetGroup.READBACKS.value, + initial_value=float(initial_values[panda_name]), ) @@ -475,6 +498,7 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( Float(), + description=_strip_description(scalar_write_field_info.description), group=WidgetGroup.PARAMETERS.value, ) @@ -489,7 +513,11 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrRW( Bool(znam="0", onam="1"), + description=_strip_description(bit_param_field_info.description), group=WidgetGroup.PARAMETERS.value, + # Initial value is string "0"/"1". + # TODO: Equip each read/readwrite field with a converter + initial_value=bool(int(initial_values[panda_name])), ) @@ -503,7 +531,9 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( Bool(znam="0", onam="1"), + description=_strip_description(bit_read_field_info.description), group=WidgetGroup.READBACKS.value, + initial_value=bool(int(initial_values[panda_name])), ) @@ -517,24 +547,11 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrW( Bool(znam="0", onam="1"), + description=_strip_description(bit_write_field_info.description), group=WidgetGroup.OUTPUTS.value, ) -class ActionReadFieldController(FieldController): - def __init__( - self, - panda_name: PandaName, - action_read_field_info: FieldInfo, - initial_values: RawInitialValuesType, - ): - super().__init__(panda_name) - self.top_level_attribute = AttrR( - Bool(znam="0", onam="1"), - group=WidgetGroup.READBACKS.value, - ) - - class ActionWriteFieldController(FieldController): def __init__( self, @@ -545,6 +562,7 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrW( Bool(znam="0", onam="1"), + description=_strip_description(action_write_field_info.description), group=WidgetGroup.OUTPUTS.value, ) @@ -559,7 +577,9 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrRW( String(), + description=_strip_description(lut_param_field_info.description), group=WidgetGroup.PARAMETERS.value, + initial_value=initial_values[panda_name], ) @@ -573,7 +593,9 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( String(), + description=_strip_description(lut_read_field_info.description), group=WidgetGroup.READBACKS.value, + initial_value=initial_values[panda_name], ) @@ -587,6 +609,7 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrR( String(), + description=_strip_description(lut_read_field_info.description), group=WidgetGroup.OUTPUTS.value, ) @@ -599,10 +622,12 @@ def __init__( initial_values: RawInitialValuesType, ): super().__init__(panda_name) - self.allowed_values = enum_param_field_info.labels self.top_level_attribute = AttrRW( String(), + description=_strip_description(enum_param_field_info.description), + allowed_values=enum_param_field_info.labels, group=WidgetGroup.PARAMETERS.value, + initial_value=initial_values[panda_name], ) @@ -614,9 +639,15 @@ def __init__( initial_values: RawInitialValuesType, ): super().__init__(panda_name) + + # We use a raw string for this since many labels won't fit into + # mbbIn fields because of EPICS limitations. + # Since this is read only it doesn't matter. self.top_level_attribute = AttrR( String(), + description=_strip_description(enum_read_field_info.description), group=WidgetGroup.READBACKS.value, + initial_value=initial_values[panda_name], ) @@ -630,6 +661,8 @@ def __init__( super().__init__(panda_name) self.top_level_attribute = AttrW( String(), + description=_strip_description(enum_write_field_info.description), + allowed_values=enum_write_field_info.labels, group=WidgetGroup.OUTPUTS.value, ) @@ -654,7 +687,6 @@ def __init__( | BitParamFieldController | BitReadFieldController | BitWriteFieldController - | ActionReadFieldController | ActionWriteFieldController | LutParamFieldController | LutReadFieldController @@ -735,11 +767,6 @@ def get_field_controller_from_field_info( return IntWriteFieldController(panda_name, field_info, initial_values) # Action types - case FieldInfo( - type="read", - subtype="action", - ): - return ActionReadFieldController(panda_name, field_info, initial_values) case FieldInfo( type="write", subtype="action", diff --git a/src/fastcs_pandablocks/types/_string_types.py b/src/fastcs_pandablocks/types/_string_types.py index 1b3107e..ea5bd28 100644 --- a/src/fastcs_pandablocks/types/_string_types.py +++ b/src/fastcs_pandablocks/types/_string_types.py @@ -11,7 +11,7 @@ PANDA_SEPARATOR = "." -def _extract_number_at_of_string(string: str) -> tuple[str, int | None]: +def _extract_number_at_end_of_string(string: str) -> tuple[str, int | None]: pattern = r"(\D+)(\d+)$" match = re.match(pattern, string) if match: @@ -40,7 +40,7 @@ def _to_python_attribute_name(string: str): return string.replace("-", "_").lower() -def _choose_sub_pv(sub_pv_1: T, sub_pv_2: T) -> T: +def _choose_sub_name(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( @@ -49,14 +49,6 @@ def _choose_sub_pv(sub_pv_1: T, sub_pv_2: T) -> T: 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) class PandaName: block: str | None = None @@ -87,7 +79,7 @@ def from_string(cls, name: str): return PandaName() block, block_number, field, sub_field = None, None, None, None - block, block_number = _extract_number_at_of_string(split_name[0]) + block, block_number = _extract_number_at_end_of_string(split_name[0]) field = split_name[1] if len(split_name) > 1 else None sub_field = split_name[2] if len(split_name) > 2 else None @@ -97,10 +89,10 @@ def from_string(cls, name: str): 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), + block=_choose_sub_name(self.block, other.block), + block_number=_choose_sub_name(self.block_number, other.block_number), + field=_choose_sub_name(self.field, other.field), + sub_field=_choose_sub_name(self.sub_field, other.sub_field), ) @cached_property @@ -116,9 +108,10 @@ def attribute_name(self) -> str: return "" def __contains__(self, other: PandaName) -> bool: - return ( - _check_eq(other.block, self.block) - and _check_eq(other.block_number, self.block_number) - and _check_eq(other.field, self.field) - and _check_eq(other.sub_field, self.sub_field) - ) + for attr in ("block", "block_number", "field", "sub_field"): + sub_value, super_value = getattr(other, attr), getattr(self, attr) + if super_value is None: + break + if sub_value != super_value: + return False + return True From 45d17c75d15b19d4a5be40dbdaafbb93e6e0f0de Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 11 Nov 2024 13:47:44 +0000 Subject: [PATCH 14/18] added `:LABEL` to fields from metadata --- src/fastcs_pandablocks/panda/controller.py | 8 ++++++++ src/fastcs_pandablocks/panda/fields.py | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py index b717400..4d4f8a9 100644 --- a/src/fastcs_pandablocks/panda/controller.py +++ b/src/fastcs_pandablocks/panda/controller.py @@ -59,6 +59,8 @@ def __init__(self, hostname: str, poll_period: float) -> None: self._raw_panda = RawPanda(hostname) self._blocks: dict[PandaName, FieldController] = {} + self.connected = False + super().__init__() @property @@ -66,9 +68,14 @@ def additional_attributes(self): return self._additional_attributes async def connect(self) -> None: + if self.connected: + # `connect` needs to be called in `initialise`, + # then FastCS will attempt to call it again. + return await self._raw_panda.connect() blocks, fields, labels, initial_values = await self._raw_panda.introspect() self._blocks = _parse_introspected_data(blocks, fields, labels, initial_values) + self.connected = True async def initialise(self) -> None: await self.connect() @@ -109,6 +116,7 @@ async def update_field_value(self, panda_name: PandaName, value: str): @scan(0.1) async def update(self): + raise RuntimeError("FINALLY CALLED!") changes = await self._raw_panda.get_changes() await asyncio.gather( *[ diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 9f1a795..c46fe58 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -56,6 +56,13 @@ def __init__( self._additional_attributes: dict[str, Attribute] = {} + if label is not None: + self._additional_attributes["label"] = AttrR( + String(), + description="Label from metadata.", + initial_value=label, + ) + super().__init__(search_device_for_attributes=False) def make_sub_fields( @@ -641,7 +648,7 @@ def __init__( super().__init__(panda_name) # We use a raw string for this since many labels won't fit into - # mbbIn fields because of EPICS limitations. + # `mbbIn` fields because of EPICS limitations. # Since this is read only it doesn't matter. self.top_level_attribute = AttrR( String(), From 304dd1b36be6eea62d884895737e61ce7cadc76d Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 11 Nov 2024 15:39:02 +0000 Subject: [PATCH 15/18] update method is working well --- src/fastcs_pandablocks/handlers.py | 2 +- src/fastcs_pandablocks/panda/controller.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/fastcs_pandablocks/handlers.py b/src/fastcs_pandablocks/handlers.py index 62e1ca2..222bbbf 100644 --- a/src/fastcs_pandablocks/handlers.py +++ b/src/fastcs_pandablocks/handlers.py @@ -15,7 +15,7 @@ async def put(self, controller: Any, attr: AttrW, value: str) -> None: class DefaultFieldUpdater(Updater): #: We update the fields from the top level - update_period = float("inf") + update_period = None def __init__(self, panda_name: PandaName): self.panda_name = panda_name diff --git a/src/fastcs_pandablocks/panda/controller.py b/src/fastcs_pandablocks/panda/controller.py index 4d4f8a9..aca7808 100644 --- a/src/fastcs_pandablocks/panda/controller.py +++ b/src/fastcs_pandablocks/panda/controller.py @@ -116,7 +116,6 @@ async def update_field_value(self, panda_name: PandaName, value: str): @scan(0.1) async def update(self): - raise RuntimeError("FINALLY CALLED!") changes = await self._raw_panda.get_changes() await asyncio.gather( *[ From ee8bb95a0d22f3c7647eba6ebf98244fed1b4ef7 Mon Sep 17 00:00:00 2001 From: Eva Date: Tue, 12 Nov 2024 16:04:42 +0000 Subject: [PATCH 16/18] set operational limits --- src/fastcs_pandablocks/panda/fields.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index c46fe58..32c8006 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -341,14 +341,12 @@ def __init__( ) self._additional_attributes["delay"] = AttrRW( - Int(), + Int(max=bit_mux_field_info.max_delay, min=0), description="Clock delay on input.", handler=DefaultFieldHandler(panda_name), group=WidgetGroup.INPUTS.value, ) - # TODO: Add DRVL DRVH to `delay`. - class PosMuxFieldController(FieldController): def __init__( @@ -375,15 +373,18 @@ def __init__( initial_values: RawInitialValuesType, ): super().__init__(panda_name) - self.top_level_attribute = AttrR( - Float(prec=0), + self.top_level_attribute = AttrRW( + Float( + max_alarm=uint_param_field_info.max_val, + max=uint_param_field_info.max_val, + min_alarm=0, + min=0, + ), description=_strip_description(uint_param_field_info.description), group=WidgetGroup.PARAMETERS.value, initial_value=float(initial_values[panda_name]), ) - # TODO: set DRVL, DRVH, HOPR (new fastcs feature) - class UintReadFieldController(FieldController): def __init__( @@ -394,7 +395,7 @@ def __init__( ): super().__init__(panda_name) self.top_level_attribute = AttrR( - Float(prec=0), + Float(prec=0, min_alarm=0, max_alarm=uint_read_field_info.max_val), description=_strip_description(uint_read_field_info.description), group=WidgetGroup.READBACKS.value, initial_value=float(initial_values[panda_name]), @@ -410,7 +411,13 @@ def __init__( ): super().__init__(panda_name) self.top_level_attribute = AttrW( - Float(prec=0), + Float( + prec=0, + max_alarm=uint_write_field_info.max_val, + max=uint_write_field_info.max_val, + min_alarm=0, + min=0, + ), description=_strip_description(uint_write_field_info.description), group=WidgetGroup.OUTPUTS.value, ) From e603c510617d3eef7c011488bc5d0a1cd8dcd314 Mon Sep 17 00:00:00 2001 From: Eva Date: Wed, 13 Nov 2024 16:08:23 +0000 Subject: [PATCH 17/18] made `:LABEL` update `.EGU` --- src/fastcs_pandablocks/handlers.py | 11 ++++++++--- src/fastcs_pandablocks/panda/fields.py | 24 ++++++++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/fastcs_pandablocks/handlers.py b/src/fastcs_pandablocks/handlers.py index 222bbbf..820e4ee 100644 --- a/src/fastcs_pandablocks/handlers.py +++ b/src/fastcs_pandablocks/handlers.py @@ -1,3 +1,4 @@ +from dataclasses import asdict from typing import Any from fastcs.attributes import Attribute, AttrR, AttrW, Handler, Sender, Updater @@ -30,13 +31,17 @@ def __init__(self, panda_name: PandaName): class EguSender(Sender): - def __init__(self, attr_to_update: Attribute): + def __init__(self, panda_name: PandaName, attr_to_update: Attribute): """Update the attr""" + self.panda_name = panda_name 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 - ... + await controller.put_value_to_panda(self.panda_name, value) + kwargs = asdict(self.attr_to_update.datatype) + kwargs["units"] = value + new_attribute_datatype = type(self.attr_to_update.datatype)(**kwargs) + self.attr_to_update.update_datatype(new_attribute_datatype) class CaptureHandler(Handler): diff --git a/src/fastcs_pandablocks/panda/fields.py b/src/fastcs_pandablocks/panda/fields.py index 32c8006..1e03272 100644 --- a/src/fastcs_pandablocks/panda/fields.py +++ b/src/fastcs_pandablocks/panda/fields.py @@ -126,8 +126,12 @@ def __init__( initial_values: RawInitialValuesType, ): super().__init__(panda_name) + + units_panda_name = panda_name + PandaName(sub_field="units") + initial_units = initial_values[units_panda_name] + self.top_level_attribute = AttrRW( - Float(), + Float(units=initial_units), handler=DefaultFieldHandler(panda_name), description=_strip_description(field_info.description), group=WidgetGroup.PARAMETERS.value, @@ -135,7 +139,7 @@ def __init__( ) self._additional_attributes["units"] = AttrW( String(), - handler=EguSender(self.top_level_attribute), + handler=EguSender(units_panda_name, self.top_level_attribute), group=WidgetGroup.PARAMETERS.value, allowed_values=field_info.units_labels, ) @@ -149,8 +153,12 @@ def __init__( initial_values: RawInitialValuesType, ): super().__init__(panda_name) + + units_panda_name = panda_name + PandaName(sub_field="units") + initial_units = initial_values[units_panda_name] + self.top_level_attribute = AttrR( - Float(), + Float(units=initial_units), handler=DefaultFieldUpdater( panda_name=panda_name, ), @@ -160,7 +168,7 @@ def __init__( ) self._additional_attributes["units"] = AttrW( String(), - handler=EguSender(self.top_level_attribute), + handler=EguSender(units_panda_name, self.top_level_attribute), group=WidgetGroup.OUTPUTS.value, allowed_values=field_info.units_labels, ) @@ -171,11 +179,15 @@ def __init__( self, panda_name: PandaName, field_info: SubtypeTimeFieldInfo, - initial_value: RawInitialValuesType, + initial_values: RawInitialValuesType, ): super().__init__(panda_name) + + units_panda_name = panda_name + PandaName(sub_field="units") + initial_units = initial_values[units_panda_name] + self.top_level_attribute = AttrW( - Float(), + Float(units=initial_units), handler=DefaultFieldSender(panda_name), description=_strip_description(field_info.description), group=WidgetGroup.OUTPUTS.value, From dd1e79ebde62c9cd65ffddcb0a4e33fc3fd6db31 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 23:37:55 +0000 Subject: [PATCH 18/18] Bump the actions group across 1 directory with 2 updates Bumps the actions group with 2 updates in the / directory: [softprops/action-gh-release](https://github.com/softprops/action-gh-release) and [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `softprops/action-gh-release` from 2.0.8 to 2.2.0 - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/c062e08bd532815e2082a85e87e3ef29c3e6d191...7b4da11513bf3f43f9999e90eabced41ab8bb048) Updates `codecov/codecov-action` from 4 to 5 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/_release.yml | 2 +- .github/workflows/_test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index 10d8ed8..c771682 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -23,7 +23,7 @@ jobs: - name: Create GitHub Release # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} files: "*" diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index f652d41..552b29d 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -54,7 +54,7 @@ jobs: run: tox -e tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: name: ${{ inputs.python-version }}/${{ inputs.runs-on }} files: cov.xml