diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 62a5540..35aa147 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -29,6 +29,6 @@ from .eiger9m_csaxs import Eiger9McSAXS from .pilatus_csaxs import PilatuscSAXS from .falcon_csaxs import FalconcSAXS from .delay_generator_csaxs import DelayGeneratorcSAXS -from .AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX +from .aerotech.AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX # from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py similarity index 97% rename from ophyd_devices/epics/devices/AerotechAutomation1.py rename to ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py index de53e05..7d9a7a4 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py @@ -21,11 +21,10 @@ from typing import Union from collections import OrderedDict -#class EpicsMotorX(EpicsMotor): -# pass - class EpicsMotorX(EpicsMotor): + """ Special motor class that provides flyer interface and progress bar. + """ SUB_PROGRESS = "progress" def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): @@ -64,19 +63,9 @@ class EpicsMotorX(EpicsMotor): value=int(100*progress), max_value=max_value, done=int(np.isclose(max_value, progress, 1e-3)), ) -class EpicsSignalPassive(Device): - value = Component(EpicsSignalRO, "", kind=Kind.omitted) - proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True) - def get(self): - self.proc.set(1).wait() - return self.value.get() - - class EpicsPassiveRO(EpicsSignalRO): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. + """ Small helper class to read PVs that need to be processed first. """ - def __init__(self, read_pv, *, string=False, name=None, **kwargs): super().__init__(read_pv, string=string, name=name, **kwargs) self._proc = EpicsSignal(read_pv+".PROC", kind=Kind.omitted, put_complete=True) @@ -113,14 +102,33 @@ class aa1Tasks(Device): The place to manage tasks and AeroScript user files on the controller. You can read/write/compile/execute AeroScript files and also retrieve - saved data files from the controller. + saved data files from the controller. It will also work around an ophyd + bug that swallows failures. - Execute does not require to store the script in a file, it will compile + Execution does not require to store the script in a file, it will compile it and run it directly on a certain thread. But there's no way to retrieve the source code. + + Write a text into a file on the aerotech controller and execute it with kickoff. + ''' + script="var $axis as axis = ROTY\\nMoveAbsolute($axis, 42, 90)" + tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk.wait_for_connection() + tsk.configure({'text': script, 'filename': "foobar.ascript", 'taskIndex': 4}) + tsk.kickoff().wait() + ''' + + Just execute an ascript file already on the aerotech controller. + ''' + tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk.wait_for_connection() + tsk.configure({'filename': "foobar.ascript", 'taskIndex': 4}) + tsk.kickoff().wait() + ''' + """ SUB_PROGRESS = "progress" - _failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) + _failure = Component(EpicsSignalRO, "FAILURE", auto_monitor=True, kind=Kind.hinted) errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) warnStatus = Component(EpicsSignalRO, "WARNW", auto_monitor=True, kind=Kind.hinted) taskStates = Component(EpicsSignalRO, "STATES-RBV", auto_monitor=True, kind=Kind.hinted) @@ -239,8 +247,11 @@ class aa1Tasks(Device): self._textToExecute = None else: raise RuntimeError("Unsupported filename-text combo") + + if self._failure.value: + raise RuntimeError("Failed to launch task, please check the Aerotech IOC") + self._isConfigured = True - new = self.read_configuration() return (old, new) @@ -455,7 +466,7 @@ class aa1DataAcquisition(Device): return data # DAQ data readback - data_rb = Component(EpicsSignalPassive, "DATA", kind=Kind.hinted) + data_rb = Component(EpicsPassiveRO, "DATA", kind=Kind.hinted) data_rows = Component(EpicsSignalRO, "DATA_ROWS", auto_monitor=True, kind=Kind.hinted) data_cols = Component(EpicsSignalRO, "DATA_COLS", auto_monitor=True, kind=Kind.hinted) data_stat = Component(EpicsSignalRO, "DATA_AVG", auto_monitor=True, kind=Kind.hinted) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1Enums.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py similarity index 100% rename from ophyd_devices/epics/devices/AerotechAutomation1Enums.py rename to ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py diff --git a/ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript b/ophyd_devices/epics/devices/aerotech/AerotechSimpleSequenceTemplate.ascript similarity index 100% rename from ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript rename to ophyd_devices/epics/devices/aerotech/AerotechSimpleSequenceTemplate.ascript diff --git a/ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript b/ophyd_devices/epics/devices/aerotech/AerotechSnapAndStepTemplate.ascript similarity index 100% rename from ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript rename to ophyd_devices/epics/devices/aerotech/AerotechSnapAndStepTemplate.ascript diff --git a/ophyd_devices/epics/devices/aerotech/README.md b/ophyd_devices/epics/devices/aerotech/README.md new file mode 100644 index 0000000..99283e4 --- /dev/null +++ b/ophyd_devices/epics/devices/aerotech/README.md @@ -0,0 +1,167 @@ + + + + + + + + +## Integration log for the Ophyd integration for the Aerotech Automation1 EPICS IOC + + +## Avoid the safespace API!!! + +The properly documented beamline scripting interface was meant for exernal users. Hence, it is idiotproof and only offers a very limited functionality, making it completely unsuitable for serious development. Anyhing more complicated should go through the undocumented scan API under 'bec/scan_server/scan_plugins'. The interface is a bit inconcvenient and there's no documentation, but there are sufficient code examples to get going, but it's quite picky about some small details (that are not documented). Note that the scan plugins will probably migrate to beamline repositories in the future. + +## Differences to vanilla Bluesky + +Unfortunately the BEC is not 100% compatible with Bluesky, thus some changes are also required from the ophyd layer. + +### Event model + +The BEC has it's own event model, that's different from vanilla Bluesky. Particularly every standard scan is framed between **stage --> ... --> complete --> unstage**. So: + + - **Bluesky stepper**: configure --> stage --> Nx(trigger+read) --> unstage + - **Bluesky flyer**: configure --> kickoff --> complete --> collect + - **BEC stepper**: stage --> configure --> Nx(trigger+ read) --> complete --> unstage + - **BEC flyer**: stage --> configure --> kickoff --> complete --> complete --> unstage + +What's more is that unless it's explicitly specified in the scan, **ALL** ophyd devices (even listeners) get staged and unstaged for every scan. This either makes device management mandatory or raises the need to explicitly prevent this in custom scans. + +### Scan server hangs + +Unfortunately a common behavior. + +### Class + +The DeviceServer's instantiates the ophyd devices from a dictionary. Therefore all arguments of '__init__(self, ...)' must be explicity named, you can't use the shorthand '__init__(self, *args, **kwargs)'. + +'''python +# Wrong example +#class aa1Controller(Device): +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self._foo = "bar" + +# Right example +class aa1Controller(Device): + def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + self._foo = "bar" +''' + +### Status objects (futures) + +This is a major time sink. Bluesky usually just calls 'StatusBase.wait()', and it doesn't even check the type, i.e. it works with anything that resembles an ophyd future. However the BEC adds it's own subscription that has some inconveniences... + +#### DeviceStatus must have a device +Since the BEC wants to subscribe to it, it needs a device to subscribe. +'''python +# Wrong example +# def complete(self): +# status = Status() +# return status + +# Right example + def complete(self): + status = Status(self) + return status +''' + +#### The device of the status shouldn't be a dynamically created object +For some reason it can't be a dynamically created ophyd object. +'''python +# Wrong example +# def complete(self): +# self.mon = EpicsSignal("PV-TO-MONITOR") +# status = SubscriptionStatus(self.mon, self._mon_cb, ...) +# return status + +# Right example + def complete(self): + status = SubscriptionStatus(self.mon, self._mon_cb, ...) + return status +''' + + +### Scans + +Important to know that 'ScanBase.num_pos' must be set to 1 at the end of the scan. Otherwise the bec_client will just hang. + + + + +'''python +class AeroScriptedSequence(FlyScanBase): + def __init__(self, *args, parameter: dict = None, **kwargs): + super().__init__(parameter=parameter, **kwargs) + self.num_pos = 0 + + def cleanup(self): + self.num_pos = 1 + return super().cleanup() +''' + +Note that is often set from a device progress that requires additional capability from the Ophyd device to report it's current progress. +''' +class ProggressMotor(EpicsMotor): + SUB_PROGRESS = "progress" + + def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + self.subscribe(self._progress_update, run=False) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + if not self.moving: + self._run_subs( sub_type="progress", value=1, max_value=1, done=1, ) + else: + progress = np.abs( (value-self._startPosition)/(self._targetPosition-self._startPosition) ) + self._run_subs(sub_type="progress", value=progress, max_value=1, done=np.isclose(1, progress, 1e-3) ) +''' + +calculated + +Otherwise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +