Skip to content

Commit 2c546da

Browse files
authored
Merge pull request #141 from DiamondLightSource/block_command_records_during_callback
Add Blocking Flag to Command PVs
2 parents 1cce8ea + 72aeb86 commit 2c546da

File tree

5 files changed

+68
-9
lines changed

5 files changed

+68
-9
lines changed

src/fastcs/transport/epics/ca/ioc.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ async def wrapped_method(_: Any):
240240
record = builder.Action(
241241
f"{pv_prefix}:{pv_name}",
242242
on_update=wrapped_method,
243+
blocking=True,
243244
)
244245

245246
_add_attr_pvi_info(record, pv_prefix, attr_name, "x")

src/fastcs/transport/epics/gui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from pvi._format.dls import DLSFormatter
1+
from pvi._format.dls import DLSFormatter # type: ignore
22
from pvi.device import (
33
LED,
44
ButtonPanel,

src/fastcs/transport/epics/pva/_pv_handlers.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,21 @@ async def put(self, pv: SharedPV, op: ServerOperation):
8181
"Maybe the command should spawn an asyncio task?"
8282
)
8383

84+
# Check if record block request recieved
85+
match op.pvRequest().todict():
86+
case {"record": {"_options": {"block": "true"}}}:
87+
blocking = True
88+
case _:
89+
blocking = False
90+
8491
# Flip to true once command task starts
8592
pv.post({"value": True, **p4p_timestamp_now(), **p4p_alarm_states()})
86-
op.done()
93+
if not blocking:
94+
op.done()
8795
alarm_states = await self._run_command()
8896
pv.post({"value": False, **p4p_timestamp_now(), **alarm_states})
97+
if blocking:
98+
op.done()
8999
else:
90100
raise RuntimeError("Commands should only take the value `True`.")
91101

tests/transport/epics/ca/test_softioc.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ def test_ioc(mocker: MockerFixture, epics_controller_api: ControllerAPI):
288288
epics_controller_api.attributes["write_bool"].datatype
289289
),
290290
)
291-
ioc_builder.Action.assert_any_call(f"{DEVICE}:Go", on_update=mocker.ANY)
291+
ioc_builder.Action.assert_any_call(
292+
f"{DEVICE}:Go", on_update=mocker.ANY, blocking=True
293+
)
292294

293295
# Check info tags are added
294296
add_pvi_info.assert_called_once_with(f"{DEVICE}:PVI")
@@ -474,8 +476,7 @@ def test_long_pv_names_discarded(mocker: MockerFixture):
474476

475477
short_command_pv_name = "command_short_name".title().replace("_", "")
476478
ioc_builder.Action.assert_called_once_with(
477-
f"{DEVICE}:{short_command_pv_name}",
478-
on_update=mocker.ANY,
479+
f"{DEVICE}:{short_command_pv_name}", on_update=mocker.ANY, blocking=True
479480
)
480481
with pytest.raises(AssertionError):
481482
long_command_pv_name = long_command_name.title().replace("_", "")

tests/transport/epics/pva/test_p4p.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -574,11 +574,15 @@ async def some_task():
574574
async def put_pvs():
575575
await asyncio.sleep(0.1)
576576
ctxt = Context("pva")
577-
await ctxt.put(f"{pv_prefix}:CommandSpawnsATask", True)
578-
await ctxt.put(f"{pv_prefix}:CommandSpawnsATask", True)
579-
await ctxt.put(f"{pv_prefix}:CommandRunsForAWhile", True)
577+
await asyncio.gather(
578+
ctxt.put(f"{pv_prefix}:CommandSpawnsATask", True),
579+
ctxt.put(f"{pv_prefix}:CommandSpawnsATask", True),
580+
)
580581
assert expected_error_string not in caplog.text
581-
await ctxt.put(f"{pv_prefix}:CommandRunsForAWhile", True)
582+
await asyncio.gather(
583+
ctxt.put(f"{pv_prefix}:CommandRunsForAWhile", True),
584+
ctxt.put(f"{pv_prefix}:CommandRunsForAWhile", True),
585+
)
582586
assert expected_error_string in caplog.text
583587

584588
serve = asyncio.ensure_future(fastcs.serve())
@@ -623,3 +627,46 @@ async def put_pvs():
623627
pytest.approx((coro_end_time - coro_start_time).total_seconds(), abs=0.05)
624628
== 0.1
625629
)
630+
631+
632+
def test_block_flag_waits_for_callback_completion():
633+
class SomeController(Controller):
634+
@command()
635+
async def command_runs_for_a_while(self):
636+
await asyncio.sleep(0.2)
637+
638+
controller = SomeController()
639+
pv_prefix = str(uuid4())
640+
fastcs = make_fastcs(pv_prefix, controller)
641+
command_runs_for_a_while_times = []
642+
643+
async def put_pvs():
644+
ctxt = Context("pva")
645+
for block in [True, False]:
646+
start_time = datetime.now()
647+
await ctxt.put(
648+
f"{pv_prefix}:CommandRunsForAWhile",
649+
True,
650+
wait=block,
651+
)
652+
command_runs_for_a_while_times.append((start_time, datetime.now()))
653+
654+
serve = asyncio.ensure_future(fastcs.serve())
655+
try:
656+
asyncio.get_event_loop().run_until_complete(
657+
asyncio.wait_for(
658+
asyncio.gather(serve, put_pvs()),
659+
timeout=0.5,
660+
)
661+
)
662+
except TimeoutError:
663+
...
664+
serve.cancel()
665+
666+
assert len(command_runs_for_a_while_times) == 2
667+
668+
for put_call, expected_duration in enumerate([0.2, 0]):
669+
start, end = command_runs_for_a_while_times[put_call]
670+
assert (
671+
pytest.approx((end - start).total_seconds(), abs=0.05) == expected_duration
672+
)

0 commit comments

Comments
 (0)