import logging
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import (
Any,
Callable,
Dict,
Generator,
Generic,
List,
Optional,
Tuple,
TypeVar,
Union,
overload,
)
from ._exchange import Exchange, ExchangeGenerator
from .responses import (
BitMuxFieldInfo,
BitOutFieldInfo,
BlockInfo,
Changes,
EnumFieldInfo,
ExtOutBitsFieldInfo,
ExtOutFieldInfo,
FieldInfo,
PosMuxFieldInfo,
PosOutFieldInfo,
ScalarFieldInfo,
SubtypeTimeFieldInfo,
TableFieldDetails,
TableFieldInfo,
TimeFieldInfo,
UintFieldInfo,
)
# Define the public API of this module
__all__ = [
"Command",
"CommandException",
"Raw",
"Get",
"GetLine",
"GetMultiline",
"Put",
"Append",
"Arm",
"Disarm",
"GetBlockInfo",
"GetFieldInfo",
"GetPcapBitsLabels",
"ChangeGroup",
"GetChanges",
"GetState",
"SetState",
]
T = TypeVar("T")
T2 = TypeVar("T2")
T3 = TypeVar("T3")
T4 = TypeVar("T4")
# Checks whether the server will interpret cmd as a table command: search for
# first of '?', '=', '<', if '<' found first then it's a multiline command.
MULTILINE_COMMAND = re.compile(r"^[^?=]*<")
def is_multiline_command(cmd: str):
return MULTILINE_COMMAND.match(cmd) is not None
[docs]
@dataclass
class Command(Generic[T]):
"""Abstract baseclass for all ControlConnection commands to be inherited from"""
def execute(self) -> ExchangeGenerator[T]:
# A generator that sends lines to the PandA, gets lines back, and returns a
# response
raise NotImplementedError(self)
[docs]
class CommandException(Exception):
"""Raised if a `Command` receives a mal-formed response"""
# `execute_commands()` actually returns a list with length equal to the number
# of tasks passed; however, Tuple is used similar to the annotation for
# zip() because typing does not support variadic type variables. See
# typeshed PR #1550 for discussion.
@overload
def _execute_commands(c1: Command[T]) -> ExchangeGenerator[Tuple[T]]: ...
@overload
def _execute_commands(
c1: Command[T], c2: Command[T2]
) -> ExchangeGenerator[Tuple[T, T2]]: ...
@overload
def _execute_commands(
c1: Command[T], c2: Command[T2], c3: Command[T3]
) -> ExchangeGenerator[Tuple[T, T2, T3]]: ...
@overload
def _execute_commands(
c1: Command[T], c2: Command[T2], c3: Command[T3], c4: Command[T4]
) -> ExchangeGenerator[Tuple[T, T2, T3, T4]]: ...
@overload
def _execute_commands(
*commands: Command[Any],
) -> ExchangeGenerator[Tuple[Any, ...]]: ...
def _execute_commands(*commands):
"""Call the `Command.execute` method on each of the commands to produce
some `Exchange` generators, which are yielded back to the connection,
then zip together the responses to those exchanges into a tuple"""
# If we add type annotations to this function then mypy complains:
# Overloaded function implementation does not accept all possible arguments
# As we want to type check this, we put the logic in _zip_with_return
ret = yield from _zip_with_return([command.execute() for command in commands])
return ret
def _zip_with_return(
generators: List[ExchangeGenerator[Any]],
) -> ExchangeGenerator[Tuple[Any, ...]]:
# Sentinel to show what generators are not yet exhausted
pending = object()
returns = [pending] * len(generators)
while True:
yields: List[Exchange] = []
for i, gen in enumerate(generators):
# If we haven't exhausted the generator
if returns[i] is pending:
try:
# Get the exchanges that it wants to fill in
exchanges = next(gen)
except StopIteration as e:
# Generator is exhausted, store its return value
returns[i] = e.value
else:
# Add the exchanges to the list
if isinstance(exchanges, list):
yields += exchanges
else:
yields.append(exchanges)
if yields:
# There were some Exchanges yielded, so yield them all up
# for the Connection to fill in
yield yields
else:
# All the generators are exhausted, so return the tuple of all
# their return values
return tuple(returns)
[docs]
@dataclass
class Raw(Command[List[str]]):
"""Send a raw command
Args:
inp: The input lines to send
For example::
Raw(["PCAP.ACTIVE?"]) -> ["OK =1"]
Raw(["SEQ1.TABLE>", "1", "1", "0", "0", ""]) -> ["OK"]
Raw(["SEQ1.TABLE?"]) -> ["!1", "!1", "!0", "!0", "."])
"""
inp: List[str]
def execute(self) -> ExchangeGenerator[List[str]]:
ex = Exchange(self.inp)
yield ex
return ex.received
[docs]
@dataclass
class Get(Command[Union[str, List[str]]]):
"""Get the value of a field or star command.
If the form of the expected return is known, consider using `GetLine`
or `GetMultiline` instead.
Args:
field: The field, attribute, or star command to get
For example::
Get("PCAP.ACTIVE") -> "1"
Get("SEQ1.TABLE") -> ["1048576", "0", "1000", "1000"]
Get("*IDN") -> "PandA 1.1..."
"""
field: str
def execute(self) -> ExchangeGenerator[Union[str, List[str]]]:
ex = Exchange(f"{self.field}?")
yield ex
if ex.is_multiline:
return ex.multiline
else:
# We got OK =value
line = ex.line
assert line.startswith("OK =")
return line[4:]
[docs]
@dataclass
class GetLine(Command[str]):
"""Get the value of a field or star command, when the result is expected to be a
single line.
Args:
field: The field, attribute, or star command to get
For example::
GetLine("PCAP.ACTIVE") -> "1"
GetLine("*IDN") -> "PandA 1.1..."
"""
field: str
def execute(self) -> ExchangeGenerator[str]:
ex = Exchange(f"{self.field}?")
yield ex
# Expect "OK =value"
line = ex.line
assert line.startswith("OK =")
return line[4:]
[docs]
@dataclass
class GetMultiline(Command[List[str]]):
"""Get the value of a field or star command, when the result is expected to be a
multiline response.
Args:
field: The field, attribute, or star command to get
For example::
GetMultiline("SEQ1.TABLE") -> ["1048576", "0", "1000", "1000"]
GetMultiline("*METADATA.*") -> ["LABEL_FILTER1", "APPNAME", ...]
"""
field: str
def execute(self) -> ExchangeGenerator[List[str]]:
ex = Exchange(f"{self.field}?")
yield ex
return ex.multiline
[docs]
@dataclass
class Put(Command[None]):
"""Put the value of a field.
Args:
field: The field, attribute, or star command to put
value: The value, either string or list of strings or empty, to put
For example::
Put("PCAP.TRIG", "PULSE1.OUT")
Put("SEQ1.TABLE", ["1048576", "0", "1000", "1000"])
Put("SFP3_SYNC_IN1.SYNC_RESET")
"""
field: str
value: Union[str, List[str]] = ""
def execute(self) -> ExchangeGenerator[None]:
if isinstance(self.value, list):
# Multiline table with blank line to terminate
ex = Exchange([f"{self.field}<"] + self.value + [""])
else:
ex = Exchange(f"{self.field}={self.value}")
yield ex
assert ex.line == "OK"
[docs]
@dataclass
class Append(Command[None]):
"""Append the value of a table field.
Args:
field: The field, attribute, or star command to append
value: The value, list of strings, to append
For example::
Append("SEQ1.TABLE", ["1048576", "0", "1000", "1000"])
"""
field: str
value: List[str]
def execute(self) -> ExchangeGenerator[None]:
# Multiline table with blank line to terminate
ex = Exchange([f"{self.field}<<"] + self.value + [""])
yield ex
assert ex.line == "OK"
[docs]
class Arm(Command[None]):
"""Arm PCAP for an acquisition by sending ``*PCAP.ARM=``"""
def execute(self) -> ExchangeGenerator[None]:
ex = Exchange("*PCAP.ARM=")
yield ex
assert ex.line == "OK"
[docs]
class Disarm(Command[None]):
"""Disarm PCAP, stopping acquisition by sending ``*PCAP.DISARM=``"""
def execute(self) -> ExchangeGenerator[None]:
ex = Exchange("*PCAP.DISARM=")
yield ex
assert ex.line == "OK"
[docs]
@dataclass
class GetBlockInfo(Command[Dict[str, BlockInfo]]):
"""Get the name, number, and description of each block type
in a dictionary, alphabetically ordered
Args:
skip_description: If `True`, prevents retrieving the description
for each Block. This will reduce network calls.
For example::
GetBlockInfo() ->
{
"LUT": BlockInfo(number=8, description="Lookup table"),
"PCAP": BlockInfo(number=1, description="Position capture control"),
...
}
"""
skip_description: bool = False
def execute(self) -> ExchangeGenerator[Dict[str, BlockInfo]]:
ex = Exchange("*BLOCKS?")
yield ex
blocks_list, commands = [], []
for line in ex.multiline:
block, num = line.split()
blocks_list.append((block, int(num)))
commands.append(GetLine(f"*DESC.{block}"))
if self.skip_description:
# Must use tuple() to match type returned by _execute_commands
description_values = tuple(None for _ in commands)
else:
description_values = yield from _execute_commands(*commands)
block_infos = {
block: BlockInfo(number=num, description=desc)
for (block, num), desc in sorted(zip(blocks_list, description_values))
}
return block_infos
# The type of the generators used for creating the Get commands for each field
# and setting the returned data into the FieldInfo structure
_FieldGeneratorType = Generator[
Union[Exchange, List[Exchange]],
None,
Tuple[str, FieldInfo],
]
[docs]
@dataclass
class GetFieldInfo(Command[Dict[str, FieldInfo]]):
"""Get the fields of a block, returning a `FieldInfo` (or appropriate subclass) for
each one, ordered to match the definition order in the PandA
Args:
block: The name of the block type
extended_metadata: If `True`, retrieves detailed metadata about a field and
all of its attributes. This will cause an additional network round trip.
If `False` only the field names and types will be returned. Default `True`.
For example::
GetFieldInfo("LUT") -> {
"INPA":
BitMuxFieldInfo(type='bit_mux',
subtype=None,
description='Input A',
max_delay=5
label=['TTLIN1.VAL', 'TTLIN2.VAL', ...]),
...}
"""
block: str
extended_metadata: bool = True
_commands_map: Dict[
Tuple[str, Optional[str]],
Callable[
[str, str, Optional[str]],
_FieldGeneratorType,
],
] = field(init=False, repr=False, default_factory=dict)
def __post_init__(self):
# Map a (type, subtype) to a method that returns the appropriate
# subclasss of FieldInfo, and a list of all the Commands to request.
# Note that fields that do not have additional attributes are not listed.
self._commands_map = {
# Order matches that of PandA server's Field Types docs
("time", None): self._time,
("bit_out", None): self._bit_out,
("pos_out", None): self._pos_out,
("ext_out", "timestamp"): self._ext_out,
("ext_out", "samples"): self._ext_out,
("ext_out", "bits"): self._ext_out_bits,
("bit_mux", None): self._bit_mux,
("pos_mux", None): self._pos_mux,
("table", None): self._table,
("param", "uint"): self._uint,
("read", "uint"): self._uint,
("write", "uint"): self._uint,
("param", "int"): self._no_attributes,
("read", "int"): self._no_attributes,
("write", "int"): self._no_attributes,
("param", "scalar"): self._scalar,
("read", "scalar"): self._scalar,
("write", "scalar"): self._scalar,
("param", "bit"): self._no_attributes,
("read", "bit"): self._no_attributes,
("write", "bit"): self._no_attributes,
("param", "action"): self._no_attributes,
("read", "action"): self._no_attributes,
("write", "action"): self._no_attributes,
("param", "lut"): self._no_attributes,
("read", "lut"): self._no_attributes,
("write", "lut"): self._no_attributes,
("param", "enum"): self._enum,
("read", "enum"): self._enum,
("write", "enum"): self._enum,
("param", "time"): self._subtype_time,
("read", "time"): self._subtype_time,
("write", "time"): self._subtype_time,
}
def _get_desc(self, field_name: str) -> GetLine:
"""Create the Command to retrieve the description"""
return GetLine(f"*DESC.{self.block}.{field_name}")
def _uint(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, maximum = yield from _execute_commands(
self._get_desc(field_name),
GetLine(f"{self.block}1.{field_name}.MAX"),
)
field_info = UintFieldInfo(field_type, field_subtype, desc, int(maximum))
return field_name, field_info
def _scalar(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, units, scale, offset = yield from _execute_commands(
self._get_desc(field_name),
GetLine(f"{self.block}.{field_name}.UNITS"),
GetLine(f"{self.block}.{field_name}.SCALE"),
GetLine(f"{self.block}.{field_name}.OFFSET"),
)
field_info = ScalarFieldInfo(
field_type, field_subtype, desc, units, float(scale), int(offset)
)
return field_name, field_info
def _subtype_time(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, units_labels = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"*ENUMS.{self.block}.{field_name}.UNITS"),
)
field_info = SubtypeTimeFieldInfo(field_type, field_subtype, desc, units_labels)
return field_name, field_info
def _enum(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, labels = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"*ENUMS.{self.block}.{field_name}"),
)
field_info = EnumFieldInfo(field_type, field_subtype, desc, labels)
return field_name, field_info
def _time(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, units, min = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"*ENUMS.{self.block}.{field_name}.UNITS"),
GetLine(f"{self.block}1.{field_name}.MIN"),
)
field_info = TimeFieldInfo(field_type, field_subtype, desc, units, float(min))
return field_name, field_info
def _bit_out(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, capture_word, offset = yield from _execute_commands(
self._get_desc(field_name),
GetLine(f"{self.block}1.{field_name}.CAPTURE_WORD"),
GetLine(f"{self.block}1.{field_name}.OFFSET"),
)
field_info = BitOutFieldInfo(
field_type, field_subtype, desc, capture_word, int(offset)
)
return field_name, field_info
def _bit_mux(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, max_delay, labels = yield from _execute_commands(
self._get_desc(field_name),
GetLine(f"{self.block}1.{field_name}.MAX_DELAY"),
GetMultiline(f"*ENUMS.{self.block}.{field_name}"),
)
field_info = BitMuxFieldInfo(
field_type, field_subtype, desc, int(max_delay), labels
)
return field_name, field_info
def _pos_mux(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, labels = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"*ENUMS.{self.block}.{field_name}"),
)
field_info = PosMuxFieldInfo(field_type, field_subtype, desc, labels)
return field_name, field_info
def _table(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
# Ignore the ROW_WORDS attribute as it's new and won't be present on all PandAs,
# and there's no easy way to try it and catch an error while also running other
# Get commands at the same time
table_desc, max_length, fields = yield from _execute_commands(
self._get_desc(field_name),
GetLine(f"{self.block}1.{field_name}.MAX_LENGTH"),
GetMultiline(f"{self.block}1.{field_name}.FIELDS"),
)
# Keep track of highest bit index
max_bit_offset: int = 0
desc_gets: List[GetLine] = []
enum_field_gets: List[GetMultiline] = []
enum_field_names: List[str] = []
fields_dict: Dict[str, TableFieldDetails] = {}
for field_details in fields:
# Fields are of the form <bit_high>:<bit_low> <name> <subtype>
bit_range, name, subtype = field_details.split()
bit_high_str, bit_low_str = bit_range.split(":")
bit_high = int(bit_high_str)
bit_low = int(bit_low_str)
if bit_high > max_bit_offset:
max_bit_offset = bit_high
if subtype == "enum":
enum_field_gets.append(
GetMultiline(f"*ENUMS.{self.block}1.{field_name}[].{name}")
)
enum_field_names.append(name)
fields_dict[name] = TableFieldDetails(subtype, bit_low, bit_high)
desc_gets.append(GetLine(f"*DESC.{self.block}1.{field_name}[].{name}"))
# Calculate the number of 32 bit words that comprises one table row
row_words = max_bit_offset // 32 + 1
# The first len(enum_field_gets) items are enum labels, type List[str]
# The second part of the list are descriptions, type str
labels_and_descriptions = yield from _execute_commands(
*enum_field_gets, *desc_gets
)
for name, labels in zip(
enum_field_names, labels_and_descriptions[: len(enum_field_gets)]
):
fields_dict[name].labels = labels
for name, desc in zip(
fields_dict.keys(), labels_and_descriptions[len(enum_field_gets) :]
):
fields_dict[name].description = desc
field_info = TableFieldInfo(
field_type,
field_subtype,
table_desc,
int(max_length),
fields_dict,
row_words,
)
return field_name, field_info
def _pos_out(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, capture_labels = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"*ENUMS.{self.block}.{field_name}.CAPTURE"),
)
field_info = PosOutFieldInfo(field_type, field_subtype, desc, capture_labels)
return field_name, field_info
def _ext_out(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, capture_labels = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"*ENUMS.{self.block}.{field_name}.CAPTURE"),
)
field_info = ExtOutFieldInfo(field_type, field_subtype, desc, capture_labels)
return field_name, field_info
def _ext_out_bits(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
desc, bits, capture_labels = yield from _execute_commands(
self._get_desc(field_name),
GetMultiline(f"{self.block}.{field_name}.BITS"),
GetMultiline(f"*ENUMS.{self.block}.{field_name}.CAPTURE"),
)
field_info = ExtOutBitsFieldInfo(
field_type, field_subtype, desc, capture_labels, bits
)
return field_name, field_info
def _no_attributes(
self, field_name: str, field_type: str, field_subtype: Optional[str]
) -> _FieldGeneratorType:
"""Calling this method indicates type-subtype pair is known and
has no attributes, so only a description needs to be retrieved"""
desc = yield from self._get_desc(field_name).execute()
return field_name, FieldInfo(field_type, field_subtype, desc)
def execute(self) -> ExchangeGenerator[Dict[str, FieldInfo]]:
ex = Exchange(f"{self.block}.*?")
yield ex
unsorted: Dict[int, Tuple[str, FieldInfo]] = {}
field_generators: List[ExchangeGenerator] = []
for line in ex.multiline:
field_name, index, type_subtype = line.split(maxsplit=2)
field_type: str
subtype: Optional[str]
# Append "None" to list below so there are always at least 2 elements
# so we can always unpack into subtype, even if no split occurs.
field_type, subtype, *_ = [*type_subtype.split(maxsplit=1), None]
# Always create default FieldInfo. If necessary we will replace it later
# with a more type-specific version.
field_info = FieldInfo(field_type, subtype, None)
if self.extended_metadata:
try:
# Construct the list of type-specific generators
field_generators.append(
self._commands_map[(field_type, subtype)](
field_name, field_type, subtype
)
)
except KeyError:
# This exception will be hit if PandA ever defines new types
logging.exception(
f"Unknown type {(field_type, subtype)} detected for "
f"{field_name}, cannot retrieve extended information for it."
)
# We can assume the new field will have a description though
field_generators.append(
self._no_attributes(field_name, field_type, subtype)
)
# Keep track of order of fields as returned by PandA. Important for later
# matching descriptions back to their field.
unsorted[int(index)] = (field_name, field_info)
# Dict keeps insertion order, so insert in the order the server said
fields = {name: field for _, (name, field) in sorted(unsorted.items())}
if self.extended_metadata is False:
# Asked to not perform the requests for extra metadata.
return fields
field_name_info: Tuple[Tuple[str, FieldInfo], ...]
field_name_info = yield from _zip_with_return(field_generators)
fields.update(field_name_info)
return fields
[docs]
class GetPcapBitsLabels(Command):
"""Get the labels for the bit fields in PCAP.
For example::
GetPcapBitsLabels() -> {"BITS0" : ["TTLIN1.VAL", "TTLIN2.VAL", ...], ...}
"""
def execute(self) -> ExchangeGenerator[Dict[str, List[str]]]:
ex = Exchange("PCAP.*?")
yield ex
bits_fields = []
for line in ex.multiline:
split = line.split()
if len(split) == 4:
field_name, _, field_type, field_subtype = split
if field_type == "ext_out" and field_subtype == "bits":
bits_fields.append(f"PCAP.{field_name}")
exchanges = [Exchange(f"{field}.BITS?") for field in bits_fields]
yield exchanges
bits = {field: ex.multiline for field, ex in zip(bits_fields, exchanges)}
return bits
[docs]
class ChangeGroup(Enum):
"""Which group of values to ask for ``*CHANGES`` on:
https://pandablocks-server.readthedocs.io/en/latest/commands.html#system-commands
"""
#: All the groups below
ALL = ""
#: Configuration settings
CONFIG = ".CONFIG"
#: Bits on the system bus
BITS = ".BITS"
#: Positions
POSN = ".POSN"
#: Polled read values
READ = ".READ"
#: Attributes (included capture enable flags)
ATTR = ".ATTR"
#: Table changes
TABLE = ".TABLE"
#: Table changes
METADATA = ".METADATA"
[docs]
@dataclass
class GetChanges(Command[Changes]):
"""Get a `Changes` object showing which fields have changed since the last
time this was called
Args:
group: Restrict to a particular `ChangeGroup`
get_multiline: If `True`, return values of multiline fields in the
`multiline_values` attribute. Note that this will invoke additional network
requests.
If `False` these fields will instead be returned in the `no_value`
attribute. Default value is `False`.
For example::
GetChanges() -> Changes(
value={"PCAP.TRIG": "PULSE1.OUT"},
no_value=["SEQ1.TABLE"],
in_error=["BAD.ENUM"],
multiline_values={}
)
GetChanges(ChangeGroup.ALL, True) -> Changes(
values={"PCAP.TRIG": "PULSE1.OUT"},
no_value=[],
in_error=["BAD.ENUM"],
multiline_values={"SEQ1.TABLE" : ["1", "2", "3",...]}
)
"""
group: ChangeGroup = ChangeGroup.ALL
get_multiline: bool = False
def execute(self) -> ExchangeGenerator[Changes]:
ex = Exchange(f"*CHANGES{self.group.value}?")
yield ex
changes = Changes({}, [], [], {})
multivalue_get_commands: List[Tuple[str, GetMultiline]] = []
for line in ex.multiline:
if line[-1] == "<":
if self.get_multiline:
field = line[0:-1]
multivalue_get_commands.append((field, GetMultiline(field)))
else:
changes.no_value.append(line[:-1])
elif line.endswith("(error)"):
changes.in_error.append(line.split(" ", 1)[0])
else:
field, value = line.split("=", maxsplit=1)
changes.values[field] = value
if self.get_multiline:
multiline_vals = yield from _execute_commands(
*[item[1] for item in multivalue_get_commands]
)
for field, value in zip(
[item[0] for item in multivalue_get_commands], multiline_vals
):
assert isinstance(value, list)
changes.multiline_values[field] = value
return changes
[docs]
@dataclass
class GetState(Command[List[str]]):
"""Get the state of all the fields in a PandA that should be saved as a
list of raw lines that could be sent with `SetState`.
NOTE: `GetState` may behave unexpectedly if `GetChanges` has previously been
called using the same client. The caller should use separate clients to avoid
potential issues.
For example::
GetState() -> [
"SEQ1.TABLE<B"
"234fds0SDklnmnr"
""
"PCAP.TRIG=PULSE1.OUT",
]
"""
def execute(self) -> ExchangeGenerator[List[str]]:
# TODO: explain in detail how this works
# See: references/how-it-works
attr, config, table, metadata = yield from _execute_commands(
GetChanges(ChangeGroup.ATTR),
GetChanges(ChangeGroup.CONFIG),
GetChanges(ChangeGroup.TABLE),
GetChanges(ChangeGroup.METADATA),
)
# Add the single line values
line_values = dict(**attr.values, **config.values, **metadata.values)
state = [f"{k}={v}" for k, v in line_values.items()]
# Get the multiline values
multiline_keys, commands = [], []
for field_name in table.no_value:
# Get tables as base64
multiline_keys.append(f"{field_name}<B")
commands.append(GetMultiline(f"{field_name}.B"))
for field_name in metadata.no_value:
# Get metadata as string list
multiline_keys.append(f"{field_name}<")
commands.append(GetMultiline(f"{field_name}"))
multiline_values = yield from _execute_commands(*commands)
for k, v in zip(multiline_keys, multiline_values):
state += [k] + v + [""]
return state
[docs]
@dataclass
class SetState(Command[None]):
"""Set the state of all the fields in a PandA
Args:
state: A list of raw lines as produced by `GetState`
For example::
SetState([
"SEQ1.TABLE<B"
"234fds0SDklnmnr"
""
"PCAP.TRIG=PULSE1.OUT",
])
"""
state: List[str]
def execute(self) -> ExchangeGenerator[None]:
commands: List[Raw] = []
command_lines: List[str] = []
for line in self.state:
command_lines.append(line)
first_line = len(command_lines) == 1
if (first_line and not is_multiline_command(line)) or not line:
# If not a multiline command
# Or blank line at the end of a multiline command
commands.append(Raw(command_lines))
command_lines = []
returns = yield from _execute_commands(*commands)
for command, ret in zip(commands, returns):
if ret != ["OK"]:
logging.warning(f"command {command.inp} failed with {ret}")