Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,17 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug Unit Test",
"name": "panda",
"type": "debugpy",
"request": "launch",
"module": "fastcs_pandablocks",
"justMyCode": false,
"program": "${file}",
"purpose": [
"debug-test"
],
"console": "integratedTerminal",
"env": {
// Enable break on exception when debugging tests (see: tests/conftest.py)
"PYTEST_RAISE": "1",
},
"args": [
"run",
"172.23.252.201",
"TEST-PANDA"
]
}
]
}
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ classifiers = [
]
description = "A softioc to control a PandABlocks-FPGA."
dependencies = [
"fastcs~=0.9.0",
"fastcs~=0.10.1",
"pandablocks~=0.10.0",
"numpy<2", # until https://github.com/mdavidsaver/p4p/issues/145 is fixed
"pydantic>2",
Expand Down
1 change: 1 addition & 0 deletions src/fastcs_pandablocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def ioc(

controller = PandaController(hostname, poll_period)
transport = FastCS(controller, [p4p_ioc_options])
transport.create_gui()
transport.run()


Expand Down
28 changes: 22 additions & 6 deletions src/fastcs_pandablocks/panda/blocks/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
TimeFieldInfo,
UintFieldInfo,
)
from pandablocks.utils import words_to_table

from fastcs_pandablocks.panda.client_wrapper import RawPanda
from fastcs_pandablocks.panda.handlers import (
Expand All @@ -33,6 +34,7 @@
DefaultFieldSender,
DefaultFieldUpdater,
TableFieldHandler,
panda_value_to_attribute_value,
)
from fastcs_pandablocks.types import (
PandaName,
Expand Down Expand Up @@ -211,7 +213,7 @@ def add_field_to_block(
match field_info:
case TableFieldInfo():
return self._make_table_field(
parent_block, field_panda_name, field_info
parent_block, field_panda_name, field_info, initial_values
)
case TimeFieldInfo(subtype=None):
self._make_time_param(
Expand Down Expand Up @@ -359,17 +361,30 @@ def _make_table_field(
parent_block: BlockController,
panda_name: PandaName,
field_info: TableFieldInfo,
initial_values: RawInitialValuesType,
):
structured_datatype = [
(name, self._table_datatypes_from_table_field_details(details))
for name, details in field_info.fields.items()
]

initial_value = panda_value_to_attribute_value(
fastcs_datatype=Table(structured_datatype),
value=words_to_table(
words=initial_values[panda_name], table_field_info=field_info
),
)

# TODO: Add units handler to update the units field and value of this one PV
# https://github.com/PandABlocks/PandABlocks-ioc/blob/c1e8056abf3f680fa3840493eb4ac6ca2be31313/src/pandablocks_ioc/ioc.py#L750-L769
parent_block.attributes[panda_name.attribute_name] = AttrRW(
attribute = AttrRW(
Table(structured_datatype),
handler=TableFieldHandler(panda_name),
handler=TableFieldHandler(
panda_name, field_info, self._raw_panda.put_value_to_panda
),
initial_value=initial_value,
)
parent_block.add_attribute(panda_name, attribute)

def _make_time_param(
self,
Expand Down Expand Up @@ -608,16 +623,17 @@ def _make_bit_mux(
bit_mux_field_info: BitMuxFieldInfo,
initial_values: RawInitialValuesType,
):
enum_type = enum.Enum("Labels", bit_mux_field_info.labels)
parent_block.add_attribute(
panda_name,
AttrRW(
String(),
Enum(enum_type),
description=bit_mux_field_info.description,
handler=DefaultFieldHandler(
panda_name, self._raw_panda.put_value_to_panda
),
group=WidgetGroup.INPUTS.value,
initial_value=initial_values[panda_name],
initial_value=enum_type[initial_values[panda_name]],
),
)

Expand All @@ -628,7 +644,7 @@ def _make_bit_mux(
Int(min=0, max=bit_mux_field_info.max_delay),
description="Clock delay on input.",
handler=DefaultFieldHandler(
panda_name, self._raw_panda.put_value_to_panda
delay_panda_name, self._raw_panda.put_value_to_panda
),
group=WidgetGroup.INPUTS.value,
initial_value=int(initial_values[delay_panda_name]),
Expand Down
45 changes: 26 additions & 19 deletions src/fastcs_pandablocks/panda/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
RawInitialValuesType,
)

from .handlers import attribute_value_to_panda_value
LOGGER = logging.getLogger(__name__)


class RawPanda:
Expand All @@ -44,11 +44,12 @@ async def disconnect(self):
await self._client.close()

async def put_value_to_panda(
self, panda_name: PandaName, fastcs_datatype: DataType[T], value: T
self,
panda_name: PandaName,
fastcs_datatype: DataType[T],
value: str | list[str],
) -> None:
await self.send(
str(panda_name), attribute_value_to_panda_value(fastcs_datatype, value)
)
await self.send(str(panda_name), value)

async def introspect(
self,
Expand All @@ -63,7 +64,7 @@ async def introspect(
for name, block_info in raw_blocks.items()
}
formatted_blocks = pformat(blocks, indent=4).replace("\n", "\n ")
logging.debug(f"BLOCKS RECEIVED:\n {formatted_blocks}")
LOGGER.debug(f"BLOCKS RECEIVED:\n {formatted_blocks}")

raw_fields = await asyncio.gather(
*[self._client.send(GetFieldInfo(str(block))) for block in blocks]
Expand All @@ -75,18 +76,18 @@ async def introspect(
}
for block_values in raw_fields
]
logging.debug("FIELDS RECEIVED (TOO VERBOSE TO LOG)")
LOGGER.debug("FIELDS RECEIVED (TOO VERBOSE TO LOG)")

field_data = (await self._client.send(GetChanges(ChangeGroup.ALL, True))).values
field_data = await self.get_changes()

for field_name, value in field_data.items():
if field_name.startswith("*METADATA"):
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_`: "
logging.warning(
"Ignoring received metadata not corresponding to a `LABEL_`: "
f"{field_name} = {value}."
)
labels[
Expand All @@ -100,27 +101,33 @@ async def introspect(
formatted_initial_values = pformat(initial_values, indent=4).replace(
"\n", "\n "
)
logging.debug(f"INITIAL VALUES:\n {formatted_initial_values}")
LOGGER.debug(f"INITIAL VALUES:\n {formatted_initial_values}")
formatted_labels = pformat(labels, indent=4).replace("\n", "\n ")
logging.debug(f"LABELS:\n {formatted_labels}")

return blocks, fields, labels, initial_values

async def send(self, name: str, value: str):
logging.debug(f"SENDING TO PANDA:\n {name} = {pformat(value, indent=4)}")
async def send(self, name: str, value: str | list[str]):
LOGGER.debug(f"SENDING TO PANDA:\n {name} = {pformat(value, indent=4)}")
await self._client.send(Put(name, value))

async def get(self, name: str) -> str | list[str]:
received = await self._client.send(Get(name))
formatted_received = pformat(received, indent=4).replace("\n", "\n ")
logging.debug(f"RECEIVED FROM PANDA:\n {name} = {formatted_received}")
LOGGER.debug(f"RECEIVED FROM PANDA:\n {name} = {formatted_received}")
return received

async def get_changes(self) -> dict[str, str]:
received = (await self._client.send(GetChanges(ChangeGroup.ALL, False))).values
formatted_received = pformat(received, indent=4).replace("\n", "\n ")
logging.debug(f"RECEIVED CHANGES:\n {formatted_received}")
return received
async def get_changes(self) -> dict[str, str | list[str]]:
changes = await self._client.send(GetChanges(ChangeGroup.ALL, True))
single_and_multiline_changes = {
**changes.values,
**changes.multiline_values,
}
formatted_received = pformat(single_and_multiline_changes, indent=4).replace(
"\n", "\n "
)
LOGGER.debug(f"RECEIVED CHANGES:\n {formatted_received}")
return single_and_multiline_changes

async def arm(self):
await self._client.send(Arm())
Expand Down
62 changes: 45 additions & 17 deletions src/fastcs_pandablocks/panda/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from dataclasses import dataclass
from typing import Any

import numpy as np
from fastcs.attributes import (
AttrHandlerR,
AttrHandlerRW,
Expand All @@ -13,30 +14,43 @@
AttrRW,
AttrW,
)
from fastcs.datatypes import Bool, DataType, Enum, Float, Int, String, T
from fastcs.datatypes import Bool, DataType, Enum, Float, Int, String, T, Table
from pandablocks.responses import TableFieldInfo
from pandablocks.utils import table_to_words

from fastcs_pandablocks.types import PandaName


def panda_value_to_attribute_value(fastcs_datatype: DataType[T], value: str) -> T:
def panda_value_to_attribute_value(
fastcs_datatype: DataType[T], value: str | dict
) -> T:
"""Converts from a value received from the panda through pandablock-client to
the attribute value.
"""
match fastcs_datatype:
case String():
return value
case Bool():
return str(int(value))
return bool(int(value))
case Int() | Float():
return fastcs_datatype.dtype(value)
case Enum():
return fastcs_datatype.enum_cls[value]
case Table():
num_rows = len(next(iter(value.values())))
structured_datatype = fastcs_datatype.structured_dtype
attribute_value = np.zeros(num_rows, fastcs_datatype.structured_dtype)
for field_name, _ in structured_datatype:
attribute_value[field_name] = value[field_name]
return attribute_value

case _:
raise NotImplementedError(f"Unknown datatype {fastcs_datatype}")


def attribute_value_to_panda_value(fastcs_datatype: DataType[T], value: T) -> str:
def attribute_value_to_panda_value(
fastcs_datatype: DataType[T], value: T
) -> str | dict:
"""Converts from an attribute value to a value that can be sent to the panda
with pandablocks-client.
"""
Expand All @@ -49,7 +63,12 @@ def attribute_value_to_panda_value(fastcs_datatype: DataType[T], value: T) -> st
return str(value)
case Enum():
return value.name

case Table():
assert isinstance(value, np.ndarray)
panda_value = {}
for field_name, _ in fastcs_datatype.structured_dtype:
panda_value[field_name] = value[field_name].tolist()
return panda_value
case _:
raise NotImplementedError(f"Unknown datatype {fastcs_datatype}")

Expand All @@ -67,13 +86,13 @@ def __init__(
self.panda_name = panda_name
self.put_value_to_panda = put_value_to_panda

async def update(self, attr: AttrR) -> None:
# TODO: Convert to panda value
...

async def put(self, attr: AttrW, value: Any) -> None:
# TODO: Convert to attribtue value
...
await self.put_value_to_panda(
self.panda_name,
attr.datatype,
attribute_value_to_panda_value(attr.datatype, value),
)


class DefaultFieldUpdater(AttrHandlerR):
Expand All @@ -85,6 +104,8 @@ class DefaultFieldUpdater(AttrHandlerR):
def __init__(self, panda_name: PandaName):
self.panda_name = panda_name

async def update(self, attr: AttrR) -> None: ...


class DefaultFieldHandler(DefaultFieldSender, DefaultFieldUpdater, AttrHandlerRW):
"""Default handler for sending and updating introspected attributes."""
Expand All @@ -102,16 +123,23 @@ def __init__(
class TableFieldHandler(AttrHandlerRW):
"""A handler for updating Table valued attributes."""

def __init__(self, panda_name: PandaName):
def __init__(
self,
panda_name: PandaName,
field_info: TableFieldInfo,
put_value_to_panda: Callable[
[PandaName, DataType, Any], Coroutine[None, None, None]
],
):
self.panda_name = panda_name

async def update(self, attr: AttrR) -> None:
# TODO: Convert to panda value
...
self.field_info = field_info
self.put_value_to_panda = put_value_to_panda

async def put(self, attr: AttrW, value: Any) -> None:
# TODO: Convert to attribtue value
...
attr_value = attribute_value_to_panda_value(attr.datatype, value)
assert isinstance(attr_value, dict)
panda_words = table_to_words(attr_value, self.field_info)
await self.put_value_to_panda(self.panda_name, attr.datatype, panda_words)


class CaptureHandler(DefaultFieldHandler):
Expand Down
Loading
Loading