ophyd-async 0.13.3__py3-none-any.whl → 0.13.5__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.
Files changed (42) hide show
  1. ophyd_async/_version.py +2 -2
  2. ophyd_async/core/__init__.py +26 -3
  3. ophyd_async/core/_derived_signal_backend.py +2 -1
  4. ophyd_async/core/_detector.py +2 -2
  5. ophyd_async/core/_device.py +9 -9
  6. ophyd_async/core/_enums.py +5 -0
  7. ophyd_async/core/_signal.py +34 -38
  8. ophyd_async/core/_signal_backend.py +3 -1
  9. ophyd_async/core/_status.py +2 -2
  10. ophyd_async/core/_table.py +8 -0
  11. ophyd_async/core/_utils.py +11 -11
  12. ophyd_async/epics/adcore/_core_logic.py +3 -1
  13. ophyd_async/epics/adcore/_utils.py +4 -4
  14. ophyd_async/epics/core/_aioca.py +2 -2
  15. ophyd_async/epics/core/_p4p.py +2 -2
  16. ophyd_async/epics/motor.py +28 -7
  17. ophyd_async/epics/pmac/_pmac_io.py +8 -4
  18. ophyd_async/epics/pmac/_pmac_trajectory.py +144 -41
  19. ophyd_async/epics/pmac/_pmac_trajectory_generation.py +692 -0
  20. ophyd_async/epics/pmac/_utils.py +1 -681
  21. ophyd_async/fastcs/jungfrau/__init__.py +2 -1
  22. ophyd_async/fastcs/jungfrau/_controller.py +29 -11
  23. ophyd_async/fastcs/jungfrau/_utils.py +10 -2
  24. ophyd_async/fastcs/panda/__init__.py +10 -0
  25. ophyd_async/fastcs/panda/_block.py +14 -0
  26. ophyd_async/fastcs/panda/_trigger.py +123 -3
  27. ophyd_async/sim/_motor.py +4 -2
  28. ophyd_async/sim/_stage.py +14 -4
  29. ophyd_async/tango/core/__init__.py +17 -3
  30. ophyd_async/tango/core/_signal.py +18 -22
  31. ophyd_async/tango/core/_tango_transport.py +407 -239
  32. ophyd_async/tango/core/_utils.py +9 -0
  33. ophyd_async/tango/demo/_mover.py +1 -2
  34. ophyd_async/tango/testing/__init__.py +2 -1
  35. ophyd_async/tango/testing/_one_of_everything.py +13 -5
  36. ophyd_async/tango/testing/_test_config.py +11 -0
  37. ophyd_async/testing/_assert.py +2 -2
  38. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/METADATA +2 -36
  39. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/RECORD +42 -40
  40. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/WHEEL +0 -0
  41. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/licenses/LICENSE +0 -0
  42. {ophyd_async-0.13.3.dist-info → ophyd_async-0.13.5.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- from ._controller import JungfrauController
1
+ from ._controller import JUNGFRAU_DEADTIME_S, JungfrauController
2
2
  from ._jungfrau import Jungfrau
3
3
  from ._signals import (
4
4
  AcquisitionType,
@@ -26,4 +26,5 @@ __all__ = [
26
26
  "AcquisitionType",
27
27
  "GainMode",
28
28
  "PedestalMode",
29
+ "JUNGFRAU_DEADTIME_S",
29
30
  ]
@@ -113,20 +113,34 @@ class JungfrauController(DetectorController):
113
113
  ]
114
114
  )
115
115
  case AcquisitionType.PEDESTAL:
116
- coros.extend(
117
- [
118
- self._driver.pedestal_mode_frames.set(
119
- trigger_info.exposures_per_event
120
- ),
121
- self._driver.pedestal_mode_loops.set(
122
- trigger_info.number_of_events
123
- ),
124
- self._driver.pedestal_mode_state.set(PedestalMode.ON),
125
- ]
126
- )
116
+ if trigger_info.number_of_events % 2 == 0:
117
+ coros.extend(
118
+ [
119
+ self._driver.pedestal_mode_frames.set(
120
+ trigger_info.exposures_per_event
121
+ ),
122
+ # No. events is double the pedestal loops,
123
+ # since pedestal scan does the entire loop
124
+ # twice.
125
+ self._driver.pedestal_mode_loops.set(
126
+ int(trigger_info.number_of_events / 2)
127
+ ),
128
+ ]
129
+ )
130
+ else:
131
+ raise ValueError(
132
+ f"Invalid trigger info for pedestal mode. "
133
+ f"{trigger_info.number_of_events=} must be divisible by two. "
134
+ f"Was create_jungfrau_pedestal_triggering_info used?"
135
+ )
127
136
 
128
137
  await asyncio.gather(*coros)
129
138
 
139
+ # Setting signals once the detector is in pedestal mode can cause errors,
140
+ # so do this last
141
+ if acquisition_type == AcquisitionType.PEDESTAL:
142
+ await self._driver.pedestal_mode_state.set(PedestalMode.ON)
143
+
130
144
  async def arm(self):
131
145
  await self._driver.acquisition_start.trigger()
132
146
 
@@ -137,3 +151,7 @@ class JungfrauController(DetectorController):
137
151
 
138
152
  async def disarm(self):
139
153
  await self._driver.acquisition_stop.trigger()
154
+ await asyncio.gather(
155
+ self._driver.pedestal_mode_state.set(PedestalMode.OFF),
156
+ self._driver.acquisition_type.set(AcquisitionType.STANDARD),
157
+ )
@@ -2,6 +2,8 @@ from pydantic import PositiveInt
2
2
 
3
3
  from ophyd_async.core import DetectorTrigger, TriggerInfo
4
4
 
5
+ from ._controller import JUNGFRAU_DEADTIME_S
6
+
5
7
 
6
8
  def create_jungfrau_external_triggering_info(
7
9
  total_triggers: PositiveInt,
@@ -11,7 +13,7 @@ def create_jungfrau_external_triggering_info(
11
13
 
12
14
  Uses parameters which more closely-align with Jungfrau terminology
13
15
  to create TriggerInfo. This device currently only supports one frame per trigger
14
- when being externally triggered, but support for this can be added if needed
16
+ when being externally triggered.
15
17
 
16
18
  Args:
17
19
  total_triggers: Total external triggers expected before ending acquisition.
@@ -24,6 +26,7 @@ def create_jungfrau_external_triggering_info(
24
26
  number_of_events=total_triggers,
25
27
  trigger=DetectorTrigger.EDGE_TRIGGER,
26
28
  livetime=exposure_time_s,
29
+ deadtime=JUNGFRAU_DEADTIME_S,
27
30
  )
28
31
 
29
32
 
@@ -60,6 +63,11 @@ def create_jungfrau_pedestal_triggering_info(
60
63
  Uses parameters which more closely-align with Jungfrau terminology
61
64
  to create TriggerInfo.
62
65
 
66
+ When the Jungfrau is triggered in pedestal mode, it will run pedestal_frames-1
67
+ frames in dynamic gain mode, then one frame in gain mode 1, then repeat this for
68
+ pedelestal_loops number of times. This entire pattern is then repeated,
69
+ but gain mode 2 is used instead of gain mode 1 for the "one frame" part.
70
+
63
71
  NOTE: To trigger the jungfrau in pedestal mode, you must first set the
64
72
  jungfrau acquisition_type signal to AcquisitionType.PEDESTAL!
65
73
 
@@ -72,7 +80,7 @@ def create_jungfrau_pedestal_triggering_info(
72
80
  `TriggerInfo`
73
81
  """
74
82
  return TriggerInfo(
75
- number_of_events=pedestal_loops,
83
+ number_of_events=pedestal_loops * 2,
76
84
  exposures_per_event=pedestal_frames,
77
85
  trigger=DetectorTrigger.INTERNAL,
78
86
  livetime=exposure_time_s,
@@ -1,9 +1,11 @@
1
1
  from ._block import (
2
2
  CommonPandaBlocks,
3
3
  DataBlock,
4
+ InencBlock,
4
5
  PandaBitMux,
5
6
  PandaCaptureMode,
6
7
  PandaPcompDirection,
8
+ PandaPosMux,
7
9
  PandaTimeUnits,
8
10
  PcapBlock,
9
11
  PcompBlock,
@@ -20,6 +22,9 @@ from ._table import (
20
22
  )
21
23
  from ._trigger import (
22
24
  PcompInfo,
25
+ PosOutScaleOffset,
26
+ ScanSpecInfo,
27
+ ScanSpecSeqTableTriggerLogic,
23
28
  SeqTableInfo,
24
29
  StaticPcompTriggerLogic,
25
30
  StaticSeqTableTriggerLogic,
@@ -29,11 +34,13 @@ from ._writer import PandaHDFWriter
29
34
  __all__ = [
30
35
  "CommonPandaBlocks",
31
36
  "DataBlock",
37
+ "InencBlock",
32
38
  "PandaBitMux",
33
39
  "PandaCaptureMode",
34
40
  "PcapBlock",
35
41
  "PcompBlock",
36
42
  "PandaPcompDirection",
43
+ "PandaPosMux",
37
44
  "PulseBlock",
38
45
  "SeqBlock",
39
46
  "PandaTimeUnits",
@@ -48,4 +55,7 @@ __all__ = [
48
55
  "SeqTableInfo",
49
56
  "StaticPcompTriggerLogic",
50
57
  "StaticSeqTableTriggerLogic",
58
+ "ScanSpecInfo",
59
+ "ScanSpecSeqTableTriggerLogic",
60
+ "PosOutScaleOffset",
51
61
  ]
@@ -58,6 +58,12 @@ class PandaBitMux(SubsetEnum):
58
58
  ONE = "ONE"
59
59
 
60
60
 
61
+ class PandaPosMux(SubsetEnum):
62
+ """Pos input in the PandA."""
63
+
64
+ ZERO = "ZERO"
65
+
66
+
61
67
  class PcompBlock(Device):
62
68
  """Position compare block in the PandA."""
63
69
 
@@ -88,6 +94,7 @@ class SeqBlock(Device):
88
94
  prescale: SignalRW[float]
89
95
  prescale_units: SignalRW[PandaTimeUnits]
90
96
  enable: SignalRW[PandaBitMux]
97
+ posa: SignalRW[PandaPosMux]
91
98
 
92
99
 
93
100
  class PcapBlock(Device):
@@ -97,6 +104,13 @@ class PcapBlock(Device):
97
104
  arm: SignalRW[bool]
98
105
 
99
106
 
107
+ class InencBlock(Device):
108
+ """In encoder block in the PandA."""
109
+
110
+ val_scale: SignalRW[float]
111
+ val_offset: SignalRW[float]
112
+
113
+
100
114
  class CommonPandaBlocks(Device):
101
115
  """Pandablocks device with blocks which are common and required on introspection."""
102
116
 
@@ -1,17 +1,33 @@
1
+ from __future__ import annotations
2
+
1
3
  import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import cast
2
6
 
7
+ import numpy as np
3
8
  from pydantic import Field
4
-
5
- from ophyd_async.core import ConfinedModel, FlyerController, wait_for_value
9
+ from scanspec.core import Path
10
+ from scanspec.specs import Spec
11
+
12
+ from ophyd_async.core import (
13
+ ConfinedModel,
14
+ FlyerController,
15
+ SignalRW,
16
+ error_if_none,
17
+ wait_for_value,
18
+ )
19
+ from ophyd_async.epics.motor import Motor
6
20
 
7
21
  from ._block import (
22
+ CommonPandaBlocks,
8
23
  PandaBitMux,
9
24
  PandaPcompDirection,
25
+ PandaPosMux,
10
26
  PandaTimeUnits,
11
27
  PcompBlock,
12
28
  SeqBlock,
13
29
  )
14
- from ._table import SeqTable
30
+ from ._table import SeqTable, SeqTrigger
15
31
 
16
32
 
17
33
  class SeqTableInfo(ConfinedModel):
@@ -22,6 +38,11 @@ class SeqTableInfo(ConfinedModel):
22
38
  prescale_as_us: float = Field(default=1, ge=0) # microseconds
23
39
 
24
40
 
41
+ class ScanSpecInfo(ConfinedModel):
42
+ spec: Spec[Motor]
43
+ deadtime: float
44
+
45
+
25
46
  class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
26
47
  """For controlling the PandA `SeqTable` when fly scanning."""
27
48
 
@@ -51,6 +72,105 @@ class StaticSeqTableTriggerLogic(FlyerController[SeqTableInfo]):
51
72
  await wait_for_value(self.seq.active, False, timeout=1)
52
73
 
53
74
 
75
+ @dataclass
76
+ class PosOutScaleOffset:
77
+ name: str
78
+ scale: SignalRW[float]
79
+ offset: SignalRW[float]
80
+
81
+ @classmethod
82
+ def from_inenc(cls, panda: CommonPandaBlocks, number: int) -> PosOutScaleOffset:
83
+ inenc = panda.inenc[number] # type: ignore
84
+ return cls(
85
+ name=f"INENC{number}.VAL",
86
+ scale=inenc.val_scale, # type: ignore
87
+ offset=inenc.val_offset, # type: ignore
88
+ )
89
+
90
+
91
+ class ScanSpecSeqTableTriggerLogic(FlyerController[ScanSpecInfo]):
92
+ def __init__(
93
+ self,
94
+ seq: SeqBlock,
95
+ motor_pos_outs: dict[Motor, PosOutScaleOffset] | None = None,
96
+ ) -> None:
97
+ self.seq = seq
98
+ self.motor_pos_outs = motor_pos_outs or {}
99
+
100
+ async def prepare(self, value: ScanSpecInfo):
101
+ await self.seq.enable.set(PandaBitMux.ZERO)
102
+ slice = Path(value.spec.calculate()).consume()
103
+ slice_duration = error_if_none(slice.duration, "Slice must have duration")
104
+
105
+ # Start of window is where the is a gap to the previous point
106
+ window_start = np.nonzero(slice.gap)[0]
107
+ # End of window is either the next gap, or the end of the scan
108
+ window_end = np.append(window_start[1:], len(slice))
109
+ fast_axis = slice.axes()[-1]
110
+ pos_out = self.motor_pos_outs.get(fast_axis)
111
+ # If we have a motor to compare against, get its scale and offset
112
+ # otherwise don't connect POSA to anything
113
+ if pos_out is not None:
114
+ scale, offset = await asyncio.gather(
115
+ pos_out.scale.get_value(),
116
+ pos_out.offset.get_value(),
117
+ )
118
+ compare_pos_name = cast(PandaPosMux, pos_out.name)
119
+ else:
120
+ scale, offset = 1, 0
121
+ compare_pos_name = PandaPosMux.ZERO
122
+
123
+ rows = SeqTable.empty()
124
+ for start, end in zip(window_start, window_end, strict=True):
125
+ # GPIO goes low then high
126
+ rows += SeqTable.row(trigger=SeqTrigger.BITA_0)
127
+ rows += SeqTable.row(trigger=SeqTrigger.BITA_1)
128
+ # Wait for position if we are comparing against a motor
129
+ if pos_out is not None:
130
+ lower = (slice.lower[fast_axis][start] - offset) / scale
131
+ midpoint = (slice.midpoints[fast_axis][start] - offset) / scale
132
+ if midpoint > lower:
133
+ trigger = SeqTrigger.POSA_GT
134
+ elif midpoint < lower:
135
+ trigger = SeqTrigger.POSA_LT
136
+ else:
137
+ trigger = None
138
+ if trigger is not None:
139
+ rows += SeqTable.row(
140
+ trigger=trigger,
141
+ position=int(lower),
142
+ )
143
+
144
+ # Time based Triggers
145
+ rows += SeqTable.row(
146
+ repeats=end - start,
147
+ trigger=SeqTrigger.IMMEDIATE,
148
+ time1=int((slice_duration[0] - value.deadtime) * 10**6),
149
+ time2=int(value.deadtime * 10**6),
150
+ outa1=True,
151
+ outa2=False,
152
+ )
153
+ # Need to do units before value for PandA, otherwise it scales the current value
154
+ await self.seq.prescale_units.set(PandaTimeUnits.US)
155
+ await asyncio.gather(
156
+ self.seq.posa.set(compare_pos_name),
157
+ self.seq.prescale.set(1.0),
158
+ self.seq.repeats.set(1),
159
+ self.seq.table.set(rows),
160
+ )
161
+
162
+ async def kickoff(self) -> None:
163
+ await self.seq.enable.set(PandaBitMux.ONE)
164
+ await wait_for_value(self.seq.active, True, timeout=1)
165
+
166
+ async def complete(self) -> None:
167
+ await wait_for_value(self.seq.active, False, timeout=None)
168
+
169
+ async def stop(self):
170
+ await self.seq.enable.set(PandaBitMux.ZERO)
171
+ await wait_for_value(self.seq.active, False, timeout=1)
172
+
173
+
54
174
  class PcompInfo(ConfinedModel):
55
175
  """Info for the PandA `PcompBlock` for fly scanning."""
56
176
 
ophyd_async/sim/_motor.py CHANGED
@@ -82,8 +82,10 @@ class SimMotor(StandardReadable, Stoppable, Subscribable[float], Locatable[float
82
82
  )
83
83
  return Location(setpoint=setpoint, readback=readback)
84
84
 
85
- def subscribe(self, function: Callback[dict[str, Reading[float]]]) -> None:
86
- self.user_readback.subscribe(function)
85
+ def subscribe_reading(self, function: Callback[dict[str, Reading[float]]]) -> None:
86
+ self.user_readback.subscribe_reading(function)
87
+
88
+ subscribe = subscribe_reading
87
89
 
88
90
  def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None:
89
91
  self.user_readback.clear_sub(function)
ophyd_async/sim/_stage.py CHANGED
@@ -1,3 +1,5 @@
1
+ from bluesky.protocols import Reading
2
+
1
3
  from ophyd_async.core import StandardReadable
2
4
  from ophyd_async.sim._pattern_generator import PatternGenerator
3
5
 
@@ -16,15 +18,23 @@ class SimStage(StandardReadable):
16
18
  # Set name of device and child devices
17
19
  super().__init__(name=name)
18
20
 
21
+ def _set_x_from_reading(self, readings: dict[str, Reading[float]]):
22
+ (x_reading,) = readings.values()
23
+ self.pattern_generator.set_x(x_reading["value"])
24
+
25
+ def _set_y_from_reading(self, readings: dict[str, Reading[float]]):
26
+ (y_reading,) = readings.values()
27
+ self.pattern_generator.set_y(y_reading["value"])
28
+
19
29
  def stage(self):
20
30
  """Stage the motors and report the position to the pattern generator."""
21
31
  # Tell the pattern generator about the motor positions
22
- self.x.user_readback.subscribe_value(self.pattern_generator.set_x)
23
- self.y.user_readback.subscribe_value(self.pattern_generator.set_y)
32
+ self.x.user_readback.subscribe_reading(self._set_x_from_reading)
33
+ self.y.user_readback.subscribe_reading(self._set_y_from_reading)
24
34
  return super().stage()
25
35
 
26
36
  def unstage(self):
27
37
  """Unstage the motors and remove the position subscription."""
28
- self.x.user_readback.clear_sub(self.pattern_generator.set_x)
29
- self.y.user_readback.clear_sub(self.pattern_generator.set_y)
38
+ self.x.user_readback.clear_sub(self._set_x_from_reading)
39
+ self.y.user_readback.clear_sub(self._set_y_from_reading)
30
40
  return super().unstage()
@@ -12,24 +12,35 @@ from ._tango_readable import TangoReadable
12
12
  from ._tango_transport import (
13
13
  AttributeProxy,
14
14
  CommandProxy,
15
+ CommandProxyReadCharacter,
16
+ TangoDoubleStringTable,
17
+ TangoLongStringTable,
15
18
  TangoSignalBackend,
16
19
  ensure_proper_executor,
20
+ get_command_character,
17
21
  get_dtype_extended,
18
22
  get_python_type,
23
+ get_source_metadata,
19
24
  get_tango_trl,
20
- get_trl_descriptor,
21
25
  )
22
- from ._utils import DevStateEnum, get_device_trl_and_attr, get_full_attr_trl
26
+ from ._utils import (
27
+ DevStateEnum,
28
+ get_device_trl_and_attr,
29
+ get_full_attr_trl,
30
+ try_to_cast_as_float,
31
+ )
23
32
 
24
33
  __all__ = [
25
34
  "AttributeProxy",
26
35
  "CommandProxy",
36
+ "CommandProxyReadCharacter",
27
37
  "DevStateEnum",
28
38
  "ensure_proper_executor",
29
39
  "TangoSignalBackend",
40
+ "get_command_character",
30
41
  "get_python_type",
31
42
  "get_dtype_extended",
32
- "get_trl_descriptor",
43
+ "get_source_metadata",
33
44
  "get_tango_trl",
34
45
  "infer_python_type",
35
46
  "infer_signal_type",
@@ -42,6 +53,9 @@ __all__ = [
42
53
  "TangoReadable",
43
54
  "TangoPolling",
44
55
  "TangoDeviceConnector",
56
+ "TangoLongStringTable",
57
+ "TangoDoubleStringTable",
58
+ "try_to_cast_as_float",
45
59
  "get_device_trl_and_attr",
46
60
  "get_full_attr_trl",
47
61
  ]
@@ -3,13 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
- from enum import Enum, IntEnum
6
+ from enum import IntEnum
7
7
 
8
8
  import numpy.typing as npt
9
9
  from tango import (
10
- AttrDataFormat,
11
10
  AttrWriteType,
12
- CmdArgType,
13
11
  DeviceProxy,
14
12
  DevState,
15
13
  )
@@ -25,7 +23,12 @@ from ophyd_async.core import (
25
23
  SignalX,
26
24
  )
27
25
 
28
- from ._tango_transport import TangoSignalBackend, get_python_type
26
+ from ._tango_transport import (
27
+ CommandProxyReadCharacter,
28
+ TangoSignalBackend,
29
+ get_command_character,
30
+ get_python_type,
31
+ )
29
32
  from ._utils import get_device_trl_and_attr
30
33
 
31
34
  logger = logging.getLogger("ophyd_async")
@@ -148,22 +151,13 @@ async def infer_python_type(
148
151
 
149
152
  if tr_name in dev_proxy.get_command_list():
150
153
  config = await dev_proxy.get_command_config(tr_name)
151
- isarray, py_type, _ = get_python_type(config.in_type)
154
+ py_type = get_python_type(config)
152
155
  elif tr_name in dev_proxy.get_attribute_list():
153
156
  config = await dev_proxy.get_attribute_config(tr_name)
154
- isarray, py_type, _ = get_python_type(config.data_type)
155
- if py_type is Enum:
156
- enum_dict = {label: i for i, label in enumerate(config.enum_labels)}
157
- py_type = IntEnum("TangoEnum", enum_dict)
158
- if config.data_format in [AttrDataFormat.SPECTRUM, AttrDataFormat.IMAGE]:
159
- isarray = True
157
+ py_type = get_python_type(config)
160
158
  else:
161
159
  raise RuntimeError(f"Cannot find {tr_name} in {device_trl}")
162
-
163
- if py_type is CmdArgType.DevState:
164
- py_type = DevState
165
-
166
- return npt.NDArray[py_type] if isarray else py_type
160
+ return py_type
167
161
 
168
162
 
169
163
  async def infer_signal_type(
@@ -190,11 +184,13 @@ async def infer_signal_type(
190
184
 
191
185
  if tr_name in dev_proxy.get_command_list():
192
186
  config = await dev_proxy.get_command_config(tr_name)
193
- if config.in_type == CmdArgType.DevVoid:
194
- return SignalX
195
- elif config.in_type != config.out_type:
196
- logger.debug("Commands with different in and out dtypes are not supported")
197
- return None
198
- else:
187
+ command_character = get_command_character(config)
188
+ if command_character == CommandProxyReadCharacter.READ:
189
+ return SignalR
190
+ elif command_character == CommandProxyReadCharacter.WRITE:
191
+ return SignalW
192
+ elif command_character == CommandProxyReadCharacter.READ_WRITE:
199
193
  return SignalRW
194
+ elif command_character == CommandProxyReadCharacter.EXECUTE:
195
+ return SignalX
200
196
  raise RuntimeError(f"Unable to infer signal character for {trl}")