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