From 1f66d629d0ffe2b4bb686c29a3e6c852d15376cc Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Wed, 8 May 2024 13:10:40 +0200 Subject: [PATCH] feat(devices): added device classes from ophyd_devices --- pyproject.toml | 2 +- tests/tests_devices/test_grashopper_tomcat.py | 226 +++ .../device_configs/microxas_test_bed.yaml | 94 ++ tomcat_bec/devices/__init__.py | 10 + .../devices/aerotech/AerotechAutomation1.py | 1490 +++++++++++++++++ .../aerotech/AerotechAutomation1Enums.py | 901 ++++++++++ .../AerotechSimpleSequenceTemplate.ascript | 140 ++ .../AerotechSnapAndStepTemplate.ascript | 87 + tomcat_bec/devices/aerotech/__init__.py | 0 tomcat_bec/devices/grashopper_tomcat.py | 432 +++++ tomcat_bec/devices/tomcat_rotation_motors.py | 182 ++ 11 files changed, 3563 insertions(+), 1 deletion(-) create mode 100644 tests/tests_devices/test_grashopper_tomcat.py create mode 100644 tomcat_bec/device_configs/microxas_test_bed.yaml create mode 100644 tomcat_bec/devices/aerotech/AerotechAutomation1.py create mode 100644 tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py create mode 100644 tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript create mode 100644 tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript create mode 100644 tomcat_bec/devices/aerotech/__init__.py create mode 100644 tomcat_bec/devices/grashopper_tomcat.py create mode 100644 tomcat_bec/devices/tomcat_rotation_motors.py diff --git a/pyproject.toml b/pyproject.toml index 9353190..8bd6709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Scientific/Engineering", ] -dependencies = [] +dependencies = ["ophyd_devices", "bec_lib"] [project.optional-dependencies] dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order", "ophyd_devices", "bec_server"] diff --git a/tests/tests_devices/test_grashopper_tomcat.py b/tests/tests_devices/test_grashopper_tomcat.py new file mode 100644 index 0000000..def052b --- /dev/null +++ b/tests/tests_devices/test_grashopper_tomcat.py @@ -0,0 +1,226 @@ +# pylint: skip-file +from unittest import mock + +import pytest + +from tomcat_bec.devices.grashopper_tomcat import ( + COLORMODE, + AutoMode, + DetectorState, + GrashopperError, + GrashopperTimeoutError, + GrashopperTOMCATSetup, + ImageBinning, + ImageMode, + MemoryPolling, + PixelFormat, + TriggerSource, + VideoMode, +) + + +def patch_dual_pvs(device): + for walk in device.walk_signals(): + if not hasattr(walk.item, "_read_pv"): + continue + if not hasattr(walk.item, "_write_pv"): + continue + if walk.item._read_pv.pvname.endswith("_RBV"): + walk.item._read_pv = walk.item._write_pv + + +@pytest.fixture(scope="function") +def mock_GrashopperSetup(): + mock_Grashopper = mock.MagicMock() + yield GrashopperTOMCATSetup(parent=mock_Grashopper) + + +# Fixture for scaninfo +@pytest.fixture( + params=[ + { + "scan_id": "1234", + "scan_type": "step", + "num_points": 500, + "frames_per_trigger": 1, + "exp_time": 0.1, + "readout_time": 0.1, + }, + { + "scan_id": "1234", + "scan_type": "step", + "num_points": 500, + "frames_per_trigger": 5, + "exp_time": 0.01, + "readout_time": 0, + }, + { + "scan_id": "1234", + "scan_type": "fly", + "num_points": 500, + "frames_per_trigger": 1, + "exp_time": 1, + "readout_time": 0.2, + }, + { + "scan_id": "1234", + "scan_type": "fly", + "num_points": 500, + "frames_per_trigger": 5, + "exp_time": 0.1, + "readout_time": 0.4, + }, + ] +) +def scaninfo(request): + return request.param + + +def test_initialize_detector(mock_GrashopperSetup): + """Test the initialize_detector method.""" + # Call the function you want to test + mock_GrashopperSetup.initialize_detector() + + # Assert the correct methods are called with the expected arguments + mock_GrashopperSetup.parent.cam.acquire.put.assert_called_once_with(0) + mock_GrashopperSetup.parent.cam.acquire_time_auto.put.assert_called_once_with( + AutoMode.CONTINUOUS + ) + mock_GrashopperSetup.parent.cam.gain_auto.put.assert_called_once_with(AutoMode.CONTINUOUS) + mock_GrashopperSetup.parent.cam.image_mode.put.assert_called_once_with(ImageMode.MULTIPLE) + mock_GrashopperSetup.parent.cam.image_binning.put.assert_called_once_with(ImageBinning.X1) + mock_GrashopperSetup.parent.cam.video_mode.put.assert_called_once_with(VideoMode.MODE0) + mock_GrashopperSetup.parent.cam.pixel_format.put.assert_called_once_with(PixelFormat.MONO16) + mock_GrashopperSetup.parent.cam.trigger_source.put.assert_called_once_with( + TriggerSource.SOFTWARE + ) + mock_GrashopperSetup.parent.cam.memory_polling.put.assert_called_once_with( + MemoryPolling.SECONDS1 + ) + mock_GrashopperSetup.parent.cam.set_image_counter.put.assert_called_once_with(0) + + +def test_initialize_detector_backend(mock_GrashopperSetup): + """Test the initialize_detector_backend method.""" + # Call the function you want to test + mock_GrashopperSetup.initialize_detector_backend() + + # Assert the correct methods are called with the expected arguments + mock_GrashopperSetup.parent.image.queue_size.put.assert_called_once_with(5) + mock_GrashopperSetup.parent.image.array_port.put.assert_called_once_with( + mock_GrashopperSetup.parent.cam.port_name.get() + ) + mock_GrashopperSetup.parent.image.enable_cb.put.assert_called_once_with(1) + mock_GrashopperSetup.parent.image.set_array_counter.put.assert_called_once_with(0) + + +@pytest.mark.parametrize("exposure_time", [1, 0.1, 0.01, 0.001, 0.0001]) +def test_set_exposure_time(mock_GrashopperSetup, exposure_time): + """Test the set_exposure_time method.""" + # Call the function you want to test + frame_rate = 1 / exposure_time + if frame_rate > mock_GrashopperSetup.low_frame_rate: + with pytest.raises(GrashopperError): + mock_GrashopperSetup.set_exposure_time(exposure_time) + else: + mock_GrashopperSetup.set_exposure_time(exposure_time) + mock_GrashopperSetup.parent.cam.frame_rate.put.assert_called_once_with(frame_rate) + + +def test_prepare_detector(mock_GrashopperSetup, scaninfo): + """Test the prepare_detector method.""" + # setup scaninof + for k, v in scaninfo.items(): + setattr(mock_GrashopperSetup.parent.scaninfo, k, v) + + # Call the function you want to test + with ( + mock.patch.object( + mock_GrashopperSetup, "set_acquisition_params" + ) as mock_set_acquisition_params, + mock.patch.object(mock_GrashopperSetup, "set_exposure_time") as mock_set_exposure_time, + ): + mock_GrashopperSetup.prepare_detector() + + # Assert the correct methods are called with the expected arguments + mock_GrashopperSetup.parent.cam.image_mode.put.assert_called_once_with(ImageMode.MULTIPLE) + mock_GrashopperSetup.parent.cam.acquire_time_auto.put.assert_called_once_with( + AutoMode.CONTINUOUS + ) + mock_GrashopperSetup.parent.set_trigger.assert_called_once_with(TriggerSource.SOFTWARE) + mock_GrashopperSetup.parent.cam.set_image_counter.put.assert_called_once_with(0) + mock_set_acquisition_params.assert_called_once() + mock_set_exposure_time.assert_called_once_with(scaninfo["exp_time"]) + + +def test_prepare_detector_backend(mock_GrashopperSetup): + """Test the prepare_detector_backend method.""" + # Call the function you want to test + mock_GrashopperSetup.prepare_detector_backend() + mock_GrashopperSetup.parent.image.set_array_counter.put.assert_called_once_with(0) + assert mock_GrashopperSetup.monitor_thread is None + assert not mock_GrashopperSetup.stop_monitor + + +@pytest.mark.parametrize("detector_state", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) +def test_arm_acquisition(mock_GrashopperSetup, detector_state): + """Test the arm_acquisition method.""" + # Call the function you want to test + mock_GrashopperSetup.parent.cam.detector_state.get.return_value = detector_state + mock_GrashopperSetup.parent.timeout = 0.1 + + if detector_state != DetectorState.WAITING: + with pytest.raises(GrashopperTimeoutError): + mock_GrashopperSetup.arm_acquisition() + assert mock_GrashopperSetup.parent.cam.acquire.put.call_count == 1 + assert mock_GrashopperSetup.parent.cam.acquire.put.call_args == mock.call(1) + else: + mock_GrashopperSetup.arm_acquisition() + assert mock_GrashopperSetup.parent.cam.acquire.put.call_count == 1 + assert mock_GrashopperSetup.parent.cam.acquire.put.call_args == mock.call(1) + + +@pytest.mark.parametrize("trigger_source", [0, 1, 2, 3]) +def test_on_trigger(mock_GrashopperSetup, trigger_source): + """Test the on_trigger method.""" + # Call the function you want to test + mock_GrashopperSetup.parent.cam.trigger_source.get.return_value = trigger_source + with mock.patch.object(mock_GrashopperSetup, "send_data") as mock_send_data: + mock_GrashopperSetup.on_trigger() + + # Assert the correct methods are called with the expected arguments + if trigger_source == TriggerSource.SOFTWARE: + mock_GrashopperSetup.parent.cam.software_trigger_device.put.assert_called_once_with(1) + assert mock_send_data.call_count == 1 + + +def test_set_acquisition_params(mock_GrashopperSetup, scaninfo): + """Test the set_acquisition_params method.""" + # Setup scaninfo + mock_GrashopperSetup.parent.scaninfo.num_points = scaninfo["num_points"] + mock_GrashopperSetup.parent.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"] + + # Call the function you want to test + mock_GrashopperSetup.set_acquisition_params() + + # Assert the correct methods are called with the expected arguments + expected_num_images = scaninfo["num_points"] * scaninfo["frames_per_trigger"] + mock_GrashopperSetup.parent.cam.num_images.put.assert_called_once_with(expected_num_images) + + +@pytest.mark.parametrize("detector_state", [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) +def test_stop_detector(mock_GrashopperSetup, detector_state): + """Test the arm_acquisition method.""" + # Call the function you want to test + mock_GrashopperSetup.parent.cam.detector_state.get.return_value = detector_state + mock_GrashopperSetup.parent.timeout = 0.1 + + if detector_state != DetectorState.IDLE: + with pytest.raises(GrashopperTimeoutError): + mock_GrashopperSetup.stop_detector() + assert mock_GrashopperSetup.parent.cam.acquire.put.call_count == 2 + assert mock_GrashopperSetup.parent.cam.acquire.put.call_args == mock.call(0) + else: + mock_GrashopperSetup.stop_detector() + assert mock_GrashopperSetup.parent.cam.acquire.put.call_count == 1 + assert mock_GrashopperSetup.parent.cam.acquire.put.call_args == mock.call(0) diff --git a/tomcat_bec/device_configs/microxas_test_bed.yaml b/tomcat_bec/device_configs/microxas_test_bed.yaml new file mode 100644 index 0000000..197c028 --- /dev/null +++ b/tomcat_bec/device_configs/microxas_test_bed.yaml @@ -0,0 +1,94 @@ +eyex: + readoutPriority: baseline + description: X-ray eye translation x + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: MTEST-X05LA-ES2-XRAYEYE:M1 + deviceTags: + - xray-eye + onFailure: buffer + enabled: true + readOnly: false + softwareTrigger: false +eyey: + readoutPriority: baseline + description: X-ray eye translation y + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: MTEST-X05LA-ES2-XRAYEYE:M2 + deviceTags: + - xray-eye + onFailure: buffer + enabled: true + readOnly: false + softwareTrigger: false +eyez: + readoutPriority: baseline + description: X-ray eye translation z + deviceClass: ophyd.EpicsMotor + deviceConfig: + prefix: MTEST-X05LA-ES2-XRAYEYE:M3 + deviceTags: + - xray-eye + onFailure: buffer + enabled: true + readOnly: false + softwareTrigger: false +femto_mean_curr: + readoutPriority: monitored + description: Femto mean current + deviceClass: ophyd.EpicsSignal + deviceConfig: + auto_monitor: true + read_pv: MTEST-X05LA-ES2-XRAYEYE:FEMTO-MEAN-CURR + deviceTags: + - xray-eye + onFailure: buffer + enabled: true + readOnly: true + softwareTrigger: false +es1_roty: + description: 'Test rotation stage' + deviceClass: ophyd_devices.epics.devices.EpicsMotorX + deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY'} + onFailure: buffer + enabled: true + readoutPriority: monitored + +# es1_tasks: +# description: 'AA1 task management interface' +# deviceClass: tomcat_bec.devices.aa1Tasks +# deviceConfig: {prefix: 'X02DA-ES1-SMP1:TASK:'} +# onFailure: buffer +# enabled: true +# readoutPriority: monitored + +# es1_psod: +# description: 'AA1 PSO output interface' +# deviceClass: tomcat_bec.devices.aa1AxisPsoDistance +# deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'} +# onFailure: buffer +# enabled: true +# readoutPriority: monitored + +# es1_ddaq: +# description: 'AA1 drive data collection interface' +# deviceClass: tomcat_bec.devices.aa1AxisDriveDataCollection +# deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'} +# onFailure: buffer +# enabled: true +# readoutPriority: monitored + +camera: + description: Grashopper Camera + deviceClass: tomcat_bec.devices.GrashopperTOMCAT + deviceConfig: + prefix: 'X02DA-PG-USB:' + deviceTags: + - camera + enabled: true + onFailure: buffer + readOnly: false + readoutPriority: monitored + softwareTrigger: true + diff --git a/tomcat_bec/devices/__init__.py b/tomcat_bec/devices/__init__.py index e69de29..341c4e8 100644 --- a/tomcat_bec/devices/__init__.py +++ b/tomcat_bec/devices/__init__.py @@ -0,0 +1,10 @@ +from .aerotech.AerotechAutomation1 import ( + EpicsMotorX, + aa1AxisDriveDataCollection, + aa1AxisPsoDistance, + aa1Controller, + aa1GlobalVariableBindings, + aa1GlobalVariables, + aa1Tasks, +) +from .grashopper_tomcat import GrashopperTOMCAT diff --git a/tomcat_bec/devices/aerotech/AerotechAutomation1.py b/tomcat_bec/devices/aerotech/AerotechAutomation1.py new file mode 100644 index 0000000..a522259 --- /dev/null +++ b/tomcat_bec/devices/aerotech/AerotechAutomation1.py @@ -0,0 +1,1490 @@ +import time +from collections import OrderedDict +from time import sleep + +import numpy as np +from ophyd import Component, Device, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind +from ophyd.status import DeviceStatus, Status, StatusBase, SubscriptionStatus + +from .AerotechAutomation1Enums import ( + DataCollectionFrequency, + DataCollectionMode, + DriveDataCaptureInput, + DriveDataCaptureTrigger, +) + + +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, + ): + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self._startPosition = None + self._targetPosition = None + self.subscribe(self._progress_update, run=False) + + def configure(self, d: dict): + if "target" in d: + self._targetPosition = d["target"] + del d["target"] + if "position" in d: + self._targetPosition = d["position"] + del d["position"] + return super().configure(d) + + def kickoff(self): + self._startPosition = float(self.position) + return self.move(self._targetPosition, wait=False) + + def move(self, position, wait=True, **kwargs): + self._startPosition = float(self.position) + return super().move(position, wait, **kwargs) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + if (self._startPosition is None) or (self._targetPosition is None) or (not self.moving): + self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) + return + + progress = np.abs( + (value - self._startPosition) / (self._targetPosition - self._startPosition) + ) + max_value = 100 + self._run_subs( + sub_type=self.SUB_PROGRESS, + value=int(100 * progress), + max_value=max_value, + done=int(np.isclose(max_value, progress, 1e-3)), + ) + + +class EpicsPassiveRO(EpicsSignalRO): + """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) + + def wait_for_connection(self, *args, **kwargs): + super().wait_for_connection(*args, **kwargs) + self._proc.wait_for_connection(*args, **kwargs) + + def get(self, *args, **kwargs): + self._proc.set(1).wait() + return super().get(*args, **kwargs) + + @property + def value(self): + return super().value + + +class aa1Controller(Device): + """Ophyd proxy class for the Aerotech Automation 1's core controller functionality""" + + controllername = Component(EpicsSignalRO, "NAME", kind=Kind.config) + serialnumber = Component(EpicsSignalRO, "SN", kind=Kind.config) + apiversion = Component(EpicsSignalRO, "API_VERSION", kind=Kind.config) + axiscount = Component(EpicsSignalRO, "AXISCOUNT", kind=Kind.config) + taskcount = Component(EpicsSignalRO, "TASKCOUNT", kind=Kind.config) + fastpoll = Component(EpicsSignalRO, "POLLTIME", auto_monitor=True, kind=Kind.hinted) + slowpoll = Component(EpicsSignalRO, "DRVPOLLTIME", auto_monitor=True, kind=Kind.hinted) + + 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, + ) + + +class aa1Tasks(Device): + """Task management API + + 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. It will also work around an ophyd + bug that swallows failures. + + 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", 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) + taskIndex = Component(EpicsSignal, "TASKIDX", kind=Kind.config, put_complete=True) + switch = Component(EpicsSignal, "SWITCH", kind=Kind.config, put_complete=True) + _execute = Component(EpicsSignal, "EXECUTE", string=True, kind=Kind.config, put_complete=True) + _executeMode = Component(EpicsSignal, "EXECUTE-MODE", kind=Kind.config, put_complete=True) + _executeReply = Component(EpicsSignalRO, "EXECUTE_RBV", string=True, auto_monitor=True) + + fileName = Component(EpicsSignal, "FILENAME", string=True, kind=Kind.omitted, put_complete=True) + _fileRead = Component(EpicsPassiveRO, "FILEREAD", string=True, kind=Kind.omitted) + _fileWrite = Component( + EpicsSignal, "FILEWRITE", string=True, kind=Kind.omitted, put_complete=True + ) + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + """__init__ MUST have a full argument list""" + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self._currentTask = None + self._textToExecute = None + self._isConfigured = False + self._isStepConfig = False + self.subscribe(self._progress_update, "progress", run=False) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + value = self.progress() + self._run_subs(sub_type=self.SUB_PROGRESS, value=value, max_value=1, done=1) + + def _progress(self) -> None: + """Progress update on the scan""" + if self._currentTaskMonitor is None: + return 1 + else: + if self._currentTaskMonitor.status.value in ["Running", 4]: + return 0 + else: + return 1 + + def readFile(self, filename: str) -> str: + """Read a file from the controller""" + # Have to use CHAR array due to EPICS LSI bug... + self.fileName.set(filename).wait() + filebytes = self._fileRead.get() + # C-strings terminate with trailing zero + if filebytes[-1] == 0: + filebytes = filebytes[:-1] + filetext = filebytes + return filetext + + def writeFile(self, filename: str, filetext: str) -> None: + """Write a file to the controller""" + self.fileName.set(filename).wait() + self._fileWrite.set(filetext).wait() + + def runScript(self, filename: str, taskIndex: int == 2, filetext=None, settle_time=0.5) -> None: + """Run a script file that either exists, or is newly created and compiled""" + + self.configure({"text": filetext, "filename": filename, "taskIndex": taskIndex}) + print("Runscript configured") + self.trigger().wait() + print("Runscript waited") + + def execute(self, text: str, taskIndex: int = 3, mode: str = 0, settle_time=0.5): + """Run a short text command on the Automation1 controller""" + + print(f"Executing program on task: {taskIndex}") + self.configure({"text": text, "taskIndex": taskIndex, "mode": mode}) + self.kickoff().wait() + + if mode in [0, "None", None]: + return None + else: + raw = self._executeReply.get() + return raw + + def configure(self, d: dict = {}) -> tuple: + """Configuration interface for flying""" + # Unrolling the configuration dict + text = str(d["text"]) if "text" in d else None + filename = str(d["filename"]) if "filename" in d else None + taskIndex = int(d["taskIndex"]) if "taskIndex" in d else 4 + settle_time = float(d["settle_time"]) if "settle_time" in d else None + mode = d["mode"] if "mode" in d else None + self._isStepConfig = d["stepper"] if "stepper" in d else False + + # Validation + if taskIndex < 1 or taskIndex > 31: + raise RuntimeError(f"Invalid task index: {taskIndex}") + if (text is None) and (filename is None): + raise RuntimeError("Task execution requires either AeroScript text or filename") + + # Common operations + old = self.read_configuration() + self.taskIndex.set(taskIndex).wait() + self._textToExecute = None + self._currentTask = taskIndex + + # Choose the right execution mode + if (filename is None) and (text not in [None, ""]): + # Direct command execution + print("Preparing for direct command execution") + if mode is not None: + self._executeMode.set(mode).wait() + self._textToExecute = text + elif (filename is not None) and (text in [None, ""]): + # Execute existing file + self.fileName.set(filename).wait() + self.switch.set("Load").wait() + elif (filename is not None) and (text not in [None, ""]): + print("Preparing to execute via intermediary file") + # Execute text via intermediate file + self.taskIndex.set(taskIndex).wait() + self.fileName.set(filename).wait() + self._fileWrite.set(text).wait() + self.switch.set("Load").wait() + 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) + + ########################################################################## + # Bluesky stepper interface + def stage(self) -> None: + """Default staging""" + super().stage() + + def unstage(self) -> None: + """Default unstaging""" + super().unstage() + + def trigger(self, settle_time=0.2) -> Status: + """Execute the script on the configured task""" + if self._isStepConfig: + return self.kickoff(settle_time) + else: + status = DeviceStatus(self, settle_time=settle_time) + status.set_finished() + if settle_time is not None: + sleep(settle_time) + return status + + def stop(self): + """Stop the currently selected task""" + self.switch.set("Stop").wait() + + ########################################################################## + # Flyer interface + def kickoff(self, settle_time=0.2) -> DeviceStatus: + """Execute the script on the configured task""" + if self._isConfigured: + if self._textToExecute is not None: + print(f"Kickoff directly executing string: {self._textToExecute}") + status = self._execute.set(self._textToExecute, settle_time=0.5) + else: + status = self.switch.set("Run", settle_time=0.1) + else: + status = DeviceStatus(self) + status.set_finished() + if settle_time is not None: + sleep(settle_time) + return status + + def complete(self) -> DeviceStatus: + """Execute the script on the configured task""" + print("Called aa1Task.complete()") + timestamp_ = 0 + taskIdx = int(self.taskIndex.get()) + + def notRunning2(*args, old_value, value, timestamp, **kwargs): + nonlocal timestamp_ + result = False if value[taskIdx] in ["Running", 4] else True + timestamp_ = timestamp + print(result) + return result + + # Subscribe and wait for update + status = SubscriptionStatus(self.taskStates, notRunning2, settle_time=0.5) + return status + + def describe_collect(self) -> OrderedDict: + dd = OrderedDict() + dd["success"] = { + "source": "internal", + "dtype": "integer", + "shape": [], + "units": "", + "lower_ctrl_limit": 0, + "upper_ctrl_limit": 0, + } + return {self.name: dd} + + def collect(self) -> OrderedDict: + ret = OrderedDict() + ret["timestamps"] = {"success": time.time()} + ret["data"] = {"success": 1} + yield ret + + +class aa1TaskState(Device): + """Task state monitoring API + + This is the task state monitoring interface for Automation1 tasks. It + does not launch execution, but can wait for the execution to complete. + """ + + index = Component(EpicsSignalRO, "INDEX", kind=Kind.config) + status = Component(EpicsSignalRO, "STATUS", auto_monitor=True, kind=Kind.hinted) + errorCode = Component(EpicsSignalRO, "ERROR", auto_monitor=True, kind=Kind.hinted) + warnCode = Component(EpicsSignalRO, "WARNING", auto_monitor=True, kind=Kind.hinted) + + def complete(self) -> StatusBase: + """Bluesky flyer interface""" + print("Called aa1TaskState.complete()") + # Define wait until the busy flag goes down (excluding initial update) + timestamp_ = 0 + + def notRunning(*args, old_value, value, timestamp, **kwargs): + nonlocal timestamp_ + result = False if (timestamp_ == 0) else (value not in ["Running", 4]) + timestamp_ = timestamp + print(result) + return result + + # Subscribe and wait for update + status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) + return status + + def kickoff(self) -> DeviceStatus: + status = DeviceStatus(self) + status.set_finished() + return status + + def describe_collect(self) -> OrderedDict: + dd = OrderedDict() + dd["success"] = { + "source": "internal", + "dtype": "integer", + "shape": [], + "units": "", + "lower_ctrl_limit": 0, + "upper_ctrl_limit": 0, + } + return dd + + def collect(self) -> OrderedDict: + ret = OrderedDict() + ret["timestamps"] = {"success": time.time()} + ret["data"] = {"success": 1} + yield ret + + +class aa1DataAcquisition(Device): + """Controller Data Acquisition - DONT USE at Tomcat + + This class implements the controller data collection feature of the + Automation1 controller. This feature logs various inputs at a + **fixed frequency** from 1 kHz up to 200 kHz. + Usage: + 1. Start a new configuration with "startConfig" + 2. Add your signals with "addXxxSignal" + 3. Start your data collection + 4. Read back the recorded data with "readback" + """ + + # Status monitoring + status = Component(EpicsSignalRO, "RUNNING", auto_monitor=True, kind=Kind.hinted) + points_max = Component(EpicsSignal, "MAXPOINTS", kind=Kind.config, put_complete=True) + signal_num = Component(EpicsSignalRO, "NITEMS", kind=Kind.config) + + points_total = Component(EpicsSignalRO, "NTOTAL", auto_monitor=True, kind=Kind.hinted) + points_collected = Component(EpicsSignalRO, "NCOLLECTED", auto_monitor=True, kind=Kind.hinted) + points_retrieved = Component(EpicsSignalRO, "NRETRIEVED", auto_monitor=True, kind=Kind.hinted) + overflow = Component(EpicsSignalRO, "OVERFLOW", auto_monitor=True, kind=Kind.hinted) + runmode = Component(EpicsSignalRO, "MODE_RBV", auto_monitor=True, kind=Kind.hinted) + # DAQ setup + numpoints = Component(EpicsSignal, "NPOINTS", kind=Kind.config, put_complete=True) + frequency = Component(EpicsSignal, "FREQUENCY", kind=Kind.config, put_complete=True) + _configure = Component(EpicsSignal, "CONFIGURE", kind=Kind.omitted, put_complete=True) + + def startConfig(self, npoints: int, frequency: DataCollectionFrequency): + self.numpoints.set(npoints).wait() + self.frequency.set(frequency).wait() + self._configure.set("START").wait() + + def clearConfig(self): + self._configure.set("CLEAR").wait() + + srcTask = Component(EpicsSignal, "SRC_TASK", kind=Kind.config, put_complete=True) + srcAxis = Component(EpicsSignal, "SRC_AXIS", kind=Kind.config, put_complete=True) + srcCode = Component(EpicsSignal, "SRC_CODE", kind=Kind.config, put_complete=True) + _srcAdd = Component(EpicsSignal, "SRC_ADD", kind=Kind.omitted, put_complete=True) + + def addAxisSignal(self, axis: int, code: int) -> None: + """Add a new axis-specific data signal to the DAQ configuration. The + most common signals are PositionFeedback and PositionError. + """ + self.srcAxis.set(axis).wait() + self.srcCode.set(code).wait() + self._srcAdd.set("AXIS").wait() + + def addTaskSignal(self, task: int, code: int) -> None: + """Add a new task-specific data signal to the DAQ configuration""" + self.srcTask.set(task).wait() + self.srcCode.set(code).wait() + self._srcAdd.set("TASK").wait() + + def addSystemSignal(self, code: int) -> None: + """Add a new system data signal to the DAQ configuration. The most + common signal is SampleCollectionTime.""" + self.srcCode.set(code).wait() + self._srcAdd.set("SYSTEM").wait() + + # Starting / stopping the DAQ + _mode = Component(EpicsSignal, "MODE", kind=Kind.config, put_complete=True) + _switch = Component(EpicsSignal, "SET", kind=Kind.omitted, put_complete=True) + + def start(self, mode=DataCollectionMode.Snapshot) -> None: + """Start a new data collection""" + self._mode.set(mode).wait() + self._switch.set("START").wait() + + def stop(self) -> None: + """Stop a running data collection""" + self._switch.set("STOP").wait() + + def run(self, mode=DataCollectionMode.Snapshot) -> None: + """Start a new data collection""" + self._mode.set(mode).wait() + self._switch.set("START").wait() + # Wait for finishing acquisition + # Note: this is very bad blocking sleep + while self.status.value != 0: + sleep(0.1) + sleep(0.1) + + # Data readback + data = self.data_rb.get() + rows = self.data_rows.get() + cols = self.data_cols.get() + if len(data) == 0 or rows == 0 or cols == 0: + sleep(0.5) + data = self.data_rb.get() + rows = self.data_rows.get() + cols = self.data_cols.get() + print(f"Data shape: {rows} x {cols}") + data = data.reshape([int(rows), -1]) + return data + + # DAQ data readback + 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) + + def dataReadBack(self) -> np.ndarray: + """Retrieves collected data from the controller""" + data = self.data_rb.get() + rows = self.data_rows.get() + cols = self.data_cols.get() + if len(data) == 0 or rows == 0 or cols == 0: + sleep(0.2) + data = self.data_rb.get() + rows = self.data_rows.get() + cols = self.data_cols.get() + print(f"Data shape: {rows} x {cols}") + data = data.reshape([int(rows), -1]) + return data + + +class aa1GlobalVariables(Device): + """Global variables + + This class provides an interface to directly read/write global variables + on the Automation1 controller. These variables are accesible from script + files and are thus a convenient way to interface with the outside word. + + Read operations take as input the memory address and the size + Write operations work with the memory address and value + + Usage: + var = aa1Tasks(AA1_IOC_NAME+":VAR:", name="var") + var.wait_for_connection() + ret = var.readInt(42) + var.writeFloat(1000, np.random.random(1024)) + ret_arr = var.readFloat(1000, 1024) + + """ + + # Status monitoring + num_real = Component(EpicsSignalRO, "NUM-REAL_RBV", kind=Kind.config) + num_int = Component(EpicsSignalRO, "NUM-INT_RBV", kind=Kind.config) + num_string = Component(EpicsSignalRO, "NUM-STRING_RBV", kind=Kind.config) + + integer_addr = Component(EpicsSignal, "INT-ADDR", kind=Kind.omitted, put_complete=True) + integer_size = Component(EpicsSignal, "INT-SIZE", kind=Kind.omitted, put_complete=True) + integer = Component(EpicsSignal, "INT", kind=Kind.omitted, put_complete=True) + integer_rb = Component(EpicsPassiveRO, "INT-RBV", kind=Kind.omitted) + integerarr = Component(EpicsSignal, "INTARR", kind=Kind.omitted, put_complete=True) + integerarr_rb = Component(EpicsPassiveRO, "INTARR-RBV", kind=Kind.omitted) + + real_addr = Component(EpicsSignal, "REAL-ADDR", kind=Kind.omitted, put_complete=True) + real_size = Component(EpicsSignal, "REAL-SIZE", kind=Kind.omitted, put_complete=True) + real = Component(EpicsSignal, "REAL", kind=Kind.omitted, put_complete=True) + real_rb = Component(EpicsPassiveRO, "REAL-RBV", kind=Kind.omitted) + realarr = Component(EpicsSignal, "REALARR", kind=Kind.omitted, put_complete=True) + realarr_rb = Component(EpicsPassiveRO, "REALARR-RBV", kind=Kind.omitted) + + string_addr = Component(EpicsSignal, "STRING-ADDR", kind=Kind.omitted, put_complete=True) + string = Component(EpicsSignal, "STRING", string=True, kind=Kind.omitted, put_complete=True) + string_rb = Component(EpicsPassiveRO, "STRING-RBV", string=True, kind=Kind.omitted) + + def readInt(self, address: int, size: int = None) -> int: + """Read a 64-bit integer global variable""" + if address > self.num_int.get(): + raise RuntimeError("Integer address {address} is out of range") + + if size is None: + self.integer_addr.set(address).wait() + return self.integer_rb.get() + else: + self.integer_addr.set(address).wait() + self.integer_size.set(size).wait() + return self.integerarr_rb.get() + + def writeInt(self, address: int, value) -> None: + """Write a 64-bit integer global variable""" + if address > self.num_int.get(): + raise RuntimeError("Integer address {address} is out of range") + + if isinstance(value, (int, float)): + self.integer_addr.set(address).wait() + self.integer.set(value).wait() + elif isinstance(value, np.ndarray): + self.integer_addr.set(address).wait() + self.integerarr.set(value).wait() + elif isinstance(value, (list, tuple)): + value = np.array(value, dtype=np.int32) + self.integer_addr.set(address).wait() + self.integerarr.set(value).wait() + else: + raise RuntimeError("Unsupported integer value type: {type(value)}") + + def readFloat(self, address: int, size: int = None) -> float: + """Read a 64-bit double global variable""" + if address > self.num_real.get(): + raise RuntimeError("Floating point address {address} is out of range") + + if size is None: + self.real_addr.set(address).wait() + return self.real_rb.get() + else: + self.real_addr.set(address).wait() + self.real_size.set(size).wait() + return self.realarr_rb.get() + + def writeFloat(self, address: int, value) -> None: + """Write a 64-bit float global variable""" + if address > self.num_real.get(): + raise RuntimeError("Float address {address} is out of range") + + if isinstance(value, (int, float)): + self.real_addr.set(address).wait() + self.real.set(value).wait() + elif isinstance(value, np.ndarray): + self.real_addr.set(address).wait() + self.realarr.set(value).wait() + elif isinstance(value, (list, tuple)): + value = np.array(value) + self.real_addr.set(address).wait() + self.realarr.set(value).wait() + else: + raise RuntimeError("Unsupported float value type: {type(value)}") + + def readString(self, address: int) -> str: + """Read a 40 letter string global variable + ToDo: Automation 1 strings are 256 bytes + """ + if address > self.num_string.get(): + raise RuntimeError("String address {address} is out of range") + + self.string_addr.set(address).wait() + return self.string_rb.get() + + def writeString(self, address: int, value) -> None: + """Write a 40 bytes string global variable""" + if address > self.num_string.get(): + raise RuntimeError("Integer address {address} is out of range") + + if isinstance(value, str): + self.string_addr.set(address).wait() + self.string.set(value).wait() + else: + raise RuntimeError("Unsupported string value type: {type(value)}") + + +class aa1GlobalVariableBindings(Device): + """Polled global variables + + This class provides an interface to read/write the first few global variables + on the Automation1 controller. These variables are continuously polled + and are thus a convenient way to interface scripts with the outside word. + """ + + int0 = Component(EpicsSignalRO, "INT0_RBV", auto_monitor=True, name="int0", kind=Kind.hinted) + int1 = Component(EpicsSignalRO, "INT1_RBV", auto_monitor=True, name="int1", kind=Kind.hinted) + int2 = Component(EpicsSignalRO, "INT2_RBV", auto_monitor=True, name="int2", kind=Kind.hinted) + int3 = Component(EpicsSignalRO, "INT3_RBV", auto_monitor=True, name="int3", kind=Kind.hinted) + int8 = Component( + EpicsSignal, + "INT8_RBV", + put_complete=True, + write_pv="INT8", + auto_monitor=True, + name="int8", + kind=Kind.hinted, + ) + int9 = Component( + EpicsSignal, + "INT9_RBV", + put_complete=True, + write_pv="INT9", + auto_monitor=True, + name="int9", + kind=Kind.hinted, + ) + int10 = Component( + EpicsSignal, + "INT10_RBV", + put_complete=True, + write_pv="INT10", + auto_monitor=True, + name="int10", + kind=Kind.hinted, + ) + int11 = Component( + EpicsSignal, + "INT11_RBV", + put_complete=True, + write_pv="INT11", + auto_monitor=True, + name="int11", + kind=Kind.hinted, + ) + + float0 = Component( + EpicsSignalRO, "REAL0_RBV", auto_monitor=True, name="float0", kind=Kind.hinted + ) + float1 = Component( + EpicsSignalRO, "REAL1_RBV", auto_monitor=True, name="float1", kind=Kind.hinted + ) + float2 = Component( + EpicsSignalRO, "REAL2_RBV", auto_monitor=True, name="float2", kind=Kind.hinted + ) + float3 = Component( + EpicsSignalRO, "REAL3_RBV", auto_monitor=True, name="float3", kind=Kind.hinted + ) + float16 = Component( + EpicsSignal, + "REAL16_RBV", + write_pv="REAL16", + put_complete=True, + auto_monitor=True, + name="float16", + kind=Kind.hinted, + ) + float17 = Component( + EpicsSignal, + "REAL17_RBV", + write_pv="REAL17", + put_complete=True, + auto_monitor=True, + name="float17", + kind=Kind.hinted, + ) + float18 = Component( + EpicsSignal, + "REAL18_RBV", + write_pv="REAL18", + put_complete=True, + auto_monitor=True, + name="float18", + kind=Kind.hinted, + ) + float19 = Component( + EpicsSignal, + "REAL19_RBV", + write_pv="REAL19", + put_complete=True, + auto_monitor=True, + name="float19", + kind=Kind.hinted, + ) + + # BEC LiveTable crashes on non-numeric values + str0 = Component( + EpicsSignalRO, "STR0_RBV", auto_monitor=True, string=True, name="str0", kind=Kind.config + ) + str1 = Component( + EpicsSignalRO, "STR1_RBV", auto_monitor=True, string=True, name="str1", kind=Kind.config + ) + str4 = Component( + EpicsSignal, + "STR4_RBV", + put_complete=True, + string=True, + auto_monitor=True, + write_pv="STR4", + name="str4", + kind=Kind.config, + ) + str5 = Component( + EpicsSignal, + "STR5_RBV", + put_complete=True, + string=True, + auto_monitor=True, + write_pv="STR5", + name="str5", + kind=Kind.config, + ) + + +class aa1AxisIo(Device): + """Analog / digital Input-Output + + This class provides convenience wrappers around the Aerotech API's axis + specific IO functionality. Note that this is a low-speed API, actual work + should be done in AeroScript. Only one pin can be writen directly but + several can be polled! + """ + + polllvl = Component(EpicsSignal, "POLLLVL", put_complete=True, kind=Kind.config) + ai0 = Component(EpicsSignalRO, "AI0-RBV", auto_monitor=True, kind=Kind.hinted) + ai1 = Component(EpicsSignalRO, "AI1-RBV", auto_monitor=True, kind=Kind.hinted) + ai2 = Component(EpicsSignalRO, "AI2-RBV", auto_monitor=True, kind=Kind.hinted) + ai3 = Component(EpicsSignalRO, "AI3-RBV", auto_monitor=True, kind=Kind.hinted) + ao0 = Component(EpicsSignalRO, "AO0-RBV", auto_monitor=True, kind=Kind.hinted) + ao1 = Component(EpicsSignalRO, "AO1-RBV", auto_monitor=True, kind=Kind.hinted) + ao2 = Component(EpicsSignalRO, "AO2-RBV", auto_monitor=True, kind=Kind.hinted) + ao3 = Component(EpicsSignalRO, "AO3-RBV", auto_monitor=True, kind=Kind.hinted) + di0 = Component(EpicsSignalRO, "DI0-RBV", auto_monitor=True, kind=Kind.hinted) + do0 = Component(EpicsSignalRO, "DO0-RBV", auto_monitor=True, kind=Kind.hinted) + + ai_addr = Component(EpicsSignal, "AI-ADDR", put_complete=True, kind=Kind.config) + ai = Component(EpicsSignalRO, "AI-RBV", auto_monitor=True, kind=Kind.hinted) + + ao_addr = Component(EpicsSignal, "AO-ADDR", put_complete=True, kind=Kind.config) + ao = Component(EpicsSignal, "AO-RBV", write_pv="AO", auto_monitor=True, kind=Kind.hinted) + + di_addr = Component(EpicsSignal, "DI-ADDR", put_complete=True, kind=Kind.config) + di = Component(EpicsSignalRO, "DI-RBV", auto_monitor=True, kind=Kind.hinted) + + do_addr = Component(EpicsSignal, "DO-ADDR", put_complete=True, kind=Kind.config) + do = Component(EpicsSignal, "DO-RBV", write_pv="DO", auto_monitor=True, kind=Kind.hinted) + + def setAnalog(self, pin: int, value: float, settle_time=0.05): + # Set the address + self.ao_addr.set(pin).wait() + # Set the voltage + self.ao.set(value, settle_time=settle_time).wait() + + def setDigital(self, pin: int, value: int, settle_time=0.05): + # Set the address + self.do_addr.set(pin).wait() + # Set the voltage + self.do.set(value, settle_time=settle_time).wait() + + +class aa1AxisPsoBase(Device): + """Position Sensitive Output - Base class + + This class provides convenience wrappers around the Aerotech IOC's PSO + functionality. As a base class, it's just a collection of PVs without + significant logic (that should be implemented in the child classes). + It uses event-waveform concept to produce signals on the configured + output pin: a specified position based event will trigger the generation + of a waveform on the oputput that can be either used as exposure enable, + as individual trigger or as a series of triggers per each event. + As a first approach, the module follows a simple pipeline structure: + Genrator --> Event --> Waveform --> Output + + Specific operation modes should be implemented in child classes. + """ + + # ######################################################################## + # General module status + status = Component(EpicsSignalRO, "STATUS", auto_monitor=True, kind=Kind.hinted) + output = Component(EpicsSignalRO, "OUTPUT-RBV", auto_monitor=True, kind=Kind.hinted) + _eventSingle = Component(EpicsSignal, "EVENT:SINGLE", put_complete=True, kind=Kind.omitted) + _reset = Component(EpicsSignal, "RESET", put_complete=True, kind=Kind.omitted) + posInput = Component(EpicsSignal, "DIST:INPUT", put_complete=True, kind=Kind.omitted) + + # ######################################################################## + # PSO Distance event module + dstEventsEna = Component(EpicsSignal, "DIST:EVENTS", put_complete=True, kind=Kind.omitted) + dstCounterEna = Component(EpicsSignal, "DIST:COUNTER", put_complete=True, kind=Kind.omitted) + dstCounterVal = Component(EpicsSignalRO, "DIST:CTR0_RBV", auto_monitor=True, kind=Kind.hinted) + dstArrayIdx = Component(EpicsSignalRO, "DIST:IDX_RBV", auto_monitor=True, kind=Kind.hinted) + dstArrayDepleted = Component( + EpicsSignalRO, "DIST:ARRAY-DEPLETED-RBV", auto_monitor=True, kind=Kind.hinted + ) + + dstDirection = Component(EpicsSignal, "DIST:EVENTDIR", put_complete=True, kind=Kind.omitted) + dstDistance = Component(EpicsSignal, "DIST:DISTANCE", put_complete=True, kind=Kind.hinted) + dstDistanceArr = Component(EpicsSignal, "DIST:DISTANCES", put_complete=True, kind=Kind.omitted) + dstArrayRearm = Component(EpicsSignal, "DIST:REARM-ARRAY", put_complete=True, kind=Kind.omitted) + + # ######################################################################## + # PSO Window event module + winEvents = Component(EpicsSignal, "WINDOW:EVENTS", put_complete=True, kind=Kind.omitted) + winOutput = Component(EpicsSignal, "WINDOW0:OUTPUT", put_complete=True, kind=Kind.omitted) + winInput = Component(EpicsSignal, "WINDOW0:INPUT", put_complete=True, kind=Kind.omitted) + winCounter = Component(EpicsSignal, "WINDOW0:COUNTER", put_complete=True, kind=Kind.omitted) + _winLower = Component(EpicsSignal, "WINDOW0:LOWER", put_complete=True, kind=Kind.omitted) + _winUpper = Component(EpicsSignal, "WINDOW0:UPPER", put_complete=True, kind=Kind.omitted) + + # ######################################################################## + # PSO waveform module + waveEnable = Component(EpicsSignal, "WAVE:ENABLE", put_complete=True, kind=Kind.omitted) + waveMode = Component(EpicsSignal, "WAVE:MODE", put_complete=True, kind=Kind.omitted) + # waveDelay = Component(EpicsSignal, "WAVE:DELAY", put_complete=True, kind=Kind.omitted) + + # PSO waveform pulse output + # pulseTrunc = Component(EpicsSignal, "WAVE:PULSE:TRUNC", put_complete=True, kind=Kind.omitted) + pulseOnTime = Component(EpicsSignal, "WAVE:PULSE:ONTIME", put_complete=True, kind=Kind.omitted) + pulseWindow = Component(EpicsSignal, "WAVE:PULSE:PERIOD", put_complete=True, kind=Kind.omitted) + pulseCount = Component(EpicsSignal, "WAVE:PULSE:COUNT", put_complete=True, kind=Kind.omitted) + pulseApply = Component(EpicsSignal, "WAVE:PULSE:APPLY", put_complete=True, kind=Kind.omitted) + + # ######################################################################## + # PSO output module + outPin = Component(EpicsSignal, "PIN", put_complete=True, kind=Kind.omitted) + outSource = Component(EpicsSignal, "SOURCE", put_complete=True, kind=Kind.omitted) + + def fire(self, settle_time=None): + """Fire a single PSO event (i.e. manual software trigger)""" + self._eventSingle.set(1, settle_time=settle_time).wait() + + def toggle(self): + orig_waveMode = self.waveMode.get() + self.waveMode.set("Toggle").wait() + self.fire(0.1) + self.waveMode.set(orig_waveMode).wait() + + +class aa1AxisPsoDistance(aa1AxisPsoBase): + """Position Sensitive Output - Distance mode + + This class provides convenience wrappers around the Aerotech API's PSO + functionality in distance mode. It uses event-waveform concept to produce + signals on the configured output pin: a specified position based event + will trigger the generation af a waveform on the oputput that can be either + used as exposure enable, as individual trigger or as a series of triggers + per each event. + As a first approach, the module follows a simple pipeline structure: + Genrator (distance) --> Event --> Waveform --> Output + + The module provides configuration interface to common functionality, such + as fixed distance or array based triggering and can serve as a base for + future advanced functionality. The relative distances ease the limitations + coming from 32 bit PSO positions. + For a more detailed description of additional signals and masking plase + refer to Automation1's online manual. + + Usage: + # Configure the PSO to raise an 'enable' signal for 180 degrees + pso = aa1AxisPsoDistance(AA1_IOC_NAME+":ROTY:PSO:", name="pso") + pso.wait_for_connection() + pso.configure(d={'distance': [180, 0.1], 'wmode': "toggle"}) + pso.kickoff().wait() + + # Configure the PSO to emmit 5 triggers every 1.8 degrees + pso = aa1AxisPsoDistance(AA1_IOC_NAME+":ROTY:PSO:", name="pso") + pso.wait_for_connection() + pso.configure(d={'distance': 1.8, 'wmode': "pulsed", 'n_pulse': 5}) + pso.kickoff().wait() + """ + + SUB_PROGRESS = "progress" + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + """__init__ MUST have a full argument list""" + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self._Vdistance = 3.141592 + self.subscribe(self._progress_update, "progress", run=False) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + if self.dstArrayDepleted.value: + print("PSO array depleted") + self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) + return + + progress = 1 + max_value = 1 + print(f"PSO array proggress: {progress}") + self._run_subs( + sub_type=self.SUB_PROGRESS, + value=int(progress), + max_value=max_value, + done=int(np.isclose(max_value, progress, 1e-3)), + ) + + # ######################################################################## + # PSO high level interface + def configure(self, d: dict = {}) -> tuple: + """Simplified configuration interface to access the most common + functionality for distance mode PSO. + + :param distance: The trigger distance or the array of distances between subsequent points. + :param wmode: Waveform mode configuration, usually pulsed/toggled. + """ + distance = d["distance"] + wmode = str(d["wmode"]) + t_pulse = float(d["t_pulse"]) if "t_pulse" in d else 100 + w_pulse = float(d["w_pulse"]) if "w_pulse" in d else 200 + n_pulse = float(d["n_pulse"]) if "n_pulse" in d else 1 + + # Validate input parameters + if wmode not in ["pulse", "pulsed", "toggle", "toggled"]: + raise RuntimeError(f"Unsupported distace triggering mode: {wmode}") + + old = self.read_configuration() + # Configure distance generator (also resets counter to 0) + self._distanceValue = distance + if isinstance(distance, (float, int)): + self.dstDistance.set(distance).wait() + elif isinstance(distance, (np.ndarray, list, tuple)): + self.dstDistanceArr.set(distance).wait() + + self.winEvents.set("Off").wait() + self.dstCounterEna.set("Off").wait() + self.dstEventsEna.set("Off").wait() + + # Configure the pulsed/toggled waveform + if wmode in ["toggle", "toggled"]: + # Switching to simple toggle mode + self.waveEnable.set("On").wait() + self.waveMode.set("Toggle").wait() + + elif wmode in ["pulse", "pulsed"]: + # Switching to pulsed mode + self.waveEnable.set("On").wait() + self.waveMode.set("Pulse").wait() + # Setting pulse shape + if w_pulse is not None: + self.pulseWindow.set(w_pulse).wait() + if t_pulse is not None: + self.pulseOnTime.set(t_pulse).wait() + if n_pulse is not None: + self.pulseCount.set(n_pulse).wait() + # Commiting configuration + self.pulseApply.set(1).wait() + # Enabling PSO waveform outputs + self.waveEnable.set("On").wait() + else: + raise RuntimeError(f"Unsupported waveform mode: {wmode}") + + # Ensure output is set to low + if self.output.value: + self.toggle() + + # Set PSO output data source + self.outSource.set("Waveform").wait() + + new = self.read_configuration() + print("PSO configured") + return (old, new) + + # ######################################################################## + # Bluesky step scan interface + def complete(self, settle_time=0.1) -> DeviceStatus: + """DDC just reads back whatever is available in the buffers""" + sleep(settle_time) + status = DeviceStatus(self) + status.set_finished() + return status + + def trigger(self): + return super().trigger() + + def unstage(self): + # Turn off counter monitoring + self.dstEventsEna.set("Off").wait() + self.dstCounterEna.set("Off").wait() + return super().unstage() + + # ######################################################################## + # Bluesky flyer interface + def kickoff(self) -> DeviceStatus: + # Rearm the configured array + if hasattr(self, "_distanceValue") and isinstance( + self._distanceValue, (np.ndarray, list, tuple) + ): + self.dstArrayRearm.set(1).wait() + # Start monitoring the counters + self.dstEventsEna.set("On").wait() + self.dstCounterEna.set("On").wait() + status = DeviceStatus(self) + status.set_finished() + print("PSO kicked off") + return status + + # def complete(self) -> DeviceStatus: + # """Bluesky flyer interface""" + # # Array mode waits until the buffer is empty + # if hasattr(self, "_distanceValue") and isinstance( + # self._distanceValue, (np.ndarray, list, tuple) + # ): + # # Define wait until the busy flag goes down (excluding initial update) + # timestamp_ = 0 + + # def notRunning(*args, old_value, value, timestamp, **kwargs): + # nonlocal timestamp_ + # result = False if (timestamp_ == 0) else bool(int(value) & 0x1000) + # print(f"Old {old_value}\tNew: {value}\tResult: {result}") + # timestamp_ = timestamp + # return result + + # # Subscribe and wait for update + # # status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) + # # Data capture can be stopped any time + # status = DeviceStatus(self) + # status.set_finished() + # else: + # # In distance trigger mode there's no specific goal + # status = DeviceStatus(self) + # status.set_finished() + # return status + + def describe_collect(self) -> OrderedDict: + ret = OrderedDict() + ret["index"] = { + "source": "internal", + "dtype": "integer", + "shape": [], + "units": "", + "lower_ctrl_limit": 0, + "upper_ctrl_limit": 0, + } + return {self.name: ret} + + def collect(self) -> OrderedDict: + ret = OrderedDict() + ret["timestamps"] = {"index": time.time()} + ret["data"] = {"index": self.dstCounterVal.value} + yield ret + + +class aa1AxisPsoWindow(aa1AxisPsoBase): + """Position Sensitive Output - Window mode + + This class provides convenience wrappers around the Aerotech API's PSO + functionality in window mode. It can either use the event-waveform concept + or provide a direct window output signal (in/out) to the output pin. The + latter is particularly well-suited for the generation of trigger enable + signals, while in event mode it allows the finetuning of trigger lenth. + As a first approach, the module follows a simple pipeline structure: + Genrator --> Event --> Waveform --> Output pin + Genrator --> Window output --> Output pin + + The module provides configuration interface to common functionality, such + as repeated trigger enable signal or fixed area scaning. Unfortunately the + entered positions are absolute, meaning this mode has an inherent limitation + with encoder counters being kept in 32 bit integers. + For a more detailed description of additional signals and masking plase + refer to Automation1's online manual. + """ + + def __init__( + self, + prefix="", + *, + name, + kind=None, + read_attrs=None, + configuration_attrs=None, + parent=None, + **kwargs, + ): + """__init__ MUST have a full argument list""" + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self._mode = "output" + self._eventMode = "Enter" + + # ######################################################################## + # PSO high level interface + def configure(self, d: dict = {}) -> tuple: + """Simplified configuration interface to access the most common + functionality for distance mode PSO. + + :param distance: The trigger distance or the array of distances between subsequent points. + :param wmode: Waveform mode configuration, usually output/pulsed/toggled. + """ + bounds = d["distance"] + wmode = str(d["wmode"]) + emode = str(d["emode"]) + t_pulse = float(d["t_pulse"]) if "t_pulse" in d else None + w_pulse = float(d["w_pulse"]) if "w_pulse" in d else None + n_pulse = float(d["n_pulse"]) if "n_pulse" in d else None + + # Validate input parameters + if wmode not in ["pulse", "pulsed", "toggle", "toggled", "output", "flag"]: + raise RuntimeError(f"Unsupported window triggering mode: {wmode}") + self._mode = wmode + self._eventMode = emode + + old = self.read_configuration() + + # Configure the window module + # Set the window ranges (MUST be in start position) + if len(bounds) == 2: + self.winCounter.set(0).wait() + self._winLower.set(bounds[0]).wait() + self._winUpper.set(bounds[1]).wait() + + elif isinstance(bounds, np.ndarray): + # ToDo... + pass + + # Don't start triggering just yet + self.winOutput.set("Off").wait() + self.winEvents.set("Off").wait() + + # Configure the pulsed/toggled waveform + if wmode in ["toggle", "toggled"]: + # Switching to simple toggle mode + self.waveEnable.set("On").wait() + self.waveMode.set("Toggle").wait() + elif wmode in ["pulse", "pulsed"]: + # Switching to pulsed mode + self.waveEnable.set("On").wait() + self.waveMode.set("Pulse").wait() + # Setting pulse shape + self.pulseWindow.set(w_pulse).wait() + self.pulseOnTime.set(t_pulse).wait() + self.pulseCount.set(n_pulse).wait() + # Commiting configuration + self.pulseApply.set(1).wait() + # Enabling PSO waveform outputs + self.waveEnable.set("On").wait() + elif wmode in ["output", "flag"]: + self.waveEnable.set("Off").wait() + else: + raise RuntimeError(f"Unsupported window mode: {wmode}") + + # Set PSO output data source + if wmode in ["toggle", "toggled", "pulse", "pulsed"]: + self.outSource.set("Waveform").wait() + elif wmode in ["output", "flag"]: + self.outSource.set("Window").wait() + + new = self.read_configuration() + return (old, new) + + def stage(self, settle_time=None): + if self.outSource.get() in ["Window", 2]: + self.winOutput.set("On").wait() + else: + self.winEvents.set(self._eventMode).wait() + if settle_time is not None: + sleep(settle_time) + return super().stage() + + def kickoff(self, settle_time=None): + if self.outSource.get() in ["Window", 2]: + self.winOutput.set("On").wait() + else: + self.winEvents.set(self._eventMode).wait() + if settle_time is not None: + sleep(settle_time) + + def unstage(self, settle_time=None): + self.winOutput.set("Off").wait() + self.winEvents.set("Off").wait() + if settle_time is not None: + sleep(settle_time) + return super().unstage() + + +class aa1AxisDriveDataCollection(Device): + """Axis data collection + + This class provides convenience wrappers around the Aerotech API's axis + specific data collection functionality. This module allows to record + hardware synchronized signals with up to 200 kHz. + + The default configuration is using a fixed memory mapping allowing up to + 1 million recorded data points on an XC4e (this depends on controller). + + Usage: + # Configure the DDC with default internal triggers + ddc = aa1AxisPsoDistance(AA1_IOC_NAME+":ROTY:DDC:", name="ddc") + ddc.wait_for_connection() + ddc.configure(d={'npoints': 5000}) + ddc.kickoff().wait() + ... + ret = yield from ddc.collect() + """ + + # ######################################################################## + # General module status + state = Component(EpicsSignalRO, "STATE", auto_monitor=True, kind=Kind.hinted) + nsamples_rbv = Component(EpicsSignalRO, "SAMPLES_RBV", auto_monitor=True, kind=Kind.hinted) + _switch = Component(EpicsSignal, "ACQUIRE", put_complete=True, kind=Kind.omitted) + _input0 = Component(EpicsSignal, "INPUT0", put_complete=True, kind=Kind.config) + _input1 = Component(EpicsSignal, "INPUT1", put_complete=True, kind=Kind.config) + _trigger = Component(EpicsSignal, "TRIGGER", put_complete=True, kind=Kind.config) + + npoints = Component(EpicsSignal, "NPOINTS", put_complete=True, kind=Kind.config) + _readback0 = Component(EpicsSignal, "AREAD0", kind=Kind.omitted) + _readstatus0 = Component(EpicsSignalRO, "AREAD0_RBV", auto_monitor=True, kind=Kind.omitted) + _readback1 = Component(EpicsSignal, "AREAD1", kind=Kind.omitted) + _readstatus1 = Component(EpicsSignalRO, "AREAD1_RBV", auto_monitor=True, kind=Kind.omitted) + + _buffer0 = Component(EpicsSignalRO, "BUFFER0", auto_monitor=True, kind=Kind.hinted) + _buffer1 = Component(EpicsSignalRO, "BUFFER1", auto_monitor=True, kind=Kind.hinted) + + 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, "progress", run=False) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + if self.state.value not in (2, "Acquiring"): + self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) + return + + progress = 1 + max_value = 1 + self._run_subs( + sub_type=self.SUB_PROGRESS, + value=int(progress), + max_value=max_value, + done=int(np.isclose(max_value, progress, 1e-3)), + ) + + def configure(self, d: dict = {}) -> tuple: + npoints = int(d["npoints"]) + trigger = int(d["trigger"]) if "trigger" in d else DriveDataCaptureTrigger.PsoOutput + source0 = int(d["source0"]) if "source0" in d else DriveDataCaptureInput.PrimaryFeedback + source1 = int(d["source1"]) if "source1" in d else DriveDataCaptureInput.PositionCommand + + old = self.read_configuration() + + self._input0.set(source0).wait() + self._input1.set(source1).wait() + self._trigger.set(trigger).wait() + # This allocates the memory... + self.npoints.set(npoints).wait() + + new = self.read_configuration() + return (old, new) + + # Bluesky step scanning interface + def stage(self, settle_time=0.1): + self._switch.set("Start", settle_time=0.5).wait() + return super().stage() + + def unstage(self, settle_time=0.1): + self._switch.set("Stop", settle_time=settle_time).wait() + super().unstage() + print(f"Recorded samples: {self.nsamples_rbv.value}") + + # Bluesky flyer interface + def kickoff(self, settle_time=0.1) -> Status: + status = self._switch.set("Start", settle_time=settle_time) + return status + + def complete(self, settle_time=0.1) -> DeviceStatus: + """DDC just reads back whatever is available in the buffers""" + sleep(settle_time) + status = DeviceStatus(self) + status.set_finished() + return status + + def _collect(self, index=0): + """Force a readback of the data buffer + + Note that there's a weird behaviour in ophyd that it issues an + initial update event with the initial value but 0 timestamp. Theese + old_values are invalid and must be filtered out. + """ + + # Define wait until the busy flag goes down (excluding initial update) + timestamp_ = 0 + + def negEdge(*args, old_value, value, timestamp, **kwargs): + nonlocal timestamp_ + result = False if (timestamp_ == 0) else (old_value == 1 and value == 0) + # print(f"\nBuffer1 status:\t{old_value} ({timestamp_}) to {value} ({timestamp}) Result: {result}") + timestamp_ = timestamp + return result + + if index == 0: + status = SubscriptionStatus(self._readstatus0, negEdge, settle_time=0.5) + self._readback0.set(1).wait() + elif index == 1: + status = SubscriptionStatus(self._readstatus1, negEdge, settle_time=0.5) + self._readback1.set(1).wait() + + # Start asynchronous readback + status.wait() + return status + + def describe_collect(self) -> OrderedDict: + ret = OrderedDict() + ret["buffer0"] = { + "source": "internal", + "dtype": "array", + "shape": [], + "units": "", + "lower_ctrl_limit": 0, + "upper_ctrl_limit": 0, + } + ret["buffer1"] = { + "source": "internal", + "dtype": "array", + "shape": [], + "units": "", + "lower_ctrl_limit": 0, + "upper_ctrl_limit": 0, + } + return {self.name: ret} + + def collect(self) -> OrderedDict: + + self._collect(0).wait() + self._collect(1).wait() + + b0 = self._buffer0.value + b1 = self._buffer1.value + ret = OrderedDict() + ret["timestamps"] = {"buffer0": time.time(), "buffer1": time.time()} + ret["data"] = {"buffer0": b0, "buffer1": b1} + yield ret + + +# Automatically start simulation if directly invoked +if __name__ == "__main__": + AA1_IOC_NAME = "X02DA-ES1-SMP1" + AA1_AXIS_NAME = "ROTY" + # Drive data collection + task = aa1Tasks(AA1_IOC_NAME + ":TASK:", name="tsk") + task.wait_for_connection() + task.describe() + ddc = aa1AxisDriveDataCollection("X02DA-ES1-SMP1:ROTY:DDC:", name="ddc") + ddc.wait_for_connection() + globb = aa1GlobalVariableBindings(AA1_IOC_NAME + ":VAR:", name="globb") + globb.wait_for_connection() + globb.describe() + mot = EpicsMotor(AA1_IOC_NAME + ":" + AA1_AXIS_NAME, name="x") + mot.wait_for_connection() diff --git a/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py b/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py new file mode 100644 index 0000000..ce75eba --- /dev/null +++ b/tomcat_bec/devices/aerotech/AerotechAutomation1Enums.py @@ -0,0 +1,901 @@ +from enum import Enum + + +class TomcatSequencerState: + IDLE = 0 + READY = 1 + ARMED = 2 + RUNNING = 3 + CHANGING = 4 + ERROR = 15 + + +class AxisDataSignal: + PositionFeedback = 0 + PositionCommand = 1 + PositionError = 2 + VelocityFeedback = 3 + VelocityCommand = 4 + VelocityError = 5 + AccelerationCommand = 6 + CurrentError = 9 + PositionCommandRaw = 12 + VelocityCommandRaw = 13 + AuxiliaryFeedback = 14 + DigitalInput = 15 + DigitalOutput = 16 + FixtureOffset = 18 + CoordinatedPositionTarget = 41 + DriveStatus = 42 + AxisStatus = 43 + AxisFault = 44 + AccelerationCommandRaw = 45 + PositionCalibrationAll = 50 + PositionFeedbackRollover = 63 + PositionCommandRollover = 64 + VelocityFeedbackAverage = 65 + CurrentFeedbackAverage = 66 + AxisParameter = 68 + Backlash = 72 + HomeState = 73 + PositionCalibration2D = 74 + NormalcyDebug = 75 + TotalMoveTime = 76 + JerkCommandRaw = 78 + ProgramPositionCommand = 79 + PositionOffset = 80 + PositionCommandRawBackwardsDiff = 82 + VelocityCommandRawBackwardsDiffDelta = 83 + DriveStatusActual = 85 + ProgramPositionFeedback = 89 + JogTrajectoryStatus = 94 + PingTest = 95 + AccelerationTime = 109 + DecelerationTime = 110 + AccelerationRate = 111 + DecelerationRate = 112 + AccelerationType = 113 + DecelerationType = 114 + AccelerationMode = 115 + DecelerationMode = 116 + ProgramPosition = 124 + SpeedTarget = 128 + PositionCommandPacket = 131 + DriveSmcMotionState = 132 + PositionCommandRawCal = 140 + VelocityCommandRawCal = 141 + VelocityCommandDrive = 142 + AccelerationCommandDrive = 143 + GalvoLaserOutputRaw = 144 + DriveInterfacePacketInt32 = 147 + DriveInterfacePacketInt16 = 148 + DriveInterfacePacketInt8 = 149 + DriveInterfacePacketDouble = 150 + DriveInterfacePacketFloat = 151 + DriveInterfaceCommandCode = 152 + AccelerationFeedback = 153 + AccelerationCommandRawCal = 154 + PositionCalibrationAllDrive = 155 + BacklashTarget = 156 + DriveMotionRate = 158 + DriveMotionDelay = 159 + CalibrationAdjustmentValue = 160 + ServoRounding = 161 + FeedforwardCurrent = 162 + DriveInterfacePacketInfoBitValue = 164 + AccelerationError = 165 + SuppressedFaults = 167 + DriveInterfacePacketStreamingData = 168 + PositionCommandRawUnfiltered = 169 + TransitionOffsetErrors = 170 + FreezeVelocityCommand = 179 + FreezeVelocityFeedback = 180 + InternalPositionOffsets = 181 + StatusHighLevelOffsetsLastMsec = 182 + ProgramVelocityCommand = 183 + ProgramVelocityFeedback = 184 + DriveMotionDelayLive = 185 + DriveCommunicationDelay = 186 + DriveCommunicationDelayLive = 187 + DriveInterfacePacketResponseInt32 = 189 + DriveInterfacePacketResponseInt16 = 190 + DriveInterfacePacketResponseInt8 = 191 + DriveInterfacePacketResponseDouble = 192 + DriveInterfacePacketResponseFloat = 193 + DriveInterfacePacketBit = 194 + DriveInterfacePacketResponseBit = 195 + SpeedTargetActual = 196 + CoordinatedDistanceRemaining = 199 + SafeZoneState = 230 + PositionErrorGalvo = 235 + MoveReferencePosition = 237 + MoveReferenceCutterOffset = 250 + MoveReferenceCornerOffset = 251 + MoveReferenceTotalOffset = 252 + DistanceLog = 264 + AutoFocusError = 295 + GalvoLaserOutputRawAdvance = 296 + GalvoLaserOnDelay = 297 + GalvoLaserOffDelay = 298 + CalibrationAdjustmentState = 301 + AccuracyCorrectionStartingPosition = 302 + AccuracyCorrectionEndingPosition = 303 + DriveCommandsDelayed = 309 + DriveCommandsLost = 310 + StoStatus = 327 + DriveAssert = 336 + PrimaryFeedback = 366 + AccelerationSCurvePercentage = 371 + DecelerationSCurvePercentage = 372 + GantryMarkerDifference = 390 + PrimaryFeedbackStatus = 392 + HomeTargetPosition = 398 + GantryRealignmentMoveTargetPosition = 399 + GantryDriveControlRealignmentState = 400 + DriveInterfacePositionCommandPhysical = 434 + DriveControlReason = 435 + CurrentFeedback = 7 + CurrentCommand = 8 + AnalogInput0 = 10 + AnalogInput1 = 11 + PhaseACurrentFeedback = 19 + PhaseBCurrentFeedback = 20 + EncoderSine = 21 + EncoderCosine = 22 + AnalogInput2 = 23 + AnalogInput3 = 24 + FrequencyResponseBefore = 25 + FrequencyResponseAfter = 26 + AnalogOutput0 = 31 + AnalogOutput1 = 32 + AnalogOutput2 = 33 + AnalogOutput3 = 34 + DriveMemoryInt32 = 35 + DriveMemoryFloat = 36 + DriveMemoryDouble = 37 + PsoStatus = 38 + DriveTimerDebug = 39 + PositionFeedbackDrive = 77 + PositionCommandDrive = 84 + DriveMemoryInt16 = 125 + DriveMemoryInt8 = 126 + PsoCounter0 = 171 + PsoCounter1 = 172 + PsoCounter2 = 173 + PsoWindow0 = 174 + PsoWindow1 = 175 + DriveDataCaptureSamples = 176 + PositionCommandGalvo = 178 + PrimaryEnDatAbsolutePosition = 197 + ControlEffort = 201 + PhaseAVoltageCommand = 208 + PhaseBVoltageCommand = 209 + PhaseCVoltageCommand = 210 + AmplifierPeakCurrent = 211 + FpgaVersion = 212 + DriveTypeId = 213 + PsoWindow0ArrayIndex = 214 + PsoWindow1ArrayIndex = 215 + PsoDistanceArrayIndex = 216 + AmplifierTemperature = 217 + PsoBitArrayIndex = 218 + MxAbsolutePosition = 219 + ServoUpdateRate = 220 + SettlingTime = 221 + InternalStatusCode = 222 + FirmwareVersionMajor = 223 + FirmwareVersionMinor = 224 + FirmwareVersionPatch = 225 + FirmwareVersionBuild = 226 + DriveTimerDebugMax = 227 + MarkerSearchDistance = 228 + PositionFeedbackGalvo = 234 + LatchedMarkerPosition = 236 + PrimaryBissAbsolutePosition = 255 + FaultPositionFeedback = 258 + MotorCommutationAngle = 259 + ExpansionBoardOption = 260 + BusVoltage = 261 + PiezoVoltageCommand = 262 + PiezoVoltageFeedback = 263 + TimeSinceReset = 273 + MaximumVoltage = 274 + CommandOutputType = 275 + DriveFeedforwardOutput = 290 + LastTickCounter = 291 + BoardRevision = 292 + GalvoLaserOutput = 294 + GalvoLaserPowerCorrectionOutput = 299 + CapacitanceSensorRawPosition = 300 + PositionCalibrationGalvo = 304 + BusVoltageNegative = 325 + ProcessorTemperature = 326 + InternalStatusTimestamp = 328 + AnalogSensorInput = 329 + MotorTemperature = 330 + PrimaryBissStatus = 332 + PsoExternalSyncFrequency = 337 + EncoderSineRaw = 346 + EncoderCosineRaw = 347 + FpgaTemperature = 353 + PrimaryEnDatStatus = 355 + DriveTimerHighPriorityThread = 356 + DriveTimerLowPriorityThread = 357 + DriveTimerLowPriorityPacket = 358 + DriveTimerServoPacket = 359 + DriveTimerServoThread = 360 + DriveTimerCurrentPacket = 361 + DriveTimerCommonCoreThread = 362 + DriveTimerServoCorePacket0 = 363 + DriveTimerServoCorePacket1 = 364 + MultiplierOption = 365 + ServoLoopFeedbackInput0 = 367 + ServoLoopFeedbackInput1 = 368 + FaultSubcode = 376 + ProcessorTemperatureMax = 378 + DriveTimerHyperWireDma = 381 + AmplifierTemperatureMax = 382 + AuxiliaryEnDatAbsolutePosition = 383 + AuxiliaryEnDatStatus = 384 + AuxiliaryBissAbsolutePosition = 385 + AuxiliaryBissStatus = 386 + PsoOption = 387 + DriveArraySize = 388 + RatedMotorSupplyVoltageOption = 389 + AbsoluteEncoderOption = 391 + AuxiliaryFeedbackStatus = 393 + AmplifierStatus = 394 + LatchedCwLimitPosition = 395 + LatchedCcwLimitPosition = 396 + GalvoLaserFpgaTransitionDelay = 397 + PiezoAccumulatedCharge = 401 + PiezoChargingTime = 402 + PrimarySsiAbsolutePosition = 403 + PrimarySsiStatus = 404 + AuxiliarySsiAbsolutePosition = 405 + AuxiliarySsiStatus = 406 + PsoDistanceActiveDistance = 407 + PsoWindow0ActiveLowerBound = 408 + PsoWindow0ActiveUpperBound = 409 + PsoWindow1ActiveLowerBound = 410 + PsoWindow1ActiveUpperBound = 411 + PsoWaveformActiveTotalTime = 412 + PsoWaveformActiveOnTime = 413 + PsoWaveformActivePulseCount = 414 + PsoEventActiveBitValue = 415 + DriveTimerDriveBasedControllerOutputDma = 419 + DriveTimerPcieInboundFsm = 420 + PrimaryFeedbackServo = 425 + AuxiliaryFeedbackServo = 426 + DriveStackUsage = 427 + ShuntResistorTemperature = 436 + + +class TaskDataSignal: + ProgramLineNumber = 17 + CoordinatedFlags = 40 + CoordinatedArcStartAngle = 53 + CoordinatedArcEndAngle = 54 + CoordinatedArcRadius = 55 + CoordinatedArcRadiusError = 56 + CoordinatedPositionCommand = 57 + CoordinatedSpeedCommand = 58 + CoordinatedAccelerationCommand = 59 + CoordinatedTotalDistance = 60 + CoordinatedPercentDone = 61 + CoordinatedPositionCommandBackwardsDiff = 62 + TaskParameter = 69 + TaskError = 70 + TaskWarning = 71 + CoordinatedSpeedTargetActual = 86 + DependentCoordinatedSpeedTargetActual = 87 + ActiveFixtureOffset = 88 + TaskStatus0 = 90 + TaskStatus1 = 91 + TaskStatus2 = 92 + SpindleSpeedTarget = 93 + CoordinateSystem1I = 96 + CoordinateSystem1J = 97 + CoordinateSystem1K = 98 + CoordinateSystem1Plane = 99 + ToolNumberActive = 100 + Mfo = 101 + CoordinatedSpeedTarget = 102 + DependentCoordinatedSpeedTarget = 103 + CoordinatedAccelerationRate = 104 + CoordinatedDecelerationRate = 105 + CoordinatedAccelerationTime = 106 + CoordinatedDecelerationTime = 107 + TaskMode = 108 + TaskState = 117 + TaskStateInternal = 118 + ExecutionMode = 121 + EnableAlignmentAxes = 127 + CoordinatedGalvoLaserOutput = 133 + CoordinatedMotionRate = 145 + CoordinatedTaskCommand = 146 + EnableState = 166 + LookaheadMovesExamined = 200 + ProfileControlMask = 231 + CoordinatedArcRadiusReciprocal = 253 + MotionEngineStage = 254 + CoordinatedTimeScale = 256 + CoordinatedTimeScaleDerivative = 257 + IfovSpeedScale = 266 + IfovSpeedScaleAverage = 267 + IfovGenerationFrameCounter = 268 + IfovGenerationTimeOriginal = 269 + IfovGenerationTimeModified = 270 + IfovCoordinatedPositionCommand = 271 + IfovCoordinatedSpeedCommand = 272 + IfovCenterPointH = 276 + IfovCenterPointV = 277 + IfovTrajectoryCount = 278 + IfovTrajectoryIndex = 279 + IfovAttemptCode = 280 + IfovGenerationFrameIndex = 281 + IfovMaximumVelocity = 282 + IfovIdealVelocity = 283 + TaskInternalDebug = 284 + IfovCoordinatedAccelerationCommand = 285 + IfovFovPositionH = 286 + IfovFovPositionV = 287 + IfovFovDimensionH = 288 + IfovFovDimensionV = 289 + MotionBufferElements = 311 + MotionBufferMoves = 312 + MotionLineNumber = 313 + MotionBufferRetraceMoves = 314 + MotionBufferRetraceElements = 315 + MotionBufferIndex = 316 + MotionBufferIndexLookahead = 317 + MotionBufferProcessingBlocked = 318 + ActiveMoveValid = 319 + TaskExecutionLines = 320 + SchedulerTaskHolds = 321 + SchedulerProgramLoopRuns = 322 + SchedulerTaskBlocked = 323 + CriticalSectionsActive = 324 + AxesSlowdownReason = 331 + TaskExecutionTime = 333 + TaskExecutionTimeMaximum = 334 + TaskExecutionLinesMaximum = 335 + LookaheadDecelReason = 338 + LookaheadDecelMoves = 339 + LookaheadDecelDistance = 340 + ProgramCounter = 341 + StackPointer = 342 + FramePointer = 343 + StringStackPointer = 344 + ProgramLineNumberSourceFileId = 349 + MotionLineNumberSourceFileId = 350 + ProgramLineNumberSourcePathId = 351 + MotionLineNumberSourcePathId = 352 + StringArgumentStackPointer = 354 + CoordinatedAccelerationSCurvePercentage = 369 + CoordinatedDecelerationSCurvePercentage = 370 + DependentCoordinatedAccelerationRate = 373 + DependentCoordinatedDecelerationRate = 374 + CriticalSectionTimeout = 375 + CommandQueueCapacity = 421 + CommandQueueUnexecutedCount = 422 + CommandQueueTimesEmptied = 423 + CommandQueueExecutedCount = 424 + + +class SystemDataSignal: + VirtualBinaryInput = 46 + VirtualBinaryOutput = 47 + VirtualRegisterInput = 48 + VirtualRegisterOutput = 49 + Timer = 51 + TimerPerformance = 52 + GlobalReal = 67 + CommunicationRealTimeErrors = 81 + LibraryCommand = 119 + DataCollectionSampleTime = 120 + DataCollectionSampleIndex = 129 + ModbusClientConnected = 134 + ModbusServerConnected = 135 + ModbusClientError = 136 + ModbusServerError = 137 + StopWatchTimer = 157 + ScopetrigId = 163 + EstimatedProcessorUsage = 177 + DataCollectionStatus = 188 + SignalLogState = 198 + SafeZoneViolationMask = 207 + SafeZoneActiveMask = 229 + ModbusClientInputWords = 240 + ModbusClientOutputWords = 241 + ModbusClientInputBits = 242 + ModbusClientOutputBits = 243 + ModbusClientOutputBitsStatus = 244 + ModbusClientOutputWordsStatus = 245 + ModbusServerInputWords = 246 + ModbusServerOutputWords = 247 + ModbusServerInputBits = 248 + ModbusServerOutputBits = 249 + SystemParameter = 265 + ThermoCompSensorTemperature = 305 + ThermoCompControllingTemperature = 306 + ThermoCompCompensatingTemperature = 307 + ThermoCompStatus = 308 + GlobalInteger = 345 + AliveAxesMask = 348 + SignalLogPointsStored = 377 + ControllerInitializationWarning = 379 + StopWatchTimerMin = 416 + StopWatchTimerMax = 417 + StopWatchTimerAvg = 418 + EthercatEnabled = 428 + EthercatError = 429 + EthercatTxPdo = 430 + EthercatTxPdoSize = 431 + EthercatRxPdo = 432 + EthercatRxPdoSize = 433 + EthercatState = 437 + ModbusClientEnabled = 438 + ModbusServerEnabled = 439 + + +class DataCollectionFrequency: + Undefined = 0 + Fixed1kHz = 1 + Fixed10kHz = 2 + Fixed20kHz = 3 + Fixed100kHz = 4 + Fixed200kHz = 5 + + +class DataCollectionMode: + Snapshot = 0 + Continouous = 1 + + +# Specifies the PSO distance input settings for the XC4e drive. +class PsoDistanceInput: + XC4PrimaryFeedback = 130 + XC4AuxiliaryFeedback = 131 + XC4SyncPortA = 132 + XC4SyncPortB = 133 + XC4DrivePulseStream = 134 + XC4ePrimaryFeedback = 135 + XC4eAuxiliaryFeedback = 136 + XC4eSyncPortA = 137 + XC4eSyncPortB = 138 + XC4eDrivePulseStream = 139 + + +class PsoWindowInput: + XC4PrimaryFeedback = 130 + XC4AuxiliaryFeedback = 131 + XC4SyncPortA = 132 + XC4SyncPortB = 133 + XC4DrivePulseStream = 134 + XC4ePrimaryFeedback = 135 + XC4eAuxiliaryFeedback = 136 + XC4eSyncPortA = 137 + XC4eSyncPortB = 138 + XC4eDrivePulseStream = 139 + XL5ePrimaryFeedback = (145,) + XL5eAuxiliaryFeedback = (146,) + XL5eSyncPortA = (147,) + XL5eSyncPortB = (148,) + XL5eDrivePulseStream = (149,) + + +# @brief Specifies the PSO output pin settings for each drive. +class XC4ePsoOutputPin: + DedicatedOutput = 111 + AuxiliaryMarkerDifferential = 112 + AuxiliaryMarkerSingleEnded = 113 + + +class XC4PsoOutputPin: + DedicatedOutput = 108 + AuxiliaryMarkerDifferential = 109 + AuxiliaryMarkerSingleEnded = 110 + + +""" +# @brief Specifies the PSO distance input settings for each drive. +class Automation1PsoDistanceInput: + Automation1PsoDistanceInput_GL4PrimaryFeedbackAxis1 = 100, + Automation1PsoDistanceInput_GL4PrimaryFeedbackAxis2 = 101, + Automation1PsoDistanceInput_GL4IfovFeedbackAxis1 = 102, + Automation1PsoDistanceInput_GL4IfovFeedbackAxis2 = 103, + Automation1PsoDistanceInput_GL4AuxiliaryFeedbackAxis1 = 104, + Automation1PsoDistanceInput_GL4AuxiliaryFeedbackAxis2 = 105, + Automation1PsoDistanceInput_GL4SyncPortA = 106, + Automation1PsoDistanceInput_GL4SyncPortB = 107, + Automation1PsoDistanceInput_GL4DrivePulseStreamAxis1 = 108, + Automation1PsoDistanceInput_GL4DrivePulseStreamAxis2 = 109, + Automation1PsoDistanceInput_XL4sPrimaryFeedback = 110, + Automation1PsoDistanceInput_XL4sAuxiliaryFeedback = 111, + Automation1PsoDistanceInput_XL4sSyncPortA = 112, + Automation1PsoDistanceInput_XL4sSyncPortB = 113, + Automation1PsoDistanceInput_XL4sDrivePulseStream = 114, + Automation1PsoDistanceInput_XR3PrimaryFeedbackAxis1 = 115, + Automation1PsoDistanceInput_XR3PrimaryFeedbackAxis2 = 116, + Automation1PsoDistanceInput_XR3PrimaryFeedbackAxis3 = 117, + Automation1PsoDistanceInput_XR3PrimaryFeedbackAxis4 = 118, + Automation1PsoDistanceInput_XR3PrimaryFeedbackAxis5 = 119, + Automation1PsoDistanceInput_XR3PrimaryFeedbackAxis6 = 120, + Automation1PsoDistanceInput_XR3AuxiliaryFeedbackAxis1 = 121, + Automation1PsoDistanceInput_XR3AuxiliaryFeedbackAxis2 = 122, + Automation1PsoDistanceInput_XR3AuxiliaryFeedbackAxis3 = 123, + Automation1PsoDistanceInput_XR3AuxiliaryFeedbackAxis4 = 124, + Automation1PsoDistanceInput_XR3AuxiliaryFeedbackAxis5 = 125, + Automation1PsoDistanceInput_XR3AuxiliaryFeedbackAxis6 = 126, + Automation1PsoDistanceInput_XR3SyncPortA = 127, + Automation1PsoDistanceInput_XR3SyncPortB = 128, + Automation1PsoDistanceInput_XR3DrivePulseStream = 129, + Automation1PsoDistanceInput_XC4PrimaryFeedback = 130, + Automation1PsoDistanceInput_XC4AuxiliaryFeedback = 131, + Automation1PsoDistanceInput_XC4SyncPortA = 132, + Automation1PsoDistanceInput_XC4SyncPortB = 133, + Automation1PsoDistanceInput_XC4DrivePulseStream = 134, + XC4ePrimaryFeedback = 135, + XC4eAuxiliaryFeedback = 136, + XC4eSyncPortA = 137, + XC4eSyncPortB = 138, + XC4eDrivePulseStream = 139, + Automation1PsoDistanceInput_XC6ePrimaryFeedback = 140, + Automation1PsoDistanceInput_XC6eAuxiliaryFeedback = 141, + Automation1PsoDistanceInput_XC6eSyncPortA = 142, + Automation1PsoDistanceInput_XC6eSyncPortB = 143, + Automation1PsoDistanceInput_XC6eDrivePulseStream = 144, + Automation1PsoDistanceInput_XL5ePrimaryFeedback = 145, + Automation1PsoDistanceInput_XL5eAuxiliaryFeedback = 146, + Automation1PsoDistanceInput_XL5eSyncPortA = 147, + Automation1PsoDistanceInput_XL5eSyncPortB = 148, + Automation1PsoDistanceInput_XL5eDrivePulseStream = 149, + Automation1PsoDistanceInput_XC2PrimaryFeedback = 150, + Automation1PsoDistanceInput_XC2AuxiliaryFeedback = 151, + Automation1PsoDistanceInput_XC2DrivePulseStream = 152, + Automation1PsoDistanceInput_XC2ePrimaryFeedback = 153, + Automation1PsoDistanceInput_XC2eAuxiliaryFeedback = 154, + Automation1PsoDistanceInput_XC2eDrivePulseStream = 155, + Automation1PsoDistanceInput_XL2ePrimaryFeedback = 156, + Automation1PsoDistanceInput_XL2eAuxiliaryFeedback = 157, + Automation1PsoDistanceInput_XL2eSyncPortA = 158, + Automation1PsoDistanceInput_XL2eSyncPortB = 159, + Automation1PsoDistanceInput_XL2eDrivePulseStream = 160, + Automation1PsoDistanceInput_XI4PrimaryFeedbackAxis1 = 161, + Automation1PsoDistanceInput_XI4PrimaryFeedbackAxis2 = 162, + Automation1PsoDistanceInput_XI4PrimaryFeedbackAxis3 = 163, + Automation1PsoDistanceInput_XI4PrimaryFeedbackAxis4 = 164, + Automation1PsoDistanceInput_XI4AuxiliaryFeedback1 = 165, + Automation1PsoDistanceInput_XI4AuxiliaryFeedback2 = 166, + Automation1PsoDistanceInput_XI4AuxiliaryFeedback3 = 167, + Automation1PsoDistanceInput_XI4AuxiliaryFeedback4 = 168, + Automation1PsoDistanceInput_XI4SyncPortA = 169, + Automation1PsoDistanceInput_XI4SyncPortB = 170, + Automation1PsoDistanceInput_XI4DrivePulseStreamAxis1 = 171, + Automation1PsoDistanceInput_XI4DrivePulseStreamAxis2 = 172, + Automation1PsoDistanceInput_XI4DrivePulseStreamAxis3 = 173, + Automation1PsoDistanceInput_XI4DrivePulseStreamAxis4 = 174, + Automation1PsoDistanceInput_iXC4PrimaryFeedback = 175, + Automation1PsoDistanceInput_iXC4AuxiliaryFeedback = 176, + Automation1PsoDistanceInput_iXC4SyncPortA = 177, + Automation1PsoDistanceInput_iXC4SyncPortB = 178, + Automation1PsoDistanceInput_iXC4DrivePulseStream = 179, + Automation1PsoDistanceInput_iXC4ePrimaryFeedback = 180, + Automation1PsoDistanceInput_iXC4eAuxiliaryFeedback = 181, + Automation1PsoDistanceInput_iXC4eSyncPortA = 182, + Automation1PsoDistanceInput_iXC4eSyncPortB = 183, + Automation1PsoDistanceInput_iXC4eDrivePulseStream = 184, + Automation1PsoDistanceInput_iXC6ePrimaryFeedback = 185, + Automation1PsoDistanceInput_iXC6eAuxiliaryFeedback = 186, + Automation1PsoDistanceInput_iXC6eSyncPortA = 187, + Automation1PsoDistanceInput_iXC6eSyncPortB = 188, + Automation1PsoDistanceInput_iXC6eDrivePulseStream = 189, + Automation1PsoDistanceInput_iXL5ePrimaryFeedback = 190, + Automation1PsoDistanceInput_iXL5eAuxiliaryFeedback = 191, + Automation1PsoDistanceInput_iXL5eSyncPortA = 192, + Automation1PsoDistanceInput_iXL5eSyncPortB = 193, + Automation1PsoDistanceInput_iXL5eDrivePulseStream = 194, + Automation1PsoDistanceInput_iXR3PrimaryFeedbackAxis1 = 195, + Automation1PsoDistanceInput_iXR3PrimaryFeedbackAxis2 = 196, + Automation1PsoDistanceInput_iXR3PrimaryFeedbackAxis3 = 197, + Automation1PsoDistanceInput_iXR3PrimaryFeedbackAxis4 = 198, + Automation1PsoDistanceInput_iXR3PrimaryFeedbackAxis5 = 199, + Automation1PsoDistanceInput_iXR3PrimaryFeedbackAxis6 = 200, + Automation1PsoDistanceInput_iXR3AuxiliaryFeedbackAxis1 = 201, + Automation1PsoDistanceInput_iXR3AuxiliaryFeedbackAxis2 = 202, + Automation1PsoDistanceInput_iXR3AuxiliaryFeedbackAxis3 = 203, + Automation1PsoDistanceInput_iXR3AuxiliaryFeedbackAxis4 = 204, + Automation1PsoDistanceInput_iXR3AuxiliaryFeedbackAxis5 = 205, + Automation1PsoDistanceInput_iXR3AuxiliaryFeedbackAxis6 = 206, + Automation1PsoDistanceInput_iXR3SyncPortA = 207, + Automation1PsoDistanceInput_iXR3SyncPortB = 208, + Automation1PsoDistanceInput_iXR3DrivePulseStream = 209, + Automation1PsoDistanceInput_GI4DrivePulseStreamAxis1 = 210, + Automation1PsoDistanceInput_GI4DrivePulseStreamAxis2 = 211, + Automation1PsoDistanceInput_GI4DrivePulseStreamAxis3 = 212, + Automation1PsoDistanceInput_iXC2PrimaryFeedback = 213, + Automation1PsoDistanceInput_iXC2AuxiliaryFeedback = 214, + Automation1PsoDistanceInput_iXC2DrivePulseStream = 215, + Automation1PsoDistanceInput_iXC2ePrimaryFeedback = 216, + Automation1PsoDistanceInput_iXC2eAuxiliaryFeedback = 217, + Automation1PsoDistanceInput_iXC2eDrivePulseStream = 218, + Automation1PsoDistanceInput_iXL2ePrimaryFeedback = 219, + Automation1PsoDistanceInput_iXL2eAuxiliaryFeedback = 220, + Automation1PsoDistanceInput_iXL2eSyncPortA = 221, + Automation1PsoDistanceInput_iXL2eSyncPortB = 222, + Automation1PsoDistanceInput_iXL2eDrivePulseStream = 223, + Automation1PsoDistanceInput_iXI4PrimaryFeedbackAxis1 = 224, + Automation1PsoDistanceInput_iXI4PrimaryFeedbackAxis2 = 225, + Automation1PsoDistanceInput_iXI4PrimaryFeedbackAxis3 = 226, + Automation1PsoDistanceInput_iXI4PrimaryFeedbackAxis4 = 227, + Automation1PsoDistanceInput_iXI4AuxiliaryFeedback1 = 228, + Automation1PsoDistanceInput_iXI4AuxiliaryFeedback2 = 229, + Automation1PsoDistanceInput_iXI4AuxiliaryFeedback3 = 230, + Automation1PsoDistanceInput_iXI4AuxiliaryFeedback4 = 231, + Automation1PsoDistanceInput_iXI4SyncPortA = 232, + Automation1PsoDistanceInput_iXI4SyncPortB = 233, + Automation1PsoDistanceInput_iXI4DrivePulseStreamAxis1 = 234, + Automation1PsoDistanceInput_iXI4DrivePulseStreamAxis2 = 235, + Automation1PsoDistanceInput_iXI4DrivePulseStreamAxis3 = 236, + Automation1PsoDistanceInput_iXI4DrivePulseStreamAxis4 = 237, + +# @brief Specifies the PSO window input settings for each drive. +class Automation1PsoWindowInput: + Automation1PsoWindowInput_GL4PrimaryFeedbackAxis1 = 100, + Automation1PsoWindowInput_GL4PrimaryFeedbackAxis2 = 101, + Automation1PsoWindowInput_GL4IfovFeedbackAxis1 = 102, + Automation1PsoWindowInput_GL4IfovFeedbackAxis2 = 103, + Automation1PsoWindowInput_GL4AuxiliaryFeedbackAxis1 = 104, + Automation1PsoWindowInput_GL4AuxiliaryFeedbackAxis2 = 105, + Automation1PsoWindowInput_GL4SyncPortA = 106, + Automation1PsoWindowInput_GL4SyncPortB = 107, + Automation1PsoWindowInput_GL4DrivePulseStreamAxis1 = 108, + Automation1PsoWindowInput_GL4DrivePulseStreamAxis2 = 109, + Automation1PsoWindowInput_XL4sPrimaryFeedback = 110, + Automation1PsoWindowInput_XL4sAuxiliaryFeedback = 111, + Automation1PsoWindowInput_XL4sSyncPortA = 112, + Automation1PsoWindowInput_XL4sSyncPortB = 113, + Automation1PsoWindowInput_XL4sDrivePulseStream = 114, + Automation1PsoWindowInput_XR3PrimaryFeedbackAxis1 = 115, + Automation1PsoWindowInput_XR3PrimaryFeedbackAxis2 = 116, + Automation1PsoWindowInput_XR3PrimaryFeedbackAxis3 = 117, + Automation1PsoWindowInput_XR3PrimaryFeedbackAxis4 = 118, + Automation1PsoWindowInput_XR3PrimaryFeedbackAxis5 = 119, + Automation1PsoWindowInput_XR3PrimaryFeedbackAxis6 = 120, + Automation1PsoWindowInput_XR3AuxiliaryFeedbackAxis1 = 121, + Automation1PsoWindowInput_XR3AuxiliaryFeedbackAxis2 = 122, + Automation1PsoWindowInput_XR3AuxiliaryFeedbackAxis3 = 123, + Automation1PsoWindowInput_XR3AuxiliaryFeedbackAxis4 = 124, + Automation1PsoWindowInput_XR3AuxiliaryFeedbackAxis5 = 125, + Automation1PsoWindowInput_XR3AuxiliaryFeedbackAxis6 = 126, + Automation1PsoWindowInput_XR3SyncPortA = 127, + Automation1PsoWindowInput_XR3SyncPortB = 128, + Automation1PsoWindowInput_XR3DrivePulseStream = 129, + Automation1PsoWindowInput_XC4PrimaryFeedback = 130, + Automation1PsoWindowInput_XC4AuxiliaryFeedback = 131, + Automation1PsoWindowInput_XC4SyncPortA = 132, + Automation1PsoWindowInput_XC4SyncPortB = 133, + Automation1PsoWindowInput_XC4DrivePulseStream = 134, + XC4ePrimaryFeedback = 135, + XC4eAuxiliaryFeedback = 136, + XC4eSyncPortA = 137, + XC4eSyncPortB = 138, + XC4eDrivePulseStream = 139, + Automation1PsoWindowInput_XC6ePrimaryFeedback = 140, + Automation1PsoWindowInput_XC6eAuxiliaryFeedback = 141, + Automation1PsoWindowInput_XC6eSyncPortA = 142, + Automation1PsoWindowInput_XC6eSyncPortB = 143, + Automation1PsoWindowInput_XC6eDrivePulseStream = 144, + Automation1PsoWindowInput_XL5ePrimaryFeedback = 145, + Automation1PsoWindowInput_XL5eAuxiliaryFeedback = 146, + Automation1PsoWindowInput_XL5eSyncPortA = 147, + Automation1PsoWindowInput_XL5eSyncPortB = 148, + Automation1PsoWindowInput_XL5eDrivePulseStream = 149, + Automation1PsoWindowInput_XC2PrimaryFeedback = 150, + Automation1PsoWindowInput_XC2AuxiliaryFeedback = 151, + Automation1PsoWindowInput_XC2DrivePulseStream = 152, + Automation1PsoWindowInput_XC2ePrimaryFeedback = 153, + Automation1PsoWindowInput_XC2eAuxiliaryFeedback = 154, + Automation1PsoWindowInput_XC2eDrivePulseStream = 155, + Automation1PsoWindowInput_XL2ePrimaryFeedback = 156, + Automation1PsoWindowInput_XL2eAuxiliaryFeedback = 157, + Automation1PsoWindowInput_XL2eSyncPortA = 158, + Automation1PsoWindowInput_XL2eSyncPortB = 159, + Automation1PsoWindowInput_XL2eDrivePulseStream = 160, + Automation1PsoWindowInput_XI4PrimaryFeedbackAxis1 = 161, + Automation1PsoWindowInput_XI4PrimaryFeedbackAxis2 = 162, + Automation1PsoWindowInput_XI4PrimaryFeedbackAxis3 = 163, + Automation1PsoWindowInput_XI4PrimaryFeedbackAxis4 = 164, + Automation1PsoWindowInput_XI4AuxiliaryFeedback1 = 165, + Automation1PsoWindowInput_XI4AuxiliaryFeedback2 = 166, + Automation1PsoWindowInput_XI4AuxiliaryFeedback3 = 167, + Automation1PsoWindowInput_XI4AuxiliaryFeedback4 = 168, + Automation1PsoWindowInput_XI4SyncPortA = 169, + Automation1PsoWindowInput_XI4SyncPortB = 170, + Automation1PsoWindowInput_XI4DrivePulseStreamAxis1 = 171, + Automation1PsoWindowInput_XI4DrivePulseStreamAxis2 = 172, + Automation1PsoWindowInput_XI4DrivePulseStreamAxis3 = 173, + Automation1PsoWindowInput_XI4DrivePulseStreamAxis4 = 174, + Automation1PsoWindowInput_iXC4PrimaryFeedback = 175, + Automation1PsoWindowInput_iXC4AuxiliaryFeedback = 176, + Automation1PsoWindowInput_iXC4SyncPortA = 177, + Automation1PsoWindowInput_iXC4SyncPortB = 178, + Automation1PsoWindowInput_iXC4DrivePulseStream = 179, + Automation1PsoWindowInput_iXC4ePrimaryFeedback = 180, + Automation1PsoWindowInput_iXC4eAuxiliaryFeedback = 181, + Automation1PsoWindowInput_iXC4eSyncPortA = 182, + Automation1PsoWindowInput_iXC4eSyncPortB = 183, + Automation1PsoWindowInput_iXC4eDrivePulseStream = 184, + Automation1PsoWindowInput_iXC6ePrimaryFeedback = 185, + Automation1PsoWindowInput_iXC6eAuxiliaryFeedback = 186, + Automation1PsoWindowInput_iXC6eSyncPortA = 187, + Automation1PsoWindowInput_iXC6eSyncPortB = 188, + Automation1PsoWindowInput_iXC6eDrivePulseStream = 189, + Automation1PsoWindowInput_iXL5ePrimaryFeedback = 190, + Automation1PsoWindowInput_iXL5eAuxiliaryFeedback = 191, + Automation1PsoWindowInput_iXL5eSyncPortA = 192, + Automation1PsoWindowInput_iXL5eSyncPortB = 193, + Automation1PsoWindowInput_iXL5eDrivePulseStream = 194, + Automation1PsoWindowInput_iXR3PrimaryFeedbackAxis1 = 195, + Automation1PsoWindowInput_iXR3PrimaryFeedbackAxis2 = 196, + Automation1PsoWindowInput_iXR3PrimaryFeedbackAxis3 = 197, + Automation1PsoWindowInput_iXR3PrimaryFeedbackAxis4 = 198, + Automation1PsoWindowInput_iXR3PrimaryFeedbackAxis5 = 199, + Automation1PsoWindowInput_iXR3PrimaryFeedbackAxis6 = 200, + Automation1PsoWindowInput_iXR3AuxiliaryFeedbackAxis1 = 201, + Automation1PsoWindowInput_iXR3AuxiliaryFeedbackAxis2 = 202, + Automation1PsoWindowInput_iXR3AuxiliaryFeedbackAxis3 = 203, + Automation1PsoWindowInput_iXR3AuxiliaryFeedbackAxis4 = 204, + Automation1PsoWindowInput_iXR3AuxiliaryFeedbackAxis5 = 205, + Automation1PsoWindowInput_iXR3AuxiliaryFeedbackAxis6 = 206, + Automation1PsoWindowInput_iXR3SyncPortA = 207, + Automation1PsoWindowInput_iXR3SyncPortB = 208, + Automation1PsoWindowInput_iXR3DrivePulseStream = 209, + Automation1PsoWindowInput_GI4DrivePulseStreamAxis1 = 210, + Automation1PsoWindowInput_GI4DrivePulseStreamAxis2 = 211, + Automation1PsoWindowInput_GI4DrivePulseStreamAxis3 = 212, + Automation1PsoWindowInput_iXC2PrimaryFeedback = 213, + Automation1PsoWindowInput_iXC2AuxiliaryFeedback = 214, + Automation1PsoWindowInput_iXC2DrivePulseStream = 215, + Automation1PsoWindowInput_iXC2ePrimaryFeedback = 216, + Automation1PsoWindowInput_iXC2eAuxiliaryFeedback = 217, + Automation1PsoWindowInput_iXC2eDrivePulseStream = 218, + Automation1PsoWindowInput_iXL2ePrimaryFeedback = 219, + Automation1PsoWindowInput_iXL2eAuxiliaryFeedback = 220, + Automation1PsoWindowInput_iXL2eSyncPortA = 221, + Automation1PsoWindowInput_iXL2eSyncPortB = 222, + Automation1PsoWindowInput_iXL2eDrivePulseStream = 223, + Automation1PsoWindowInput_iXI4PrimaryFeedbackAxis1 = 224, + Automation1PsoWindowInput_iXI4PrimaryFeedbackAxis2 = 225, + Automation1PsoWindowInput_iXI4PrimaryFeedbackAxis3 = 226, + Automation1PsoWindowInput_iXI4PrimaryFeedbackAxis4 = 227, + Automation1PsoWindowInput_iXI4AuxiliaryFeedback1 = 228, + Automation1PsoWindowInput_iXI4AuxiliaryFeedback2 = 229, + Automation1PsoWindowInput_iXI4AuxiliaryFeedback3 = 230, + Automation1PsoWindowInput_iXI4AuxiliaryFeedback4 = 231, + Automation1PsoWindowInput_iXI4SyncPortA = 232, + Automation1PsoWindowInput_iXI4SyncPortB = 233, + Automation1PsoWindowInput_iXI4DrivePulseStreamAxis1 = 234, + Automation1PsoWindowInput_iXI4DrivePulseStreamAxis2 = 235, + Automation1PsoWindowInput_iXI4DrivePulseStreamAxis3 = 236, + Automation1PsoWindowInput_iXI4DrivePulseStreamAxis4 = 237, + +# @brief Specifies the PSO output pin settings for each drive. +class Automation1PsoOutputPin: + Automation1PsoOutputPin_GL4None = 100, + Automation1PsoOutputPin_GL4LaserOutput0 = 101, + Automation1PsoOutputPin_XL4sNone = 102, + Automation1PsoOutputPin_XL4sLaserOutput0 = 103, + Automation1PsoOutputPin_XR3None = 104, + Automation1PsoOutputPin_XR3PsoOutput1 = 105, + Automation1PsoOutputPin_XR3PsoOutput2 = 106, + Automation1PsoOutputPin_XR3PsoOutput3 = 107, + Automation1PsoOutputPin_XC4DedicatedOutput = 108, + Automation1PsoOutputPin_XC4AuxiliaryMarkerDifferential = 109, + Automation1PsoOutputPin_XC4AuxiliaryMarkerSingleEnded = 110, + XC4eDedicatedOutput = 111, + XC4eAuxiliaryMarkerDifferential = 112, + XC4eAuxiliaryMarkerSingleEnded = 113, + Automation1PsoOutputPin_XC6eDedicatedOutput = 114, + Automation1PsoOutputPin_XC6eAuxiliaryMarkerDifferential = 115, + Automation1PsoOutputPin_XC6eAuxiliaryMarkerSingleEnded = 116, + Automation1PsoOutputPin_XL5eDedicatedOutput = 117, + Automation1PsoOutputPin_XL5eAuxiliaryMarkerDifferential = 118, + Automation1PsoOutputPin_XL5eAuxiliaryMarkerSingleEnded = 119, + Automation1PsoOutputPin_XC2DedicatedOutput = 120, + Automation1PsoOutputPin_XC2eDedicatedOutput = 121, + Automation1PsoOutputPin_XL2eDedicatedOutput = 122, + Automation1PsoOutputPin_XI4DedicatedOutput = 123, + Automation1PsoOutputPin_iXC4DedicatedOutput = 124, + Automation1PsoOutputPin_iXC4AuxiliaryMarkerDifferential = 125, + Automation1PsoOutputPin_iXC4AuxiliaryMarkerSingleEnded = 126, + Automation1PsoOutputPin_iXC4eDedicatedOutput = 127, + Automation1PsoOutputPin_iXC4eAuxiliaryMarkerDifferential = 128, + Automation1PsoOutputPin_iXC4eAuxiliaryMarkerSingleEnded = 129, + Automation1PsoOutputPin_iXC6eDedicatedOutput = 130, + Automation1PsoOutputPin_iXC6eAuxiliaryMarkerDifferential = 131, + Automation1PsoOutputPin_iXC6eAuxiliaryMarkerSingleEnded = 132, + Automation1PsoOutputPin_iXL5eDedicatedOutput = 133, + Automation1PsoOutputPin_iXL5eAuxiliaryMarkerDifferential = 134, + Automation1PsoOutputPin_iXL5eAuxiliaryMarkerSingleEnded = 135, + Automation1PsoOutputPin_iXR3None = 136, + Automation1PsoOutputPin_iXR3PsoOutput1 = 137, + Automation1PsoOutputPin_iXR3PsoOutput2 = 138, + Automation1PsoOutputPin_iXR3PsoOutput3 = 139, + Automation1PsoOutputPin_GI4None = 140, + Automation1PsoOutputPin_GI4LaserOutput0 = 141, + Automation1PsoOutputPin_iXC2eDedicatedOutput = 143, + Automation1PsoOutputPin_iXL2eDedicatedOutput = 144, + Automation1PsoOutputPin_iXI4DedicatedOutput = 145, +""" + + +class DriveDataCaptureInput: + PositionCommand = 0 + PrimaryFeedback = 1 + AuxiliaryFeedback = 2 + AnalogInput0 = 3 + AnalogInput1 = 4 + AnalogInput2 = 5 + AnalogInput3 = 6 + + +class DriveDataCaptureTrigger: + PsoOutput = 0 + PsoEvent = 1 + HighSpeedInput0RisingEdge = 2 + HighSpeedInput0FallingEdge = 3 + HighSpeedInput1RisingEdge = 4 + HighSpeedInput1FallingEdge = 5 + AuxiliaryMarkerRisingEdge = 6 + AuxiliaryMarkerFallingEdge = 7 + + +class PsoOutputPin: + GL4None = (100,) + GL4LaserOutput0 = (101,) + XL4sNone = (102,) + XL4sLaserOutput0 = (103,) + XR3None = (104,) + XR3PsoOutput1 = (105,) + XR3PsoOutput2 = (106,) + XR3PsoOutput3 = (107,) + XC4DedicatedOutput = (108,) + XC4AuxiliaryMarkerDifferential = (109,) + XC4AuxiliaryMarkerSingleEnded = (110,) + XC4eDedicatedOutput = (111,) + XC4eAuxiliaryMarkerDifferential = (112,) + XC4eAuxiliaryMarkerSingleEnded = (113,) + XC6eDedicatedOutput = (114,) + XC6eAuxiliaryMarkerDifferential = (115,) + XC6eAuxiliaryMarkerSingleEnded = (116,) + XL5eDedicatedOutput = (117,) + XL5eAuxiliaryMarkerDifferential = (118,) + XL5eAuxiliaryMarkerSingleEnded = (119,) + XC2DedicatedOutput = (120,) + XC2eDedicatedOutput = (121,) + XL2eDedicatedOutput = (122,) + XI4DedicatedOutput = (123,) + iXC4DedicatedOutput = (124,) + iXC4AuxiliaryMarkerDifferential = (125,) + iXC4AuxiliaryMarkerSingleEnded = (126,) + iXC4eDedicatedOutput = (127,) + iXC4eAuxiliaryMarkerDifferential = (128,) + iXC4eAuxiliaryMarkerSingleEnded = (129,) + iXC6eDedicatedOutput = (130,) + iXC6eAuxiliaryMarkerDifferential = (131,) + iXC6eAuxiliaryMarkerSingleEnded = (132,) + iXL5eDedicatedOutput = (133,) + iXL5eAuxiliaryMarkerDifferential = (134,) + iXL5eAuxiliaryMarkerSingleEnded = (135,) + iXR3None = (136,) + iXR3PsoOutput1 = (137,) + iXR3PsoOutput2 = (138,) + iXR3PsoOutput3 = (139,) + GI4None = (140,) + GI4LaserOutput0 = (141,) + iXC2DedicatedOutput = (142,) + iXC2eDedicatedOutput = (143,) + iXL2eDedicatedOutput = (144,) + iXI4DedicatedOutput = (145,) diff --git a/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript b/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript new file mode 100644 index 0000000..94d4caa --- /dev/null +++ b/tomcat_bec/devices/aerotech/AerotechSimpleSequenceTemplate.ascript @@ -0,0 +1,140 @@ +// Test program for simple zig-zag line scanning with PSO window output +// "enable" signal and DDC synchronized to external trigger input. +// The file expects external parameter validation +// The PSO locations arrays are set externally from EPICS PV +// + +enum ScanType + Pos = 0 + Neg = 1 + PosNeg = 2 + NegPos = 3 +end + + +program + ////////////////////////////////////////////////////////////////////////// + // External parameters - USE THEESE + var $fStartPosition as real = {{ scan.startpos }} + var $fScanRange as real = $fStartPosition + {{ scan.scanrange }} + var $iNumRepeat as integer = {{ scan.nrepeat }} + var $eScanType as ScanType = ScanType.{{ scan.scandir or 'Pos' }} + var $iNumDdcRead as integer = {{ scan.npoints }} + + + var $fVelJog as real = {{ scan.jogvel or 200 }} + var $fVelScan as real = {{ scan.scanvel }} + var $fAcceleration = {{ scan.scanacc or 500 }} + var $fSafeDist = 10.0 + + ////////////////////////////////////////////////////////////////////////// + // Internal parameters - dont use + var $axis as axis = ROTY + var $ii as integer + var $iDdcSafeSpace as integer = 4096 + + // Set acceleration + SetupAxisRampType($axis, RampType.Linear) + SetupAxisRampValue($axis,0,$fAcceleration) + var $fAccDistance as real = 0.5 * $fVelScan * $fVelScan / $fAcceleration + $fSafeDist + + // set the actual scan range + var $fPosStart as real + var $fPosEnd as real + if $eScanType == ScanType.Pos + $fPosStart = $fStartPosition - $fAccDistance + $fPosEnd = $fStartPosition + $fScanRange + $fAccDistance + elseif $eScanType == ScanType.Neg + $fPosStart = $fStartPosition + $fAccDistance + $fPosEnd = $fStartPosition - $fScanRange - $fAccDistance + elseif $eScanType == ScanType.PosNeg + $fPosStart = $fStartPosition - $fAccDistance + $fPosEnd = $fStartPosition + $fScanRange + $fAccDistance + elseif $eScanType == ScanType.NegPos + $fPosStart = $fStartPosition + $fAccDistance + $fPosEnd = $fStartPosition - $fScanRange - $fAccDistance + end + + // Move to start position before the scan + MoveAbsolute($axis, $fPosStart, $fVelJog) + WaitForInPosition($axis) + + // Configure PSO + PsoDistanceCounterOff($axis) + PsoDistanceEventsOff($axis) + PsoWindowConfigureEvents($axis, PsoWindowEventMode.None) + PsoWaveformOff($axis) + PsoOutputConfigureSource($axis, PsoOutputSource.Waveform) + var $iPsoArrayAddr as integer = 0 + // Simple PSO trigger pattern + var $iPsoArrayPos[] as real = [{% for psoDist in scan.psoBoundsPos[:-1] %} UnitsToCounts($axis, {{ psoDist }}), {% endfor %} UnitsToCounts($axis, {{ scan.psoBoundsPos[-1] }})] + // var $iPsoArrayPos[] as real = [UnitsToCounts($axis, $fAccDistance), {% for psoDist in scan.psoBoundsPos[:-1] %} UnitsToCounts($axis, psoDist), {% endfor %}] + // var $iPsoArrayNeg[] as real = [UnitsToCounts($axis, $fAccDistance), {% for psoDist in scan.psoBoundsNeg[:-1] %} UnitsToCounts($axis, psoDist), {% endfor %}] + + DriveArrayWrite($axis, $iPsoArrayPos, $iPsoArrayAddr, length($iPsoArrayPos), DriveArrayType.PsoDistanceEventDistances) + PsoDistanceConfigureArrayDistances($axis, $iPsoArrayAddr, length($iPsoArrayPos), 0) + PsoDistanceEventsOn($axis) + + PsoWaveformConfigureMode($axis, PsoWaveformMode.Toggle) + PsoWaveformOn($axis) + + // Configure Drive Data Collection + var $iDdcArrayAddr as integer = 8388608 + var $iDdcArraySize as integer = $iNumDdcRead + + DriveDataCaptureConfigureInput($axis, 0, DriveDataCaptureInput.PrimaryFeedback); + DriveDataCaptureConfigureInput($axis, 1, DriveDataCaptureInput.AnalogInput0 ); + + DriveDataCaptureConfigureTrigger($axis, 0, DriveDataCaptureTrigger.PsoOutput ); + DriveDataCaptureConfigureTrigger($axis, 1, DriveDataCaptureTrigger.PsoOutput ); + + DriveDataCaptureConfigureArray($axis, 0, $iDdcArrayAddr, $iDdcArraySize); + DriveDataCaptureConfigureArray($axis, 1, $iDdcArrayAddr + $iDdcSafeSpace + 8 * $iDdcArraySize, $iDdcArraySize); + + // Directly before scan + PsoDistanceCounterOn($axis) + DriveDataCaptureOn($axis, 0) + DriveDataCaptureOn($axis, 1) + + /////////////////////////////////////////////////////////// + // Start the actual scanning + /////////////////////////////////////////////////////////// + for $ii = 0 to ($iNumRepeat-1) + PsoDistanceConfigureArrayDistances($axis, $iPsoArrayAddr, length($iPsoArrayPos), 0) + + if $eScanType == ScanType.Pos + MoveAbsolute($axis, $fPosEnd, $fVelScan) + WaitForInPosition($axis) + MoveAbsolute($axis, $fPosStart, $fVelScan) + WaitForInPosition($axis) + elseif $eScanType == ScanType.Neg + MoveAbsolute($axis, $fPosEnd, $fVelScan) + WaitForInPosition($axis) + MoveAbsolute($axis, $fPosStart, $fVelScan) + WaitForInPosition($axis) + elseif $eScanType == ScanType.PosNeg + if ($ii % 2) == 0 + MoveAbsolute($axis, $fPosEnd, $fVelScan) + WaitForInPosition($axis) + elseif ($ii % 2) == 1 + MoveAbsolute($axis, $fPosStart, $fVelScan) + WaitForInPosition($axis) + end + elseif $eScanType == ScanType.NegPos + if ($ii % 2) == 0 + MoveAbsolute($axis, $fPosEnd, $fVelScan) + WaitForInPosition($axis) + elseif ($ii % 2) == 1 + MoveAbsolute($axis, $fPosStart, $fVelScan) + WaitForInPosition($axis) + end + end + Dwell(0.2) + end + + // Directly after scan + PsoDistanceCounterOff($axis) + DriveDataCaptureOff($axis, 0) + DriveDataCaptureOff($axis, 1) +end + diff --git a/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript b/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript new file mode 100644 index 0000000..c7e01c8 --- /dev/null +++ b/tomcat_bec/devices/aerotech/AerotechSnapAndStepTemplate.ascript @@ -0,0 +1,87 @@ +// Snap-and-step +// Test program for high speed step scanning with individual triggers on PSO output. +// The file expects external parameter validation. + + + +program + // External parameters + var $fStartPosition as real = {{ scan.startpos }} + var $iNumSteps as integer = {{ scan.numsteps }} + var $fStepSize as real = {{ scan.stepsize }} + var $fExposureTimeSec as real = {{ scan.exptime }} + var $fVelJog as real = {{ scan.travel or 200 }} + var $fVelScan as real = {{ scan.velocity or 50 }} + var $fAcceleration = {{ scan.acceleration or 500 }} + + // Internal + var $axis as axis = ROTY + var $ii as integer + + // Set acceleration + SetupAxisRampType($axis, RampType.Linear) + SetupAxisRampValue($axis,0,$fAcceleration) + + // Move to start position before the scan + var $fPosNext as real = $fStartPosition + MoveAbsolute($axis, $fPosNext, $fVelJog) + WaitForInPosition($axis) + + // Configure PSO (to manual event generation) + PsoDistanceEventsOff($axis) + PsoWindowConfigureEvents($axis, PsoWindowEventMode.None) + PsoWaveformConfigurePulseFixedTotalTime($axis, 50) + PsoWaveformConfigurePulseFixedOnTime($axis, 20) + PsoWaveformConfigurePulseFixedCount($axis, 1) + PsoWaveformApplyPulseConfiguration($axis) + PsoWaveformConfigureMode($axis, PsoWaveformMode.Pulse) + PsoOutputConfigureSource($axis, PsoOutputSource.Waveform) + PsoWaveformOn($axis) + + // Configure Drive Data Collection + var $iDdcArrayAddr as integer = 8388608 + var $iDdcArraySize as integer = iMaximum(5000, $iNumSteps) + var $iDdcSafeSpace as integer = 4096 + + DriveDataCaptureConfigureInput($axis, 0, DriveDataCaptureInput.PrimaryFeedback); + DriveDataCaptureConfigureInput($axis, 1, DriveDataCaptureInput.AnalogInput0 ); + + DriveDataCaptureConfigureTrigger($axis, 0, DriveDataCaptureTrigger.PsoOutput ); + DriveDataCaptureConfigureTrigger($axis, 1, DriveDataCaptureTrigger.PsoOutput ); + + DriveDataCaptureConfigureArray($axis, 0, $iDdcArrayAddr, $iDdcArraySize); + DriveDataCaptureConfigureArray($axis, 1, $iDdcArrayAddr + $iDdcSafeSpace + 8 * $iDdcArraySize, $iDdcArraySize); + + // Directly before scan + PsoDistanceCounterOn($axis) + DriveDataCaptureOn($axis, 0) + DriveDataCaptureOn($axis, 1) + + /////////////////////////////////////////////////////////// + // Start the actual scanning + /////////////////////////////////////////////////////////// + for $ii = 0 to ($iNumSteps-1) + MoveAbsolute($axis, $fPosNext, $fVelScan) + WaitForInPosition($axis) + PsoEventGenerateSingle($axis) + Dwell($fExposureTimeSec) + $fPosNext = $fPosNext + $fStepSize + end + + // Directly after scan + PsoWaveformOff($axis) + DriveDataCaptureOff($axis, 0) + DriveDataCaptureOff($axis, 1) +end + +// Demonstrates using a switch/case conditional. +function iMaximum($A as integer, $B as integer) as integer + var $retVal + if ($A > $B) + $retVal = $A + else + $retVal = $B + end + return $retVal +end + diff --git a/tomcat_bec/devices/aerotech/__init__.py b/tomcat_bec/devices/aerotech/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tomcat_bec/devices/grashopper_tomcat.py b/tomcat_bec/devices/grashopper_tomcat.py new file mode 100644 index 0000000..47a932f --- /dev/null +++ b/tomcat_bec/devices/grashopper_tomcat.py @@ -0,0 +1,432 @@ +import enum +import threading +import time as ttime + +from bec_lib.logger import bec_logger + +# from typing import Any +from ophyd import ADComponent as ADCpt +from ophyd import Component as Cpt +from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV + +# import numpy as np +from ophyd.ophydobj import Kind +from ophyd_devices.epics.devices.psi_detector_base import CustomDetectorMixin, PSIDetectorBase + +# os.environ["EPICS_CA_AUTO_ADDR_LIST"] = "No" +# os.environ["EPICS_CA_ADDR_LIST"] = "129.129.208.143" + + +logger = bec_logger.logger + + +class GrashopperError(Exception): + """Base class for Grashopper detector errors.""" + + +class GrashopperTimeoutError(GrashopperError): + """Base class for Grashopper detector errors.""" + + +class AutoMode(enum.IntEnum): + """Acquire time auto for Grashopper detector. + + class for acquire_auto and gain_auto + + Off: Gain tap balancing is user controlled using Gain. + Once: Gain tap balancing is automatically adjusted once by the device. + Once it has converged, it automatically returns to the Off state. + Continuous: Gain tap balancing is constantly adjusted by the device. + """ + + OFF = 0 + ONCE = 1 + CONTINUOUS = 2 + + +class ImageMode(enum.IntEnum): + """Image mode for Grashopper detector. + + Single: Acquire a single image, ignores NumImages PV + Multiple: Acquire NumImages images + Continuous: Acquire images continuously + """ + + SINGLE = 0 + MULTIPLE = 1 + CONTINUOUS = 2 + + +class DetectorState(enum.IntEnum): + """Detector states for Grashopper detector""" + + IDLE = 0 + ACQUIRE = 1 + READOUT = 2 + CORRECT = 3 + SAVING = 4 + ABORTING = 5 + ERROR = 6 + WAITING = 7 + INITIALIZING = 8 + DISCONNECTED = 9 + ABORTED = 10 + + +class ImageBinning(enum.IntEnum): + """Image binning for Grashopper detector""" + + X1 = 1 + X2 = 2 + X4 = 4 + + +class VideoMode(enum.IntEnum): + """Video mode for Grashopper detector. + + For details, consult EPICs IOC manual. + """ + + MODE0 = 0 + MODE1 = 1 + MODE2 = 2 + MODE3 = 3 + + +class PixelFormat(enum.IntEnum): + """Pixel format for Grashopper detector.""" + + MONO8 = 0 + MONO12PACKED = 1 + MONO12P = 2 + MONO16 = 3 + + +class COLORMODE(enum.IntEnum): + """Color mode for Grashopper detector. + + Only for readback values from color_mode RO PV. + """ + + MONO = 0 + BAYER = 1 + RGB1 = 2 + RGB2 = 3 + RGB3 = 4 + YUV444 = 5 + YUV422 = 6 + YUV421 = 7 + + +class TriggerSource(enum.IntEnum): + """Trigger signals for Grashopper detector""" + + SOFTWARE = 0 + LINE0 = 1 + LINE2 = 2 + LINE3 = 3 + + +class MemoryPolling(enum.IntEnum): + """Memory polling for Grashopper detector. + + Defines update rate of memory polling for IOC (1s suggested). + """ + + PASSIVE = 0 + EVENT = 1 + IOINTR = 2 + SECONDS10 = 3 + SECONDS5 = 4 + SECONDS2 = 5 + SECONDS1 = 6 + SECONDS05 = 7 + SECONDS02 = 8 + SECONDS01 = 9 + + +class GrashopperTOMCATSetup(CustomDetectorMixin): + """Mixin class to setup TOMCAT specific implementations of the detector. + + This class will be called by the custom_prepare_cls attribute of the detector class. + """ + + def __init__(self, *_args, parent: Device = None, **_kwargs) -> None: + super().__init__(*_args, parent=parent, **_kwargs) + + self.image_shape = (self.parent.cam.image_size_y.get(), self.parent.cam.image_size_x.get()) + self.monitor_thread = None + self.stop_monitor = False + self.update_frequency = 1 + self.low_frame_rate = 80 + + def initialize_detector(self) -> None: + """Initialize detector.""" + self.parent.cam.acquire.put(0) + self.parent.cam.acquire_time_auto.put(AutoMode.CONTINUOUS) + self.parent.cam.gain_auto.put(AutoMode.CONTINUOUS) + self.parent.cam.image_mode.put(ImageMode.MULTIPLE) + self.parent.cam.image_binning.put(ImageBinning.X1) + self.parent.cam.video_mode.put(VideoMode.MODE0) + self.parent.cam.pixel_format.put(PixelFormat.MONO16) + self.parent.cam.trigger_source.put(TriggerSource.SOFTWARE) + self.parent.cam.memory_polling.put(MemoryPolling.SECONDS1) + self.parent.cam.set_image_counter.put(0) + + def initialize_detector_backend(self) -> None: + self.parent.image.queue_size.put(5) + self.parent.image.array_port.put(self.parent.cam.port_name.get()) + self.parent.image.enable_cb.put(1) + self.parent.image.set_array_counter.put(0) + + def set_exposure_time(self, exposure_time: float) -> None: + """Set the detector framerate. + + Args: + framerate (float): Desired framerate in Hz smallest is 87Hz + """ + framerate = 1 / exposure_time + if framerate > self.low_frame_rate: + raise GrashopperError( + f"Trying to set exposure time to {exposure_time}s, this is below the lowest" + f" possible exposure of {1/self.low_frame_rate}s" + ) + self.parent.cam.frame_rate.put(framerate) + + def prepare_detector(self) -> None: + """Prepare detector for acquisition.""" + self.parent.cam.image_mode.put(ImageMode.MULTIPLE) + self.parent.cam.acquire_time_auto.put(AutoMode.CONTINUOUS) + self.set_exposure_time(self.parent.scaninfo.exp_time) + self.parent.set_trigger(TriggerSource.SOFTWARE) + self.parent.cam.set_image_counter.put(0) + self.set_acquisition_params() + + def set_acquisition_params(self) -> None: + """Set acquisition parameters for the detector""" + + # Set number of images and frames (frames is for internal burst of detector) + self.parent.cam.num_images.put( + int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger) + ) + + def prepare_detector_backend(self) -> None: + """Prepare detector backend for acquisition.""" + self.parent.image.set_array_counter.put(0) + self.monitor_thread = None + self.stop_monitor = False + # self.run_monitor() + + def arm_acquisition(self) -> None: + """Arm grashopper detector for acquisition""" + self.parent.cam.acquire.put(1) + signal_conditions = [(self.parent.cam.detector_state.get, DetectorState.WAITING)] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout, + check_stopped=True, + all_signals=False, + ): + raise GrashopperTimeoutError( + f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}" + ) + + def on_trigger(self) -> None: + """Trigger the detector""" + if self.parent.cam.trigger_source.get() == TriggerSource.SOFTWARE: + self.parent.cam.software_trigger_device.put(1) + ttime.sleep(0.1) + self.send_data() + + def run_monitor(self) -> None: + """ + Run the monitor loop in a separate thread. + """ + self.monitor_thread = threading.Thread(target=self.monitor_loop, daemon=True) + self.monitor_thread.start() + + def monitor_loop(self) -> None: + """ + Monitor the detector status and send data. + """ + while True: + self.send_data() + ttime.sleep(1 / self.update_frequency) + if self.parent.stopped: + break + + def send_data(self) -> None: + """Send data to monitor endpoint in redis.""" + try: + img = self.parent.image.array_data.get().reshape(self.image_shape) + # pylint: disable=protected-access + self.parent._run_subs(sub_type=self.parent.SUB_VALUE, value=img) + except Exception as e: + logger.debug(f"{e} for image with shape {self.parent.image.array_data.get().shape}") + + def stop_detector(self) -> None: + """Stop detector.""" + self.parent.cam.acquire.put(0) + signal_conditions = [(self.parent.cam.detector_state.get, DetectorState.IDLE)] + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout - self.parent.timeout // 2, + check_stopped=True, + all_signals=False, + ): + # Retry stop detector and wait for remaining time + self.parent.cam.acquire.put(0) + if not self.wait_for_signals( + signal_conditions=signal_conditions, + timeout=self.parent.timeout - self.parent.timeout // 2, + check_stopped=True, + all_signals=False, + ): + raise GrashopperTimeoutError( + f"Failed to stop detector, detector state {signal_conditions[0][0]}" + ) + + def stop_detector_backend(self) -> None: + """Stop the data backend sending data.""" + self.stop_monitor = True + + +class SLSDetectorCam(Device): + """ + SLS Detector Camera - Grashoppter + + Base class to map EPICS PVs to ophyd signals. + """ + + ## Deprecated PVs, to be checked! + # acquire_time = ADCpt(EpicsSignal, "AcquireTime", kind=Kind.omitted) + # num_exposures = ADCpt(EpicsSignal, "NumExposures", kind=Kind.omitted) + # acquire_period = ADCpt(EpicsSignalWithRBV, "AcquirePeriod", kind=Kind.config) + + # Control PVs + acquire_time_auto = ADCpt(EpicsSignal, "AcquireTimeAuto", kind=Kind.config) + + frame_rate = ADCpt(EpicsSignalWithRBV, "FrameRate", kind=Kind.normal) + num_images = ADCpt(EpicsSignalWithRBV, "NumImages", kind=Kind.normal) + num_images_counter = ADCpt(EpicsSignalRO, "NumImagesCounter_RBV", kind=Kind.normal) + image_mode = ADCpt(EpicsSignalWithRBV, "ImageMode", kind=Kind.config) + acquire = ADCpt(EpicsSignalWithRBV, "Acquire", kind=Kind.config) + detector_state = ADCpt(EpicsSignalRO, "DetectorState_RBV", kind=Kind.normal) + status_message = ADCpt(EpicsSignalRO, "StatusMessage_RBV", string=True, kind=Kind.config) + + set_image_counter = ADCpt(EpicsSignal, "ArrayCounter", kind=Kind.config) + image_counter = ADCpt(EpicsSignal, "ArrayCounter_RBV", kind=Kind.normal) + image_rate = ADCpt(EpicsSignalRO, "ArrayRate_RBV", kind=Kind.omitted) + + # Asyn Port name + port_name = ADCpt(EpicsSignalRO, "PortName_RBV", string=True, kind=Kind.omitted) + + # Readout related PVs + max_image_size_x = ADCpt(EpicsSignalRO, "MaxSizeX_RBV", kind=Kind.config) + max_image_size_y = ADCpt(EpicsSignalRO, "MaxSizeY_RBV", kind=Kind.config) + image_size_x = ADCpt(EpicsSignalRO, "ArraySizeX_RBV", kind=Kind.config) + image_size_y = ADCpt(EpicsSignalRO, "ArraySizeY_RBV", kind=Kind.config) + # Only BinY PV is working, sets both + image_binning = ADCpt(EpicsSignalWithRBV, "BinY", kind=Kind.config) + + gain = ADCpt(EpicsSignalWithRBV, "Gain", kind=Kind.config) + gain_auto = ADCpt(EpicsSignalWithRBV, "GainAuto", kind=Kind.config) + video_mode = ADCpt(EpicsSignalWithRBV, "VideoMode", kind=Kind.config) + pixel_format = ADCpt(EpicsSignalWithRBV, "PixelFormat", kind=Kind.config) + # Desired to set this in future? + color_mode = ADCpt(EpicsSignalRO, "ColorMode_RBV", kind=Kind.config) + + # HW Status PVs + temperature_actual = ADCpt(EpicsSignal, "TemperatureActual", kind=Kind.omitted) + + # Trigger + trigger_mode_active = ADCpt(EpicsSignalWithRBV, "TriggerMode", kind=Kind.config) + trigger_source = ADCpt(EpicsSignalWithRBV, "TriggerSource", kind=Kind.config) + trigger_delay = ADCpt(EpicsSignalWithRBV, "TriggerDelay", kind=Kind.omitted) + exposure_mode = ADCpt(EpicsSignalWithRBV, "ExposureMode", kind=Kind.omitted) + software_trigger_device = ADCpt(EpicsSignal, "SoftwareTrigger", kind=Kind.config) + + # buffer + memory_polling = ADCpt(EpicsSignal, "PoolUsedMem.SCAN", kind=Kind.omitted) + + +class SLSImagePlugin(Device): + """SLS Image Plugin + + Image plugin for SLS detector imitating the behaviour of ImagePlugin from + ophyd's areadetector plugins. + """ + + # Control + array_port = Cpt(EpicsSignal, "NDArrayPort", kind=Kind.omitted, string=True) + enable_cb = Cpt(EpicsSignal, "EnableCallbacks", kind=Kind.config) + queue_size = Cpt(EpicsSignal, "QueueSize", kind=Kind.config) + set_array_counter = Cpt(EpicsSignal, "ArrayCounter", kind=Kind.config) + array_counter = Cpt(EpicsSignal, "ArrayCounter_RBV", kind=Kind.normal) + set_dropped_arrays = Cpt(EpicsSignal, "DroppedArrays", kind=Kind.config) + dropped_arrays = Cpt(EpicsSignal, "DroppedArrays_RBV", kind=Kind.normal) + image_id = Cpt(EpicsSignal, "UniqueId_RBV", kind=Kind.normal) + + # Data + array_data = Cpt(EpicsSignal, "ArrayData", kind=Kind.omitted) + + # Size related PVs from Plugin + array_size_0 = Cpt(EpicsSignalRO, "ArraySize0_RBV", kind=Kind.omitted) + array_size_1 = Cpt(EpicsSignalRO, "ArraySize1_RBV", kind=Kind.omitted) + array_size_2 = Cpt(EpicsSignalRO, "ArraySize2_RBV", kind=Kind.omitted) + array_dimension_size = Cpt(EpicsSignalRO, "NDimensions_RBV", kind=Kind.omitted) + + +class GrashopperTOMCAT(PSIDetectorBase): + """ + Grashopper detector for TOMCAT + + Parent class: PSIDetectorBase + + class attributes: + custom_prepare_cls (GrashopperTOMCATSetup) : Custom detector setup class for TOMCAT, + inherits from CustomDetectorMixin + cam (SLSDetectorCam) : Detector camera + image (SLSImagePlugin) : Image plugin for detector + """ + + # Specify which functions are revealed to the user in BEC client + USER_ACCESS = ["describe"] + + SUB_MONITOR = "monitor" + SUB_VALUE = "value" + _default_sub = SUB_VALUE + + # specify Setup class + custom_prepare_cls = GrashopperTOMCATSetup + # specify minimum readout time for detector + MIN_READOUT = 0 + # specify class attributes + cam = ADCpt(SLSDetectorCam, "cam1:") + image = ADCpt(SLSImagePlugin, "image1:") + + def stage(self) -> list[object]: + rtr = super().stage() + self.custom_prepare.arm_acquisition() + return rtr + + def unstage(self) -> list[object]: + rtr = super().unstage() + self.custom_prepare.stop_monitor = True + return rtr + + def set_trigger(self, trigger_source: TriggerSource) -> None: + """Set trigger source for the detector. + Check the TriggerSource enum for possible values + + Args: + trigger_source (TriggerSource): Trigger source for the detector + + """ + value = trigger_source + self.cam.trigger_source.put(value) + + +if __name__ == "__main__": + hopper = GrashopperTOMCAT(name="hopper", prefix="X02DA-PG-USB:", sim_mode=True) + hopper.wait_for_connection(all_signals=True) diff --git a/tomcat_bec/devices/tomcat_rotation_motors.py b/tomcat_bec/devices/tomcat_rotation_motors.py new file mode 100644 index 0000000..334dbe6 --- /dev/null +++ b/tomcat_bec/devices/tomcat_rotation_motors.py @@ -0,0 +1,182 @@ +""" Module for Tomcat rotation motors. + +The following classes implement the rotation motors for: + +- AerotechAutomation1 (Tomcat), based on EpicsMotorIOC. + +""" + +import threading +import time + +import numpy as np +from bec_lib import threadlocked +from ophyd import DeviceStatus +from ophyd_devices.interfaces.base_classes.ophyd_rotation_base import EpicsRotationBase +from ophyd_devices.interfaces.protocols.bec_protocols import BECFlyerProtocol, BECScanProtocol + + +class TomcatAerotechRotation(EpicsRotationBase, BECFlyerProtocol, BECScanProtocol): + """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, + ): + """Implementation of the Tomcat AerotechAutomation 1 rotation motor class. + + This motor class is based on EpicsRotationBase and provides in addition the flyer interface for BEC + and a progress update. + """ + super().__init__( + prefix=prefix, + name=name, + kind=kind, + read_attrs=read_attrs, + configuration_attrs=configuration_attrs, + parent=parent, + **kwargs, + ) + self._start_position = None + self._target_position = None + self._stopped = False + self._rlock = threading.RLock() + self.subscribe(self._progress_update, run=False) + + # ------------------ alternative to using configure method --------------------- # + @property + def start_position(self) -> float: + """Get the start position.""" + return self._start_position + + @start_position.setter + def start_position(self, value: float) -> None: + """Set the start position.""" + self._start_position = value + + @property + def target_position(self) -> float: + """Get the start position.""" + return self._target_position + + @target_position.setter + def target_position(self, value: float) -> None: + """Set the start position.""" + self._target_position = value + + # ------------------ alternative to using configure method --------------------- # + # configure method is optional for flyers within BEC, you can use stage method or pre_scan method to + # set relevant parameters on the device. + + # def configure(self, d: dict) -> dict: + # """Configure method from the device. + + # This method is usually used to set configuration parameters for the device. + + # Args: + # d (dict): Dictionary with configuration parameters. + + # """ + # if "target" in d: + # self._target_position = d["target"] + # del d["target"] + # if "position" in d: + # self._target_position = d["position"] + # del d["position"] + # return super().configure(d) + + def pre_scan(self): + """Perform pre-scan operation, e.g. move to start position.""" + if self._start_position: + self.move(self._start_position, wait=True) + + def kickoff(self) -> DeviceStatus: + """Kickoff the scan. + + The kickoff method should return a status object that is set to finish once the flyer flys, and is ready for the next actions. + I would consider the following implementation. + """ + self._start_position = float(self.position) + self.move(self._target_position, wait=False) + status = DeviceStatus(self) + status.set_finished() + return status + + def complete(self) -> DeviceStatus: + """Complete method of the scan. + + This will be called in a fly scan after the kickoff, thus, the stage will be moving to it's target position. + It should + + The complete method should return a status object that is set to finish once the flyer is done and the scan is complete. + I would consider the following implementation. + """ + threading.Thread(target=self._is_motor_moving, daemon=True).start() + status = DeviceStatus(self) + self.subscribe(status.set_finished, event_type=self.SUB_DONE, run=False) + return status + + def stage(self) -> list[object]: + """Stage the scan. + + We add here in addition the setting of the _stopped flag to False for the thread. + """ + self._stopped = False + return super().stage() + + def stop(self, success: bool = False) -> None: + """Stop the scan. + + If the device is stopped, the _stopped flag is set to True. + """ + self._stopped = True + super().stop(success=success) + + @threadlocked + def _is_motor_moving(self): + """Function to check if the motor is moving. + + This function is used in a thread to check if the motor is moving. + It resolves by running""" + while self.motor_done_move.get(): + if self._stopped: + self._done_moving(success=False) + return + time.sleep(0.1) + self._done_moving(success=True) + + # TODO This logic could be refined to be more robust for various scan types, i.e. at the moment it just takes + # the start and target position and calculates the progress based on the current position. + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan. + + Runs the progress update on the device progress during the scan. + It uses the SUB_PROGRESS event from ophyd to update BEC about the progress. + Scans need to be aware which device progress is relevant for the scan. + + Args: + value (float): The value of the motor position. + """ + if (self._start_position is None) or (self._target_position is None) or (not self.moving): + self._run_subs(sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1) + return + + progress = np.abs( + (value - self._start_position) / (self._target_position - self._start_position) + ) + max_value = 100 + self._run_subs( + sub_type=self.SUB_PROGRESS, + value=int(100 * progress), + max_value=max_value, + done=int(np.isclose(max_value, progress, 1e-3)), + )