Skip to content

Commit

Permalink
Merge branch 'main' into async-ad
Browse files Browse the repository at this point in the history
  • Loading branch information
canismarko authored Oct 22, 2024
2 parents 5332b97 + a2224c1 commit e772685
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 102 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "haven-spc"
version = "2024.10.4"
version = "2024.10.5"
authors = [
{ name="Mark Wolfman", email="[email protected]" },
]
Expand Down
2 changes: 1 addition & 1 deletion src/firefly/run_browser.ui
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
<item>
<widget class="QLabel" name="sleep_label">
<property name="text">
<string>&lt;- Press the button</string>
<string> Press the button</string>
</property>
</widget>
</item>
Expand Down
4 changes: 2 additions & 2 deletions src/firefly/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ def add_shutter_widgets(self):
for shutter in shutters[::-1]:
# Add a layout with the buttons
layout = QHBoxLayout()
name = shutter.attr_name if shutter.attr_name != "" else shutter.name
label = name_to_title(name) + ":"
label = name_to_title(shutter.name) + ":"
form.insertRow(row_idx, label, layout)
# Indicator to show if the shutter is open
indicator = PyDMByteIndicator(
Expand All @@ -62,6 +61,7 @@ def add_shutter_widgets(self):
relative=False,
init_channel=f"haven://{shutter.name}.setpoint",
)
print(f"{shutter.name} - {getattr(shutter, 'allow_open', True)=}")
open_btn.setEnabled(getattr(shutter, "allow_open", True))
layout.addWidget(open_btn)
# Button to close the shutter
Expand Down
46 changes: 3 additions & 43 deletions src/haven/devices/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,17 @@
safe_ophyd_name = lambda n: n
from ophyd import Component as Cpt
from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO, Kind
from ophyd_async.core import (
CALCULATE_TIMEOUT,
DEFAULT_TIMEOUT,
AsyncStatus,
CalculatableTimeout,
ConfigSignal,
SignalBackend,
SignalX,
SubsetEnum,
)
from ophyd_async.core import DEFAULT_TIMEOUT, ConfigSignal, SubsetEnum
from ophyd_async.epics.motor import Motor as MotorBase
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
from ophyd_async.epics.signal._signal import _epics_signal_backend
from ophydregistry import Registry

from .motor_flyer import MotorFlyer
from .signal import epics_signal_xval

log = logging.getLogger(__name__)


class SignalX(SignalX):
trigger_value = 1

def __init__(self, *args, trigger_value=1, **kwargs):
self.trigger_value = trigger_value
super().__init__(*args, **kwargs)

def trigger(
self, wait=False, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
) -> AsyncStatus:
"""Trigger the action and return a status saying when it's done"""
if timeout is CALCULATE_TIMEOUT:
timeout = self._timeout
coro = self._backend.put(self.trigger_value, wait=wait, timeout=timeout)
return AsyncStatus(coro)


def epics_signal_x(write_pv: str, name: str = "", trigger_value=1) -> SignalX:
"""Create a `SignalX` backed by 1 EPICS PVs
Parameters
----------
write_pv:
The PV to write its initial value to on trigger
trigger_value:
The value to send to the write PV.
"""
backend: SignalBackend = _epics_signal_backend(None, write_pv, write_pv)
return SignalX(backend, name=name, trigger_value=trigger_value)


class Motor(MotorBase):
"""The default motor for asynchrnous movement."""

Expand Down Expand Up @@ -95,7 +55,7 @@ def __init__(
# Load all the parent signals
super().__init__(prefix=prefix, name=name)
# Override the motor stop signal to use the right trigger value
self.motor_stop = epics_signal_x(f"{prefix}.STOP")
self.motor_stop = epics_signal_xval(f"{prefix}.STOP")
self.set_name(self.name)

async def connect(
Expand Down
57 changes: 34 additions & 23 deletions src/haven/devices/shutter.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging
import warnings
from enum import IntEnum, unique
from typing import Mapping

from ophyd import Component as Cpt
from ophyd import EpicsSignal, EpicsSignalRO
from ophyd.pv_positioner import PVPositionerIsClose
from ophyd.utils.errors import ReadOnlyError
from pcdsdevices.signal import MultiDerivedSignal
from pcdsdevices.type_hints import SignalToValue
from ophyd_async.core import soft_signal_rw
from ophyd_async.epics.signal import epics_signal_r

from ..positioner import Positioner
from .signal import derived_signal_rw, epics_signal_xval

# from apstools.devices.shutters import ApsPssShutterWithStatus as Shutter

Expand All @@ -23,7 +24,8 @@ class ShutterState(IntEnum):
UNKNOWN = 4 # 0b100


class PssShutter(PVPositionerIsClose):
class PssShutter(Positioner):
_ophyd_labels_ = {"shutters"}
_last_setpoint: int = ShutterState.UNKNOWN
allow_open: bool
allow_close: bool
Expand All @@ -39,7 +41,25 @@ def __init__(
):
self.allow_open = allow_open
self.allow_close = allow_close
super().__init__(prefix=prefix, name=name, labels=labels, **kwargs)
# Actuators for opening/closing the shutter
self.open_signal = epics_signal_xval(f"{prefix}OpenEPICSC")
self.close_signal = epics_signal_xval(f"{prefix}CloseEPICSC")
# Just use convenient values for these since there's no real position
self.velocity = soft_signal_rw(float, initial_value=0.5)
self.units = soft_signal_rw(str, initial_value="")
self.precision = soft_signal_rw(int, initial_value=0)
# Positioner signals for moving the shutter
self.readback = epics_signal_r(bool, f"{prefix}BeamBlockingM.VAL")
self.setpoint = derived_signal_rw(
int,
derived_from={
"open_signal": self.open_signal,
"close_signal": self.close_signal,
},
forward=self._actuate_shutter,
inverse=self._shutter_setpoint,
)
super().__init__(name=name, **kwargs)

def check_value(self, pos):
"""Check that the shutter has the right permissions."""
Expand All @@ -52,20 +72,21 @@ def check_value(self, pos):
f"Shutter {self.name} is not permitted to be opened per iconfig.toml. Set `allow_open` for this shutter."
)

def _actuate_shutter(self, mds: MultiDerivedSignal, value: int) -> SignalToValue:
async def _actuate_shutter(self, value: int, open_signal, close_signal) -> Mapping:
"""Open/close the shutter using derived-from signals."""
self.check_value(value)
if value == ShutterState.OPEN:
items = {self.open_signal: 1}
items = {open_signal: 1}
elif value == ShutterState.CLOSED:
items = {self.close_signal: 1}
items = {close_signal: 1}
else:
raise ValueError(f"Invalid shutter state for {self}")
return items

def _shutter_setpoint(self, mds: MultiDerivedSignal, items: SignalToValue) -> int:
def _shutter_setpoint(self, values: Mapping, open_signal, close_signal) -> int:
"""Determine whether the shutter was last opened or closed."""
do_open = items[self.open_signal]
do_close = items[self.close_signal]
do_open = values[open_signal]
do_close = values[close_signal]
if do_open and do_close:
# Shutter is both opening and closing??
warnings.warn("Unknown shutter setpoint")
Expand All @@ -76,16 +97,6 @@ def _shutter_setpoint(self, mds: MultiDerivedSignal, items: SignalToValue) -> in
self._last_setpoint = ShutterState.CLOSED
return self._last_setpoint

readback = Cpt(EpicsSignalRO, "BeamBlockingM.VAL")
setpoint = Cpt(
MultiDerivedSignal,
attrs=["open_signal", "close_signal"],
calculate_on_put=_actuate_shutter,
calculate_on_get=_shutter_setpoint,
)
open_signal = Cpt(EpicsSignal, "OpenEPICSC", kind="omitted")
close_signal = Cpt(EpicsSignal, "CloseEPICSC", kind="omitted")


# -----------------------------------------------------------------------------
# :author: Mark Wolfman
Expand Down
50 changes: 49 additions & 1 deletion src/haven/devices/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@
import numpy as np
from bluesky.protocols import Reading, Subscribable
from ophyd_async.core import (
CALCULATE_TIMEOUT,
DEFAULT_TIMEOUT,
AsyncStatus,
CalculatableTimeout,
ReadingValueCallback,
SignalBackend,
SignalMetadata,
SignalR,
SignalRW,
SignalX,
SoftSignalBackend,
T,
)
from ophyd_async.epics.signal._signal import _epics_signal_backend


class DerivedSignalBackend(SoftSignalBackend):
Expand Down Expand Up @@ -195,8 +200,17 @@ async def get_reading(self) -> Reading:
return self.combine_readings(readings)

async def get_value(self) -> T:
# Sort out which types of signals we have
gettable_signals = [
sig for sig in self._derived_from.values() if hasattr(sig, "get_value")
]
# Retrieve current values from signals
values = {sig: (await sig.get_value()) for sig in self._derived_from.values()}
values = await asyncio.gather(*(sig.get_value() for sig in gettable_signals))
values = {sig: val for sig, val in zip(gettable_signals, values)}
# Set default value of None for missing signals
for sig in self._derived_from.values():
values.setdefault(sig, None)
# Compute the new value
new_value = self.inverse(values, **self._derived_from)
return self.converter.value(new_value)

Expand Down Expand Up @@ -417,3 +431,37 @@ def __init__(self, prefix, name="", **kwargs):
)
signal = SignalX(backend, name=name)
return signal


class SignalXVal(SignalX):
trigger_value = 1

def __init__(self, *args, trigger_value=1, **kwargs):
self.trigger_value = trigger_value
super().__init__(*args, **kwargs)

def trigger(
self, wait=False, timeout: CalculatableTimeout = CALCULATE_TIMEOUT
) -> AsyncStatus:
"""Trigger the action and return a status saying when it's done"""
if timeout is CALCULATE_TIMEOUT:
timeout = self._timeout
coro = self._backend.put(self.trigger_value, wait=wait, timeout=timeout)
return AsyncStatus(coro)


def epics_signal_xval(write_pv: str, name: str = "", trigger_value=1) -> SignalXVal:
"""Create a `SignalX` backed by 1 EPICS PVs. This differs from the
standard ophyd-async trigger in that it accepts a prescribed
*trigger_value* that will be sent to the PV when triggered.
Parameters
----------
write_pv:
The PV to write its initial value to on trigger
trigger_value:
The value to send to the write PV.
"""
backend: SignalBackend = _epics_signal_backend(None, write_pv, write_pv)
return SignalXVal(backend, name=name, trigger_value=trigger_value)
1 change: 0 additions & 1 deletion src/haven/iconfig_testing.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ prefix = "S255ID-PSS:FES:"
allow_close = false
# allow_open = true # Default


[[ pss_shutter ]]
name = "hutch_shutter"
prefix = "S255ID-PSS:SCS:"
Expand Down
2 changes: 1 addition & 1 deletion src/haven/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ async def load(
"energy": EnergyPositioner,
"sim_detector": SimDetector,
"camera": AravisDetector,
"pss_shutter": PssShutter,
# Threaded ophyd devices
"blade_slits": BladeSlits,
"aperture_slits": ApertureSlits,
Expand All @@ -417,7 +418,6 @@ async def load(
"synchrotron": ApsMachine,
"robot": Robot,
"pfcu4": PFCUFilterBank, # <-- fails if mocked
"pss_shutter": PssShutter,
"xspress": make_xspress_device,
"dxp": make_dxp_device,
"beamline_manager": BeamlineManager,
Expand Down
17 changes: 10 additions & 7 deletions src/haven/positioner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import logging
import warnings
from functools import partial

import numpy as np
Expand Down Expand Up @@ -106,12 +107,12 @@ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEO
self.watch_done, done_event=done_event, started_event=started_event
)
)
done_status = AsyncStatus(asyncio.wait_for(done_event.wait(), timeout))
aws = asyncio.gather(done_event.wait(), set_status)
done_status = AsyncStatus(asyncio.wait_for(aws, timeout))
else:
# Monitor based on readback position
done_status = AsyncStatus(
asyncio.wait_for(reached_setpoint.wait(), timeout)
)
aws = asyncio.gather(reached_setpoint.wait(), set_status)
done_status = AsyncStatus(asyncio.wait_for(aws, timeout))
# Monitor the position of the readback value
async for current_position in observe_value(
self.readback, done_status=done_status
Expand All @@ -122,7 +123,7 @@ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEO
target=new_position,
name=self.name,
unit=units,
precision=precision,
precision=int(precision),
)
# Check if the move has finished
target_reached = current_position is not None and np.isclose(
Expand All @@ -139,5 +140,7 @@ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEO

async def stop(self, success=True):
self._set_success = success
status = self.stop_signal.trigger()
await status
if hasattr(self, "stop_signal"):
await self.stop_signal.trigger()
else:
warnings.warn(f"Positioner {self.name} has no stop signal.")
Loading

0 comments on commit e772685

Please sign in to comment.