fastcs-pandablocks 0.2.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastcs_pandablocks/__init__.py +3 -0
- fastcs_pandablocks/__main__.py +9 -0
- fastcs_pandablocks/_version.py +34 -0
- fastcs_pandablocks/panda/__init__.py +0 -0
- fastcs_pandablocks/panda/blocks/__init__.py +4 -0
- fastcs_pandablocks/panda/blocks/block_controller.py +48 -0
- fastcs_pandablocks/panda/blocks/blocks.py +1034 -0
- fastcs_pandablocks/panda/blocks/data.py +625 -0
- fastcs_pandablocks/panda/blocks/versions.py +69 -0
- fastcs_pandablocks/panda/client_wrapper.py +143 -0
- fastcs_pandablocks/panda/controller.yaml +10 -0
- fastcs_pandablocks/panda/io/arm.py +37 -0
- fastcs_pandablocks/panda/io/bits.py +43 -0
- fastcs_pandablocks/panda/io/default.py +36 -0
- fastcs_pandablocks/panda/io/table.py +38 -0
- fastcs_pandablocks/panda/io/units.py +49 -0
- fastcs_pandablocks/panda/panda_controller.py +125 -0
- fastcs_pandablocks/panda/utils.py +52 -0
- fastcs_pandablocks/types/__init__.py +34 -0
- fastcs_pandablocks/types/_annotations.py +42 -0
- fastcs_pandablocks/types/_string_types.py +117 -0
- fastcs_pandablocks-0.2.0a3.dist-info/METADATA +264 -0
- fastcs_pandablocks-0.2.0a3.dist-info/RECORD +27 -0
- fastcs_pandablocks-0.2.0a3.dist-info/WHEEL +5 -0
- fastcs_pandablocks-0.2.0a3.dist-info/entry_points.txt +2 -0
- fastcs_pandablocks-0.2.0a3.dist-info/licenses/LICENSE +201 -0
- fastcs_pandablocks-0.2.0a3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This method has a `RawPanda` which handles all the io with the client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import AsyncGenerator
|
|
7
|
+
from pprint import pformat
|
|
8
|
+
|
|
9
|
+
from fastcs.datatypes import DataType
|
|
10
|
+
from fastcs.logging import bind_logger
|
|
11
|
+
from pandablocks.asyncio import AsyncioClient
|
|
12
|
+
from pandablocks.commands import (
|
|
13
|
+
Arm,
|
|
14
|
+
ChangeGroup,
|
|
15
|
+
Disarm,
|
|
16
|
+
Get,
|
|
17
|
+
GetBlockInfo,
|
|
18
|
+
GetChanges,
|
|
19
|
+
GetFieldInfo,
|
|
20
|
+
Put,
|
|
21
|
+
)
|
|
22
|
+
from pandablocks.responses import Data
|
|
23
|
+
|
|
24
|
+
from fastcs_pandablocks.types import (
|
|
25
|
+
PandaName,
|
|
26
|
+
RawBlocksType,
|
|
27
|
+
RawFieldsType,
|
|
28
|
+
RawInitialValuesType,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = bind_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RawPanda:
|
|
35
|
+
"""A wrapper for interacting with pandablocks-client."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, hostname: str):
|
|
38
|
+
self._client = AsyncioClient(host=hostname)
|
|
39
|
+
|
|
40
|
+
async def connect(self):
|
|
41
|
+
await self._client.connect()
|
|
42
|
+
|
|
43
|
+
async def disconnect(self):
|
|
44
|
+
await self._client.close()
|
|
45
|
+
|
|
46
|
+
async def put_value_to_panda(
|
|
47
|
+
self,
|
|
48
|
+
panda_name: PandaName,
|
|
49
|
+
fastcs_datatype: DataType,
|
|
50
|
+
value: str | list[str],
|
|
51
|
+
) -> None:
|
|
52
|
+
await self.send(str(panda_name), value)
|
|
53
|
+
|
|
54
|
+
async def introspect(
|
|
55
|
+
self,
|
|
56
|
+
) -> tuple[
|
|
57
|
+
RawBlocksType, RawFieldsType, RawInitialValuesType, RawInitialValuesType
|
|
58
|
+
]:
|
|
59
|
+
blocks, fields, labels, initial_values = {}, [], {}, {}
|
|
60
|
+
|
|
61
|
+
raw_blocks = await self._client.send(GetBlockInfo())
|
|
62
|
+
blocks = {
|
|
63
|
+
PandaName.from_string(name): block_info
|
|
64
|
+
for name, block_info in raw_blocks.items()
|
|
65
|
+
}
|
|
66
|
+
formatted_blocks = pformat(blocks, indent=4).replace("\n", "\n ")
|
|
67
|
+
logger.debug(f"BLOCKS RECEIVED:\n {formatted_blocks}")
|
|
68
|
+
|
|
69
|
+
raw_fields = await asyncio.gather(
|
|
70
|
+
*[self._client.send(GetFieldInfo(str(block))) for block in blocks]
|
|
71
|
+
)
|
|
72
|
+
fields = [
|
|
73
|
+
{
|
|
74
|
+
PandaName(field=name): field_info
|
|
75
|
+
for name, field_info in block_values.items()
|
|
76
|
+
}
|
|
77
|
+
for block_values in raw_fields
|
|
78
|
+
]
|
|
79
|
+
logger.debug("FIELDS RECEIVED (TOO VERBOSE TO LOG)")
|
|
80
|
+
|
|
81
|
+
field_data = await self.get_changes()
|
|
82
|
+
|
|
83
|
+
for field_name, value in field_data.items():
|
|
84
|
+
if field_name.startswith("*METADATA"):
|
|
85
|
+
field_name_without_prefix = field_name.removeprefix("*METADATA.")
|
|
86
|
+
if field_name_without_prefix == "DESIGN":
|
|
87
|
+
continue # TODO: Handle design.
|
|
88
|
+
elif not field_name_without_prefix.startswith("LABEL_"):
|
|
89
|
+
logger.warning(
|
|
90
|
+
"Ignoring received metadata not corresponding to a `LABEL_`",
|
|
91
|
+
field_name=field_name,
|
|
92
|
+
value=value,
|
|
93
|
+
)
|
|
94
|
+
labels[
|
|
95
|
+
PandaName.from_string(
|
|
96
|
+
field_name_without_prefix.removeprefix("LABEL_")
|
|
97
|
+
)
|
|
98
|
+
] = value
|
|
99
|
+
else: # Field is a default value
|
|
100
|
+
initial_values[PandaName.from_string(field_name)] = value
|
|
101
|
+
|
|
102
|
+
formatted_initial_values = pformat(initial_values, indent=4).replace(
|
|
103
|
+
"\n", "\n "
|
|
104
|
+
)
|
|
105
|
+
logger.debug(f"INITIAL VALUES:\n {formatted_initial_values}")
|
|
106
|
+
formatted_labels = pformat(labels, indent=4).replace("\n", "\n ")
|
|
107
|
+
logger.debug(f"LABELS:\n {formatted_labels}")
|
|
108
|
+
|
|
109
|
+
return blocks, fields, labels, initial_values
|
|
110
|
+
|
|
111
|
+
async def send(self, name: str, value: str | list[str]):
|
|
112
|
+
logger.debug(f"SENDING TO PANDA:\n {name} = {pformat(value, indent=4)}")
|
|
113
|
+
await self._client.send(Put(name, value))
|
|
114
|
+
|
|
115
|
+
async def get(self, name: str) -> str | list[str]:
|
|
116
|
+
received = await self._client.send(Get(name))
|
|
117
|
+
formatted_received = pformat(received, indent=4).replace("\n", "\n ")
|
|
118
|
+
logger.debug(f"RECEIVED FROM PANDA:\n {name} = {formatted_received}")
|
|
119
|
+
return received
|
|
120
|
+
|
|
121
|
+
async def get_changes(self) -> dict[str, str | list[str]]:
|
|
122
|
+
changes = await self._client.send(GetChanges(ChangeGroup.ALL, True))
|
|
123
|
+
single_and_multiline_changes = {
|
|
124
|
+
**changes.values,
|
|
125
|
+
**changes.multiline_values,
|
|
126
|
+
}
|
|
127
|
+
formatted_received = pformat(single_and_multiline_changes, indent=4).replace(
|
|
128
|
+
"\n", "\n "
|
|
129
|
+
)
|
|
130
|
+
logger.debug(f"RECEIVED CHANGES:\n {formatted_received}")
|
|
131
|
+
return single_and_multiline_changes
|
|
132
|
+
|
|
133
|
+
async def arm(self):
|
|
134
|
+
await self._client.send(Arm())
|
|
135
|
+
|
|
136
|
+
async def disarm(self):
|
|
137
|
+
await self._client.send(Disarm())
|
|
138
|
+
|
|
139
|
+
async def data(
|
|
140
|
+
self, scaled: bool, flush_period: float
|
|
141
|
+
) -> AsyncGenerator[Data, None]:
|
|
142
|
+
async for data in self._client.data(scaled=scaled, flush_period=flush_period):
|
|
143
|
+
yield data
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import Callable, Coroutine
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from fastcs.attributes import (
|
|
8
|
+
AttributeIO,
|
|
9
|
+
AttributeIORef,
|
|
10
|
+
AttrW,
|
|
11
|
+
)
|
|
12
|
+
from fastcs.datatypes import DType_T
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ArmCommand(enum.Enum):
|
|
16
|
+
"""Enum class for PandA arm fields."""
|
|
17
|
+
|
|
18
|
+
DISARM = "Disarm"
|
|
19
|
+
ARM = "Arm"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ArmIORef(AttributeIORef):
|
|
24
|
+
arm: Callable[[], Coroutine[None, None, None]]
|
|
25
|
+
disarm: Callable[[], Coroutine[None, None, None]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ArmIO(AttributeIO[DType_T, ArmIORef]):
|
|
29
|
+
"""A sender for arming and disarming the Pcap."""
|
|
30
|
+
|
|
31
|
+
async def send(self, attr: AttrW[DType_T, ArmIORef], value: Any):
|
|
32
|
+
if value is ArmCommand.ARM:
|
|
33
|
+
logging.info("Arming PandA.")
|
|
34
|
+
await attr.io_ref.arm()
|
|
35
|
+
else:
|
|
36
|
+
logging.info("Disarming PandA.")
|
|
37
|
+
await attr.io_ref.disarm()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import enum
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastcs.attributes import (
|
|
7
|
+
AttrRW,
|
|
8
|
+
)
|
|
9
|
+
from fastcs.datatypes import DType_T, Enum
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def _set_attr_if_not_already_value(attribute: AttrRW[DType_T], value: DType_T):
|
|
13
|
+
if attribute.get() != value:
|
|
14
|
+
await attribute.update(value)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BitGroupOnUpdate:
|
|
19
|
+
"""Bits are tied together in bit groups so that when one is set for capture,
|
|
20
|
+
they all are.
|
|
21
|
+
|
|
22
|
+
This callback sets all capture attributes in the group when one of them is set.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
capture_attribute: AttrRW[enum.Enum]
|
|
26
|
+
bit_attributes: list[AttrRW[bool]]
|
|
27
|
+
|
|
28
|
+
async def __call__(self, value: Any):
|
|
29
|
+
if isinstance(value, enum.Enum):
|
|
30
|
+
bool_value = bool(self.capture_attribute.datatype.index_of(value)) # type: ignore
|
|
31
|
+
enum_value = value
|
|
32
|
+
else:
|
|
33
|
+
bool_value = value
|
|
34
|
+
assert isinstance(self.capture_attribute.datatype, Enum)
|
|
35
|
+
enum_value = self.capture_attribute.datatype.members[int(value)]
|
|
36
|
+
|
|
37
|
+
await asyncio.gather(
|
|
38
|
+
*[
|
|
39
|
+
_set_attr_if_not_already_value(bit_attr, bool_value)
|
|
40
|
+
for bit_attr in self.bit_attributes
|
|
41
|
+
],
|
|
42
|
+
_set_attr_if_not_already_value(self.capture_attribute, enum_value),
|
|
43
|
+
)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastcs.attributes import (
|
|
6
|
+
AttributeIO,
|
|
7
|
+
AttributeIORef,
|
|
8
|
+
AttrW,
|
|
9
|
+
)
|
|
10
|
+
from fastcs.datatypes import DataType, DType_T
|
|
11
|
+
|
|
12
|
+
from fastcs_pandablocks.panda.utils import (
|
|
13
|
+
attribute_value_to_panda_value,
|
|
14
|
+
)
|
|
15
|
+
from fastcs_pandablocks.types import PandaName
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class DefaultFieldIORef(AttributeIORef):
|
|
20
|
+
panda_name: PandaName
|
|
21
|
+
put_value_to_panda: Callable[
|
|
22
|
+
[PandaName, DataType, Any], Coroutine[None, None, None]
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DefaultFieldIO(AttributeIO[DType_T, DefaultFieldIORef]):
|
|
27
|
+
"""Default IO for sending and updating introspected attributes."""
|
|
28
|
+
|
|
29
|
+
async def send(
|
|
30
|
+
self, attr: AttrW[DType_T, DefaultFieldIORef], value: DType_T
|
|
31
|
+
) -> None:
|
|
32
|
+
await attr.io_ref.put_value_to_panda(
|
|
33
|
+
attr.io_ref.panda_name,
|
|
34
|
+
attr.datatype,
|
|
35
|
+
attribute_value_to_panda_value(attr.datatype, value),
|
|
36
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from collections.abc import Callable, Coroutine
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastcs.attributes import (
|
|
6
|
+
AttributeIO,
|
|
7
|
+
AttributeIORef,
|
|
8
|
+
AttrW,
|
|
9
|
+
)
|
|
10
|
+
from fastcs.datatypes import DataType, DType_T
|
|
11
|
+
from pandablocks.responses import TableFieldInfo
|
|
12
|
+
from pandablocks.utils import table_to_words
|
|
13
|
+
|
|
14
|
+
from fastcs_pandablocks.panda.utils import (
|
|
15
|
+
attribute_value_to_panda_value,
|
|
16
|
+
)
|
|
17
|
+
from fastcs_pandablocks.types import PandaName
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TableFieldIORef(AttributeIORef):
|
|
22
|
+
panda_name: PandaName
|
|
23
|
+
field_info: TableFieldInfo
|
|
24
|
+
put_value_to_panda: Callable[
|
|
25
|
+
[PandaName, DataType, Any], Coroutine[None, None, None]
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TableFieldIO(AttributeIO[DType_T, TableFieldIORef]):
|
|
30
|
+
"""An IO for updating Table valued attributes."""
|
|
31
|
+
|
|
32
|
+
async def send(self, attr: AttrW[DType_T, TableFieldIORef], value: DType_T) -> None:
|
|
33
|
+
attr_value = attribute_value_to_panda_value(attr.datatype, value)
|
|
34
|
+
assert isinstance(attr_value, dict)
|
|
35
|
+
panda_words = table_to_words(attr_value, attr.io_ref.field_info)
|
|
36
|
+
await attr.io_ref.put_value_to_panda(
|
|
37
|
+
attr.io_ref.panda_name, attr.datatype, panda_words
|
|
38
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
from collections.abc import Callable, Coroutine
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastcs.attributes import (
|
|
7
|
+
AttributeIO,
|
|
8
|
+
AttributeIORef,
|
|
9
|
+
AttrRW,
|
|
10
|
+
AttrW,
|
|
11
|
+
)
|
|
12
|
+
from fastcs.datatypes import DataType, Float
|
|
13
|
+
|
|
14
|
+
from fastcs_pandablocks.panda.utils import (
|
|
15
|
+
attribute_value_to_panda_value,
|
|
16
|
+
)
|
|
17
|
+
from fastcs_pandablocks.types import PandaName
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TimeUnit(enum.Enum):
|
|
21
|
+
"""Enum class for PandA time fields."""
|
|
22
|
+
|
|
23
|
+
min = "min"
|
|
24
|
+
s = "s"
|
|
25
|
+
ms = "ms"
|
|
26
|
+
us = "us"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class UnitsIORef(AttributeIORef):
|
|
31
|
+
attribute_to_scale: AttrRW
|
|
32
|
+
current_scale: TimeUnit
|
|
33
|
+
panda_name: PandaName
|
|
34
|
+
put_value_to_panda: Callable[
|
|
35
|
+
[PandaName, DataType, Any], Coroutine[None, None, None]
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class UnitsIO(AttributeIO[enum.Enum, UnitsIORef]):
|
|
40
|
+
"""A sender for arming and disarming the Pcap."""
|
|
41
|
+
|
|
42
|
+
async def send(self, attr: AttrW[enum.Enum, UnitsIORef], value: enum.Enum):
|
|
43
|
+
await attr.io_ref.put_value_to_panda(
|
|
44
|
+
attr.io_ref.panda_name,
|
|
45
|
+
attr.datatype,
|
|
46
|
+
attribute_value_to_panda_value(attr.datatype, value),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
attr.io_ref.attribute_to_scale.update_datatype(Float(units=value.name, prec=5))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastcs.attributes import Attribute, AttrR
|
|
6
|
+
from fastcs.controllers import Controller
|
|
7
|
+
from fastcs.datatypes import Table
|
|
8
|
+
from fastcs.logging import bind_logger
|
|
9
|
+
from fastcs.methods import scan
|
|
10
|
+
from pandablocks.utils import words_to_table
|
|
11
|
+
|
|
12
|
+
from fastcs_pandablocks.panda.blocks import Blocks
|
|
13
|
+
from fastcs_pandablocks.panda.client_wrapper import RawPanda
|
|
14
|
+
from fastcs_pandablocks.panda.io.arm import ArmIO
|
|
15
|
+
from fastcs_pandablocks.panda.io.default import DefaultFieldIO
|
|
16
|
+
from fastcs_pandablocks.panda.io.table import TableFieldIO, TableFieldIORef
|
|
17
|
+
from fastcs_pandablocks.panda.io.units import UnitsIO
|
|
18
|
+
from fastcs_pandablocks.panda.utils import panda_value_to_attribute_value
|
|
19
|
+
from fastcs_pandablocks.types import PandaName
|
|
20
|
+
|
|
21
|
+
logger = bind_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class PandaControllerSettings:
|
|
26
|
+
address: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PandaController(Controller):
|
|
30
|
+
"""Controller for polling data from the panda through pandablocks-client.
|
|
31
|
+
|
|
32
|
+
Changes are received at a given poll period and passed to sub-controllers.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, settings: PandaControllerSettings) -> None:
|
|
36
|
+
# TODO https://github.com/DiamondLightSource/FastCS/issues/62
|
|
37
|
+
|
|
38
|
+
self._raw_panda = RawPanda(settings.address)
|
|
39
|
+
self._ios = [ArmIO(), DefaultFieldIO(), TableFieldIO(), UnitsIO()]
|
|
40
|
+
self._blocks: Blocks = Blocks(self._raw_panda, ios=self._ios)
|
|
41
|
+
self.connected = False
|
|
42
|
+
|
|
43
|
+
super().__init__(ios=self._ios)
|
|
44
|
+
|
|
45
|
+
async def connect(self) -> None:
|
|
46
|
+
if self.connected:
|
|
47
|
+
# `connect` needs to be called in `initialise`,
|
|
48
|
+
# then FastCS will attempt to call it again.
|
|
49
|
+
return
|
|
50
|
+
await self._raw_panda.connect()
|
|
51
|
+
await self._blocks.parse_introspected_data()
|
|
52
|
+
await self._blocks.setup_post_introspection()
|
|
53
|
+
self.connected = True
|
|
54
|
+
|
|
55
|
+
async def initialise(self) -> None:
|
|
56
|
+
await self.connect()
|
|
57
|
+
|
|
58
|
+
for block_name, block in self._blocks.controllers():
|
|
59
|
+
# Numerically named controllers are registered to
|
|
60
|
+
# alphabetically named ControllerVectors, so only
|
|
61
|
+
# alphabetically named controllers
|
|
62
|
+
# should be registed to top level Controller
|
|
63
|
+
if str(block_name).isalpha():
|
|
64
|
+
self.add_sub_controller(block_name.lower(), block)
|
|
65
|
+
await block.initialise()
|
|
66
|
+
|
|
67
|
+
async def update_field_value(self, panda_name: PandaName, value: str | list[str]):
|
|
68
|
+
"""Update a panda field with either a single value or a list of words."""
|
|
69
|
+
|
|
70
|
+
attribute = self._blocks.get_attribute(panda_name)
|
|
71
|
+
assert isinstance(attribute, AttrR)
|
|
72
|
+
if attribute is None:
|
|
73
|
+
logger.error(f"Couldn't find panda field for {panda_name}.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
attribute_value = self._coerce_value_to_panda_type(attribute, value)
|
|
78
|
+
except ValueError:
|
|
79
|
+
logger.opt(exception=True).error("Coerce failed")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
await self.update_attribute(attribute, attribute_value)
|
|
83
|
+
|
|
84
|
+
def _coerce_value_to_panda_type(
|
|
85
|
+
self, attribute: Attribute, value: str | list[str]
|
|
86
|
+
) -> Any:
|
|
87
|
+
"""Convert a provided value into an attribute_value for this panda attribute."""
|
|
88
|
+
match value:
|
|
89
|
+
case list() as words:
|
|
90
|
+
if not isinstance(attribute.datatype, Table):
|
|
91
|
+
raise ValueError(f"{attribute} is not a Table attribute")
|
|
92
|
+
io_ref = attribute.io_ref
|
|
93
|
+
if not isinstance(io_ref, TableFieldIORef):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"AttributeIORef for {attribute} is not TableFieldIORef"
|
|
96
|
+
)
|
|
97
|
+
table_values = words_to_table(
|
|
98
|
+
words, io_ref.field_info, convert_enum_indices=True
|
|
99
|
+
)
|
|
100
|
+
return panda_value_to_attribute_value(attribute.datatype, table_values)
|
|
101
|
+
case _:
|
|
102
|
+
return panda_value_to_attribute_value(attribute.datatype, value)
|
|
103
|
+
|
|
104
|
+
async def update_attribute(self, attribute: AttrR, attribute_value: Any) -> None:
|
|
105
|
+
"""Dispatch setting logic based on attribute type."""
|
|
106
|
+
value = attribute.datatype.validate(attribute_value)
|
|
107
|
+
await attribute.update(value)
|
|
108
|
+
|
|
109
|
+
@scan(0.1)
|
|
110
|
+
async def update(self):
|
|
111
|
+
try:
|
|
112
|
+
changes = await self._raw_panda.get_changes()
|
|
113
|
+
await asyncio.gather(
|
|
114
|
+
*[
|
|
115
|
+
self.update_field_value(
|
|
116
|
+
PandaName.from_string(raw_panda_name), value
|
|
117
|
+
)
|
|
118
|
+
for raw_panda_name, value in changes.items()
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
# TODO: General exception is not ideal; narrow this dowm.
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise RuntimeError(
|
|
124
|
+
"Failed to update changes from PandaBlocks client"
|
|
125
|
+
) from e
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from fastcs.datatypes import Bool, DataType, Enum, Float, Int, String, Table
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def panda_value_to_attribute_value(fastcs_datatype: DataType, value: str | dict) -> Any:
|
|
8
|
+
"""Converts from a value received from the panda to the attribute value."""
|
|
9
|
+
|
|
10
|
+
match fastcs_datatype:
|
|
11
|
+
case String():
|
|
12
|
+
return fastcs_datatype.validate(value)
|
|
13
|
+
case Bool():
|
|
14
|
+
assert isinstance(value, str)
|
|
15
|
+
return fastcs_datatype.validate(int(value))
|
|
16
|
+
case Int() | Float():
|
|
17
|
+
return fastcs_datatype.validate(value)
|
|
18
|
+
case Enum():
|
|
19
|
+
return fastcs_datatype.enum_cls[value] # type: ignore
|
|
20
|
+
case Table():
|
|
21
|
+
assert isinstance(value, dict)
|
|
22
|
+
num_rows = len(next(iter(value.values())))
|
|
23
|
+
structured_datatype = fastcs_datatype.structured_dtype
|
|
24
|
+
attribute_value = np.zeros(num_rows, fastcs_datatype.structured_dtype)
|
|
25
|
+
for field_name, _ in structured_datatype:
|
|
26
|
+
attribute_value[field_name] = value[field_name.upper()]
|
|
27
|
+
return attribute_value
|
|
28
|
+
|
|
29
|
+
case _:
|
|
30
|
+
raise NotImplementedError(f"Unknown datatype {fastcs_datatype}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def attribute_value_to_panda_value(fastcs_datatype: DataType, value: Any) -> str | dict:
|
|
34
|
+
"""Converts from an attribute value to a value that can be sent to the panda."""
|
|
35
|
+
|
|
36
|
+
match fastcs_datatype:
|
|
37
|
+
case String():
|
|
38
|
+
return value
|
|
39
|
+
case Bool():
|
|
40
|
+
return str(int(value))
|
|
41
|
+
case Int() | Float():
|
|
42
|
+
return str(value)
|
|
43
|
+
case Enum():
|
|
44
|
+
return value.name
|
|
45
|
+
case Table():
|
|
46
|
+
assert isinstance(value, np.ndarray)
|
|
47
|
+
panda_value = {}
|
|
48
|
+
for field_name, _ in fastcs_datatype.structured_dtype:
|
|
49
|
+
panda_value[field_name.upper()] = value[field_name].tolist()
|
|
50
|
+
return panda_value
|
|
51
|
+
case _:
|
|
52
|
+
raise NotImplementedError(f"Unknown datatype {fastcs_datatype}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from ._annotations import (
|
|
4
|
+
RawBlocksType,
|
|
5
|
+
RawFieldsType,
|
|
6
|
+
RawInitialValuesType,
|
|
7
|
+
ResponseType,
|
|
8
|
+
)
|
|
9
|
+
from ._string_types import (
|
|
10
|
+
PANDA_SEPARATOR,
|
|
11
|
+
PandaName,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WidgetGroup(Enum):
|
|
16
|
+
"""Group that an attribute will be added to on the screen."""
|
|
17
|
+
|
|
18
|
+
NONE = None
|
|
19
|
+
PARAMETERS = "Parameters"
|
|
20
|
+
OUTPUTS = "Outputs"
|
|
21
|
+
INPUTS = "Inputs"
|
|
22
|
+
READBACKS = "Readbacks"
|
|
23
|
+
CAPTURE = "Capture"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"PANDA_SEPARATOR",
|
|
28
|
+
"PandaName",
|
|
29
|
+
"ResponseType",
|
|
30
|
+
"RawBlocksType",
|
|
31
|
+
"RawFieldsType",
|
|
32
|
+
"RawInitialValuesType",
|
|
33
|
+
"WidgetGroup",
|
|
34
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from pandablocks.responses import (
|
|
4
|
+
BitMuxFieldInfo,
|
|
5
|
+
BitOutFieldInfo,
|
|
6
|
+
BlockInfo,
|
|
7
|
+
EnumFieldInfo,
|
|
8
|
+
ExtOutBitsFieldInfo,
|
|
9
|
+
ExtOutFieldInfo,
|
|
10
|
+
FieldInfo,
|
|
11
|
+
PosMuxFieldInfo,
|
|
12
|
+
PosOutFieldInfo,
|
|
13
|
+
ScalarFieldInfo,
|
|
14
|
+
SubtypeTimeFieldInfo,
|
|
15
|
+
TableFieldInfo,
|
|
16
|
+
TimeFieldInfo,
|
|
17
|
+
UintFieldInfo,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from ._string_types import PandaName
|
|
21
|
+
|
|
22
|
+
# Pyright gives us variable not allowed in type expression error
|
|
23
|
+
# if we try to use the new (|) syntax
|
|
24
|
+
ResponseType = Union[
|
|
25
|
+
BitMuxFieldInfo,
|
|
26
|
+
BitOutFieldInfo,
|
|
27
|
+
EnumFieldInfo,
|
|
28
|
+
ExtOutBitsFieldInfo,
|
|
29
|
+
ExtOutFieldInfo,
|
|
30
|
+
FieldInfo,
|
|
31
|
+
PosMuxFieldInfo,
|
|
32
|
+
PosOutFieldInfo,
|
|
33
|
+
ScalarFieldInfo,
|
|
34
|
+
SubtypeTimeFieldInfo,
|
|
35
|
+
TableFieldInfo,
|
|
36
|
+
TimeFieldInfo,
|
|
37
|
+
UintFieldInfo,
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
RawBlocksType = dict[PandaName, BlockInfo]
|
|
41
|
+
RawFieldsType = list[dict[PandaName, ResponseType]]
|
|
42
|
+
RawInitialValuesType = dict[PandaName, str]
|