Skip to content

Commit

Permalink
Support motor blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
Tom-Willemsen committed Aug 11, 2024
1 parent 05c61d6 commit 0356f48
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 8 deletions.
11 changes: 6 additions & 5 deletions src/ibex_bluesky_core/demo_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from typing import Generator

import bluesky.plan_stubs as bps
from bluesky.callbacks import LiveTable
from bluesky.preprocessors import run_decorator
from bluesky.utils import Msg
from ophyd_async.epics.motor import Motor
from ophyd_async.plan_stubs import ensure_connected

from ibex_bluesky_core.devices import get_pv_prefix
from ibex_bluesky_core.devices.block import BlockRwRbv, block_rw_rbv
from ibex_bluesky_core.devices.block import block_mot
from ibex_bluesky_core.devices.dae import Dae
from ibex_bluesky_core.run_engine import get_run_engine

Expand All @@ -27,13 +29,12 @@ def run_demo_plan() -> None:
"""
RE = get_run_engine()
prefix = get_pv_prefix()
block = block_rw_rbv(float, "mot")
block = block_mot("mot")
dae = Dae(prefix)
# RE(demo_plan(block, dae), LiveTable(["mot", "DAE"]))
RE(demo_plan(block, dae), print)
RE(demo_plan(block, dae), LiveTable(["mot", "DAE"]))


def demo_plan(block: BlockRwRbv, dae: Dae) -> Generator[Msg, None, None]:
def demo_plan(block: Motor, dae: Dae) -> Generator[Msg, None, None]:
"""Demonstration plan which moves a block and reads the DAE."""
yield from ensure_connected(block, dae, force_reconnect=True)

Expand Down
58 changes: 55 additions & 3 deletions src/ibex_bluesky_core/devices/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
StandardReadable,
observe_value,
)
from ophyd_async.epics.motor import Motor
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw

from ibex_bluesky_core.devices import get_pv_prefix
Expand All @@ -29,6 +30,7 @@
"block_r",
"block_rw",
"block_rw_rbv",
"block_mot",
]


Expand Down Expand Up @@ -215,14 +217,20 @@ async def locate(self) -> Location[T]:


def block_r(datatype: Type[T], block_name: str) -> BlockR[T]:
"""Get a local read-only block for the current instrument."""
"""Get a local read-only block for the current instrument.
See documentation of BlockR for more information.
"""
return BlockR(datatype=datatype, prefix=get_pv_prefix(), block_name=block_name)


def block_rw(
datatype: Type[T], block_name: str, *, write_config: BlockWriteConfiguration[T] | None = None
) -> BlockRw[T]:
"""Get a local read-write block for the current instrument."""
"""Get a local read-write block for the current instrument.
See documentation of BlockRw for more information.
"""
return BlockRw(
datatype=datatype, prefix=get_pv_prefix(), block_name=block_name, write_config=write_config
)
Expand All @@ -231,7 +239,51 @@ def block_rw(
def block_rw_rbv(
datatype: Type[T], block_name: str, *, write_config: BlockWriteConfiguration[T] | None = None
) -> BlockRwRbv[T]:
"""Get a local read/write/setpoint readback block for the current instrument."""
"""Get a local read/write/setpoint readback block for the current instrument.
See documentation of BlockRwRbv for more information.
"""
return BlockRwRbv(
datatype=datatype, prefix=get_pv_prefix(), block_name=block_name, write_config=write_config
)


def block_mot(block_name: str) -> Motor:
"""Get a local block pointing at a motor record for the local instrument.
The 'Motor' object supports motion-specific functionality such as:
- Stopping if a scan is aborted (supports the bluesky 'Stoppable' protocol)
- Limit checking (before a move starts - supports the bluesky 'Checkable' protocol)
- Automatic calculation of move timeouts based on motor velocity
- Fly scanning
However, it generally relies on the underlying motor being "well-behaved". For example, a motor
which does many retries may exceed the simple default timeout based on velocity (it is possible
to explicitly specify a timeout on set() to override this).
Blocks pointing at motors do not take a BlockWriteConfiguration parameter, as these parameters
duplicate functionality which already exists in the motor record. The mapping is:
use_completion_callback:
Motors always use completion callbacks to check whether motion has completed. Whether to
wait on that completion callback can be configured by the 'wait' keyword argument on set().
set_success_func:
Use .RDBD and .RTRY to control motor retries if the position has not been reached to within
a specified tolerance. Note that motors which retry a lot may exceed the default motion
timeout which is calculated based on velocity, distance and acceleration.
set_timeout_s:
A suitable timeout is calculated automatically based on velocity, distance and acceleration
as defined on the motor record. This may be overridden by the 'timeout' keyword-argument on
set().
settle_time_s:
Use .DLY on the motor record to configure this.
"""
# GWBLOCK aliases .VAL to .RBV on a motor record for a block pointing at MOT:MTRxxxx.RBV, which
# is what we have recommended to our users for motor blocks... That means that you can't write
# to .VAL on a motor block. ophyd_async (reasonably) assumes you can write to .VAL for a motor
# which you want to move.
#
# However, we also have motor record aliases for :SP and :SP:RBV, which *don't* get mangled by
# GWBLOCK in that way. So by pointing at CS:SB:blockname:SP:RBV rather than CS:SB:blockname
# here, we avoid a write access exception when moving a motor block.
return Motor(f"{get_pv_prefix()}CS:SB:{block_name}:SP:RBV", name=block_name)
17 changes: 17 additions & 0 deletions tests/devices/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ibex_bluesky_core.devices.block import (
BlockRwRbv,
BlockWriteConfiguration,
block_mot,
block_r,
block_rw,
block_rw_rbv,
Expand Down Expand Up @@ -196,3 +197,19 @@ async def test_runcontrol_hints(simple_block):
async def test_runcontrol_monitors_correct_pv(simple_block):
source = simple_block.run_control.in_range.source
assert source.endswith("UNITTEST:MOCK:CS:SB:float_block:RC:INRANGE")


def test_block_mot():
with patch("ibex_bluesky_core.devices.block.get_pv_prefix") as mock_get_prefix:
mock_get_prefix.return_value = MOCK_PREFIX
mot = block_mot("foo")

# Slightly counterintuitive, but looking at foo:SP:RBV here is INTENTIONAL and NECESSARY.
# GWBLOCK mangles foo.VAL and foo.RBV (to make them display nicely in the GUI), but that
# mangling *breaks* ophyd-async. The mangling is not applied to the :SP:RBV alias, so we use
# that instead to preserve sane motor record behaviour.
assert mot.user_setpoint.source.endswith("UNITTEST:MOCK:CS:SB:foo:SP:RBV.VAL")
assert mot.user_readback.source.endswith("UNITTEST:MOCK:CS:SB:foo:SP:RBV.RBV")
assert mot.name == "foo"
assert mot.user_readback.name == "foo"
assert mot.user_setpoint.name == "foo-user_setpoint"

0 comments on commit 0356f48

Please sign in to comment.