From 26ee4e2d9bd9cb37eebefe9102ca78aa0fd55b33 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 13 Feb 2024 13:02:15 +0100 Subject: [PATCH 01/16] feat: Moving the Automation1 device to BEC repo --- .../epics/db/office_teststand_bec_config.yaml | 28 + .../epics/devices/AerotechAutomation1.py | 1110 +++++++++++++++++ .../epics/devices/AerotechAutomation1Enums.py | 914 ++++++++++++++ ophyd_devices/epics/devices/__init__.py | 1 + 4 files changed, 2053 insertions(+) create mode 100644 ophyd_devices/epics/db/office_teststand_bec_config.yaml create mode 100644 ophyd_devices/epics/devices/AerotechAutomation1.py create mode 100644 ophyd_devices/epics/devices/AerotechAutomation1Enums.py diff --git a/ophyd_devices/epics/db/office_teststand_bec_config.yaml b/ophyd_devices/epics/db/office_teststand_bec_config.yaml new file mode 100644 index 0000000..92be2bb --- /dev/null +++ b/ophyd_devices/epics/db/office_teststand_bec_config.yaml @@ -0,0 +1,28 @@ +es1_roty: + description: 'Test rotation stage' + deviceClass: EpicsMotor + deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY'} + onFailure: buffer + enabled: true + readoutPriority: monitored + +es1_aa1: + description: 'Rotation stage controller status' + deviceClass: epics:devices:aa1Controller + deviceConfig: {prefix: 'X02DA-ES1-SMP1:CTRL:'} + onFailure: buffer + enabled: true + readoutPriority: monitored + + +es1_aa1Tasks: + description: 'AA1 task management' + deviceClass: epics:devices:aa1Tasks + deviceConfig: {prefix: 'X02DA-ES1-SMP1:TASK:'} + onFailure: buffer + enabled: true + readoutPriority: monitored + + + + diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py new file mode 100644 index 0000000..3cd4c57 --- /dev/null +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -0,0 +1,1110 @@ +from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal +from ophyd.status import Status, SubscriptionStatus, StatusBase +from time import sleep +import warnings +import numpy as np +import time + +from .AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, + AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, + TaskDataSignal, SystemDataSignal, TomcatSequencerState) + +from .AerotechAutomation1Enums import * +from typing import Union +from collections import OrderedDict + + +def epicsCharArray2String(raw: bytes) -> str: + """ Ophyd might convert strings to uint16 vector...""" + if isinstance(raw, np.ndarray): + raw = raw[:-1] if raw[-1]==0 else raw + sReply = ''.join(str(s, encoding='ASCII') for s in raw.astype(np.uint8)) + print(f"Raw reply: {raw}\nConverted reply: {sReply}") + return sReply + else: + return raw + + + + +class EpicsSignalPassive(Device): + value = Component(EpicsSignalRO, "", kind=Kind.hinted) + proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True) + def get(self): + self.proc.set(1).wait() + return self.value.get() + + +class EpicsStringPassiveRO(EpicsSignalRO): + """Special helper class to work around a bug in ophyd (caproto backend) + that reads CHAR array strigs as uint16 arrays. + """ + + 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() + raw = super().get(*args, **kwargs) + return epicsCharArray2String(raw) + + @property + def value(self): + raw = super().value + return epicsCharArray2String(raw) + + +class EpicsStringPassive(EpicsSignal): + """Special helper class to work around a bug in ophyd (caproto backend) + that reads CHAR array strigs as uint16 arrays. + """ + def __init__(self, read_pv, write_pv=None, *, put_complete=False, + string=False, limits=False, name=None, **kwargs): + super().__init__(read_pv, write_pv, put_complete, string, limits, 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() + raw = super().get(*args, **kwargs) + return epicsCharArray2String(raw) + + @property + def value(self): + raw = super().value + return epicsCharArray2String(raw) + + +class EpicsString(EpicsSignal): + """Special helper class to work around a bug in ophyd (caproto backend) + that reads CHAR array strigs as uint16 arrays. + """ + def get(self, *args, **kwargs): + raw = super().get(*args, **kwargs) + return epicsCharArray2String(raw) + + @property + def value(self): + raw = super().value + return epicsCharArray2String(raw) + + +class EpicsStringRO(EpicsSignalRO): + """Special helper class to work around a bug in ophyd (caproto backend) + that reads CHAR array strigs as uint16 arrays. + """ + def get(self, *args, **kwargs): + raw = super().get(*args, **kwargs) + return epicsCharArray2String(raw) + + @property + def value(self): + raw = super().value + return epicsCharArray2String(raw) + + +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, "RUNNING", auto_monitor=True, kind=Kind.hinted) + slowpoll = Component(EpicsSignalRO, "RUNNING", 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. + + Execute 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. + """ + _failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) + errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) + warnStatus = Component(EpicsSignalRO, "WARNW", 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", kind=Kind.config, put_complete=True) + _executeMode = Component(EpicsSignal, "EXECUTE-MODE", kind=Kind.config, put_complete=True) + _executeReply = Component(EpicsSignalRO, "EXECUTE_RBV", auto_monitor=True) + + fileName = Component(EpicsSignal, "FILENAME", kind=Kind.config, put_complete=True) + _fileList = Component(EpicsSignalPassive, "FILELIST", kind=Kind.config) + _fileRead = Component(EpicsSignalPassive, "FILEREAD", kind=Kind.config) + _fileWrite = Component(EpicsSignal, "FILEWRITE", kind=Kind.config, 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._currentTaskMonitor = None + self._textToExecute = None + + def listFiles(self) -> list: + """ List all available files on the controller """ + # Have to use CHAR array due to EPICS LSI bug... + namesbytes = self._fileList.get().astype(np.uint8).tobytes() + nameslist = namesbytes.decode('ascii').split('\t') + return nameslist + + 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().astype(np.uint8).tobytes() + # C-strings terminate with trailing zero + if filebytes[-1]==0: + filebytes = filebytes[:-1] + filetext = filebytes.decode('ascii') + 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) + self.trigger(settle_time=settle_time).wait() + + 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.trigger(settle_time=settle_time).wait() + + if mode in [0, "None", None]: + return None + else: + raw = self._executeReply.get() + return epicsCharArray2String(raw) + + def configure(self, text: str=None, filename: str=None, taskIndex: int=4, settle_time=None, **kwargs): + """ Interface for configuration """ + # Validation + if taskIndex < 0 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") + if 'mode' in kwargs: + if kwargs['mode'] not in [0, 1, 2, 3, 4, "None", "Axis", "Int", "Double", "String", None]: + raise RuntimeError(f"Unknown execution mode: {kwargs['mode']}") + if kwargs['mode'] is None: + kwargs['mode'] = "None" + else: + kwargs['mode'] = "None" + + # common operations + self.taskIndex.set(taskIndex).wait() + self._textToExecute = None + #self._currentTaskMonitor = aa1TaskState() + + # Choose the right execution mode + if (filename is None) and (text not in [None, ""]): + # Direct command execution + print("Preparing for direct command execution") + self._executeMode.set(kwargs['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") + + ########################################################################## + # 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) -> StatusBase: + """ Execute the script on the configured task""" + if self._textToExecute is not None: + status = self._execute.set(self._textToExecute, settle_time=settle_time) + else: + status = self.switch.set("Run", settle_time=settle_time) + return status + + def stop(self): + """ Stop the currently selected task """ + self.switch.set("Stop").wait() + + ########################################################################## + # Flyer interface + def kickoff(self) -> StatusBase: + """ Execute the script on the configured task""" + if self._textToExecute is not None: + status = self._execute.set(self._textToExecute, settle_time=0.5) + else: + status = self.switch.set("Run", settle_time=0.1) + return status + + def complete(self) -> StatusBase: + """ Execute the script on the configured task""" + #return self._currentTaskMonitor.complete() + status = StatusBase() + 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 {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""" + # 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]) + 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) + return status + + def kickoff(self) -> StatusBase: + status = StatusBase() + 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 + + 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(EpicsSignalPassive, "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 + """ + # 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(EpicsSignalPassive, "INT-RBV", kind=Kind.omitted) + integerarr = Component(EpicsSignal, "INTARR", kind=Kind.omitted, put_complete=True) + integerarr_rb = Component(EpicsSignalPassive, "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(EpicsSignalPassive, "REAL-RBV", kind=Kind.omitted) + realarr = Component(EpicsSignal, "REALARR", kind=Kind.omitted, put_complete=True) + realarr_rb = Component(EpicsSignalPassive, "REALARR-RBV", kind=Kind.omitted) + + string_addr = Component(EpicsSignal, "STRING-ADDR", kind=Kind.omitted, put_complete=True) + string = Component(EpicsSignal, "STRING", kind=Kind.omitted, put_complete=True) + string_rb = Component(EpicsStringPassiveRO, "STRING-RBV", 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): + """ 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 + """ + 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) + + _str0 = Component(EpicsStringRO, "STR0_RBV", auto_monitor=True, name="str0_raw", kind=Kind.omitted) + str0 = Component(DerivedSignal, "_str0", inverse=epicsCharArray2String, auto_monitor=True, name="str0", kind=Kind.hinted) + + str1 = Component(EpicsStringRO, "STR1_RBV", auto_monitor=True, name="str1", kind=Kind.hinted) + str4 = Component(EpicsString, "STR4_RBV", put_complete=True, auto_monitor=True, write_pv="STR4", name="str4", kind=Kind.hinted) + str5 = Component(EpicsString, "STR5_RBV", put_complete=True, auto_monitor=True, write_pv="STR5", name="str4", kind=Kind.hinted) + + + +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 IO pin is polled simultaneously! + """ + polllvl = Component(EpicsSignal, "POLLLVL", put_complete=True, kind=Kind.config) + + 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) + 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 --> 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. + """ + + def __init__(self, *args, **kwargs): + """ Member declarations in init""" + self._Vdistance = 3.141592 + super().__init__(*args, **kwargs) + + # ######################################################################## + # PSO high level interface + def configure(self, distance: Union[float, np.ndarray, list, tuple], wmode: str, + t_pulse: float=2000, w_pulse: float=5000, n_pulse: int=1, + posInput: int=None, pinOutput: int=None, **argv) -> None: + """ 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. + """ + # Validate input parameters + if wmode not in ["pulse", "pulsed", "toggle", "toggled"]: + raise RuntimeError(f"Unsupported distace triggering mode: {wmode}") + + + # Set the position data source and output pin + if posInput is not None: + self.posInput.set(posInput).wait() + if pinOutput is not None: + self.pinOutput.set(pinOutput).wait() + + # 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 + 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() + 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() + + # ######################################################################## + # Bluesky step scan interface + def stage(self, settle_time=None): + self.dstEventsEna.set("On").wait() + if isinstance(self._distanceValue, (np.ndarray, list, tuple)): + self.dstArrayRearm.set(1).wait() + self.dstCounterEna.set("On").wait() + if settle_time is not None: + sleep(settle_time) + return super().stage() + + 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) -> StatusBase: + # 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 = StatusBase() + status.set_finished() + return status + + def complete(self) -> StatusBase: + """ Bluesky flyer interface""" + # Array mode waits until the buffer is empty + if 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) + else: + # In distance trigger mode there's no specific goal + status = StatusBase() + 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, *args, **kwargs): + """ Member declarations in init""" + self._mode = "output" + self._eventMode = "Enter" + super().__init__(*args, **kwargs) + + # ######################################################################## + # PSO high level interface + def configure(self, bounds: Union[np.ndarray, list, tuple], wmode: str, emode: str="Enter", + t_pulse: float=2000, w_pulse: float=5000, n_pulse: int=1, + posInput: int=None, pinOutput: int=None, **argv) -> None: + """ 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. + """ + # 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 + + # Set the position data source and output pin + if posInput is not None: + self.posInput.set(posInput).wait() + if pinOutput is not None: + self.outPin.set(pinOutput).wait() + + # 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() + + 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). + """ + + # ######################################################################## + # General module status + 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) + + def configure(self, npoints, + trigger: int=DriveDataCaptureTrigger.PsoOutput, + source0: int=DriveDataCaptureInput.PrimaryFeedback, + source1: int=DriveDataCaptureInput.PositionCommand): + self._input0.set(source0).wait() + self._input1.set(source1).wait() + self._trigger.set(trigger).wait() + # This allocates the memory... + self.npoints.set(npoints).wait() + + + # Bluesky step scanning interface + def stage(self, settle_time=0.1): + super().stage() + self._switch.set("Start", settle_time=0.5).wait() + status = Status(timeout=0.1, settle_time=settle_time).set_finished() + return status + + def unstage(self, settle_time=0.1): + self._switch.set("Stop", settle_time=settle_time).wait() + super().unstage() + + # 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) -> Status: + """ DDC just reads back whatever is available in the buffers""" + status = Status(settle_time=settle_time) + 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__": + + # Drive data collection + tcDdc = aa1AxisDriveDataCollection("X02DA-ES1-SMP1:ROTY:DDC:", name="tcddc") + tcDdc.wait_for_connection() + + diff --git a/ophyd_devices/epics/devices/AerotechAutomation1Enums.py b/ophyd_devices/epics/devices/AerotechAutomation1Enums.py new file mode 100644 index 0000000..ac0f981 --- /dev/null +++ b/ophyd_devices/epics/devices/AerotechAutomation1Enums.py @@ -0,0 +1,914 @@ +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/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 2b5c0c2..05a1546 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -29,5 +29,6 @@ from .eiger9m_csaxs import Eiger9McSAXS from .pilatus_csaxs import PilatuscSAXS from .falcon_csaxs import FalconcSAXS from .delay_generator_csaxs import DelayGeneratorcSAXS +from .AerotechAutomation1 import aa1Controller, aa1Tasks # from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin From 47815ee2386bee5b5e0ae3dbbb940d10c6ce1bd4 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Mon, 19 Feb 2024 15:55:32 +0100 Subject: [PATCH 02/16] Seuence scan via Ophyd works --- .../epics/db/office_teststand_bec_config.yaml | 45 ++- .../epics/devices/AerotechAutomation1.py | 280 +++++++++++------- ophyd_devices/epics/devices/__init__.py | 2 +- 3 files changed, 221 insertions(+), 106 deletions(-) diff --git a/ophyd_devices/epics/db/office_teststand_bec_config.yaml b/ophyd_devices/epics/db/office_teststand_bec_config.yaml index 92be2bb..77866eb 100644 --- a/ophyd_devices/epics/db/office_teststand_bec_config.yaml +++ b/ophyd_devices/epics/db/office_teststand_bec_config.yaml @@ -16,13 +16,56 @@ es1_aa1: es1_aa1Tasks: - description: 'AA1 task management' + description: 'AA1 task management interface' deviceClass: epics:devices:aa1Tasks deviceConfig: {prefix: 'X02DA-ES1-SMP1:TASK:'} onFailure: buffer enabled: true readoutPriority: monitored +es1_aa1GlobalVar: + description: 'AA1 global variables interface' + deviceClass: epics:devices:aa1GlobalVariables + deviceConfig: {prefix: 'X02DA-ES1-SMP1:VAR:'} + onFailure: buffer + enabled: true + readoutPriority: monitored + +es1_aa1GlobalMon: + description: 'AA1 polled global variable interface' + deviceClass: epics:devices:aa1GlobalVariableBindings + deviceConfig: {prefix: 'X02DA-ES1-SMP1:VAR:'} + onFailure: buffer + enabled: true + readoutPriority: monitored + +es1_psod: + description: 'AA1 PSO output interface' + deviceClass: epics:devices:aa1AxisPsoDistance + deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY:PSO:'} + onFailure: buffer + enabled: true + readoutPriority: monitored + +es1_ddaq: + description: 'AA1 drive data collection interface' + deviceClass: epics:devices:aa1AxisDriveDataCollection + deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY:DDC:'} + onFailure: buffer + enabled: true + readoutPriority: monitored + + + + + + + + + + + + diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index 3cd4c57..9b4d2ff 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -1,5 +1,5 @@ from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal -from ophyd.status import Status, SubscriptionStatus, StatusBase +from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus from time import sleep import warnings import numpy as np @@ -14,27 +14,60 @@ from typing import Union from collections import OrderedDict +class EpicsMotorX(Device): + mot = Component(EpicsMotor, "", name='mot') + def move(position): + self.mot.move(position, wait=True) + + def goto(position, wait=True): + super().move(position, wait) + + def sleep(time_ms): + sleep(time_ms) + + def epicsCharArray2String(raw: bytes) -> str: """ Ophyd might convert strings to uint16 vector...""" if isinstance(raw, np.ndarray): raw = raw[:-1] if raw[-1]==0 else raw sReply = ''.join(str(s, encoding='ASCII') for s in raw.astype(np.uint8)) - print(f"Raw reply: {raw}\nConverted reply: {sReply}") + #print(f"Raw reply: {raw}\nConverted reply: {sReply}") return sReply else: return raw - - - class EpicsSignalPassive(Device): - value = Component(EpicsSignalRO, "", kind=Kind.hinted) + value = Component(EpicsSignalRO, "", kind=Kind.omitted) proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True) def get(self): self.proc.set(1).wait() return self.value.get() +class EpicsPassiveRO(EpicsSignalRO): + """Special helper class to work around a bug in ophyd (caproto backend) + that reads CHAR array strigs as uint16 arrays. + """ + + 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 EpicsStringPassiveRO(EpicsSignalRO): """Special helper class to work around a bug in ophyd (caproto backend) that reads CHAR array strigs as uint16 arrays. @@ -118,8 +151,8 @@ class aa1Controller(Device): 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, "RUNNING", auto_monitor=True, kind=Kind.hinted) - slowpoll = Component(EpicsSignalRO, "RUNNING", auto_monitor=True, kind=Kind.hinted) + 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) @@ -139,27 +172,20 @@ class aa1Tasks(Device): warnStatus = Component(EpicsSignalRO, "WARNW", 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", 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", auto_monitor=True) + _executeReply = Component(EpicsSignalRO, "EXECUTE_RBV", string=True, auto_monitor=True) - fileName = Component(EpicsSignal, "FILENAME", kind=Kind.config, put_complete=True) - _fileList = Component(EpicsSignalPassive, "FILELIST", kind=Kind.config) - _fileRead = Component(EpicsSignalPassive, "FILEREAD", kind=Kind.config) - _fileWrite = Component(EpicsSignal, "FILEWRITE", kind=Kind.config, put_complete=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._currentTaskMonitor = None self._textToExecute = None - - def listFiles(self) -> list: - """ List all available files on the controller """ - # Have to use CHAR array due to EPICS LSI bug... - namesbytes = self._fileList.get().astype(np.uint8).tobytes() - nameslist = namesbytes.decode('ascii').split('\t') - return nameslist + self._isConfigured = False def readFile(self, filename: str) -> str: """ Read a file from the controller """ @@ -181,14 +207,14 @@ class aa1Tasks(Device): """ Run a script file that either exists, or is newly created and compiled""" self.configure(text=filetext, filename=filename, taskIndex=taskIndex) - self.trigger(settle_time=settle_time).wait() + self.trigger().wait() 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.trigger(settle_time=settle_time).wait() + self.trigger().wait() if mode in [0, "None", None]: return None @@ -196,22 +222,24 @@ class aa1Tasks(Device): raw = self._executeReply.get() return epicsCharArray2String(raw) - def configure(self, text: str=None, filename: str=None, taskIndex: int=4, settle_time=None, **kwargs): - """ Interface for configuration """ + 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 + # Validation - if taskIndex < 0 or taskIndex > 31: + 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") - if 'mode' in kwargs: - if kwargs['mode'] not in [0, 1, 2, 3, 4, "None", "Axis", "Int", "Double", "String", None]: - raise RuntimeError(f"Unknown execution mode: {kwargs['mode']}") - if kwargs['mode'] is None: - kwargs['mode'] = "None" - else: - kwargs['mode'] = "None" - # common operations + # Common operations + old = self.read_configuration() self.taskIndex.set(taskIndex).wait() self._textToExecute = None #self._currentTaskMonitor = aa1TaskState() @@ -236,42 +264,60 @@ class aa1Tasks(Device): self._textToExecute = None else: raise RuntimeError("Unsupported filename-text combo") + self._isConfigured = True + + new = self.read_configuration() + return (old, new) ########################################################################## # Bluesky stepper interface def stage(self) -> None: """ Default staging """ + print("Staging") super().stage() def unstage(self) -> None: """ Default unstaging """ + print("Unstaging") super().unstage() - def trigger(self, settle_time=0.2) -> StatusBase: + def trigger(self, settle_time=0.2) -> Status: """ Execute the script on the configured task""" - if self._textToExecute is not None: - status = self._execute.set(self._textToExecute, settle_time=settle_time) + print("Triggering") + if self._isConfigured: + if self._textToExecute is not None: + status = self._execute.set(self._textToExecute, settle_time=settle_time) + else: + status = self.switch.set("Run", settle_time=settle_time) else: - status = self.switch.set("Run", settle_time=settle_time) + status = Status() + status.set_finished() return status def stop(self): """ Stop the currently selected task """ + print(type(self.switch)) self.switch.set("Stop").wait() ########################################################################## # Flyer interface - def kickoff(self) -> StatusBase: + def kickoff(self) -> DeviceStatus: """ Execute the script on the configured task""" - if self._textToExecute is not None: - status = self._execute.set(self._textToExecute, settle_time=0.5) + print("Kickoff") + if self._isConfigured: + if self._textToExecute is not None: + status = self._execute.set(self._textToExecute, settle_time=0.5) + else: + status = self.switch.set("Run", settle_time=0.1) else: - status = self.switch.set("Run", settle_time=0.1) + status = DeviceStatus(self) + status.set_finished() return status - def complete(self) -> StatusBase: + def complete(self) -> DeviceStatus: """ Execute the script on the configured task""" + print("Complete") #return self._currentTaskMonitor.complete() - status = StatusBase() + status = DeviceStatus(self) status.set_finished() return status @@ -281,6 +327,7 @@ class aa1Tasks(Device): return {self.name: dd} def collect(self) -> OrderedDict: + print("Collecting") ret = OrderedDict() ret["timestamps"] = {"success": time.time()} ret["data"] = {"success": 1} @@ -305,7 +352,6 @@ class aa1TaskState(Device): def notRunning(*args, old_value, value, timestamp, **kwargs): nonlocal timestamp_ result = False if (timestamp_== 0) else (value not in ["Running", 4]) - print(f"Old {old_value}\tNew: {value}\tResult: {result}") timestamp_ = timestamp return result @@ -313,8 +359,8 @@ class aa1TaskState(Device): status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) return status - def kickoff(self) -> StatusBase: - status = StatusBase() + def kickoff(self) -> DeviceStatus: + status = DeviceStatus(self) status.set_finished() return status @@ -331,7 +377,7 @@ class aa1TaskState(Device): class aa1DataAcquisition(Device): - """ Controller Data Acquisition + """ 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 @@ -466,20 +512,20 @@ class aa1GlobalVariables(Device): 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(EpicsSignalPassive, "INT-RBV", kind=Kind.omitted) + integer_rb = Component(EpicsPassiveRO, "INT-RBV", kind=Kind.omitted) integerarr = Component(EpicsSignal, "INTARR", kind=Kind.omitted, put_complete=True) - integerarr_rb = Component(EpicsSignalPassive, "INTARR-RBV", kind=Kind.omitted) + 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(EpicsSignalPassive, "REAL-RBV", kind=Kind.omitted) + real_rb = Component(EpicsPassiveRO, "REAL-RBV", kind=Kind.omitted) realarr = Component(EpicsSignal, "REALARR", kind=Kind.omitted, put_complete=True) - realarr_rb = Component(EpicsSignalPassive, "REALARR-RBV", kind=Kind.omitted) + 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", kind=Kind.omitted, put_complete=True) - string_rb = Component(EpicsStringPassiveRO, "STRING-RBV", kind=Kind.omitted) + 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 """ @@ -594,13 +640,12 @@ class aa1GlobalVariableBindings(Device): 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) - - _str0 = Component(EpicsStringRO, "STR0_RBV", auto_monitor=True, name="str0_raw", kind=Kind.omitted) - str0 = Component(DerivedSignal, "_str0", inverse=epicsCharArray2String, auto_monitor=True, name="str0", kind=Kind.hinted) - str1 = Component(EpicsStringRO, "STR1_RBV", auto_monitor=True, name="str1", kind=Kind.hinted) - str4 = Component(EpicsString, "STR4_RBV", put_complete=True, auto_monitor=True, write_pv="STR4", name="str4", kind=Kind.hinted) - str5 = Component(EpicsString, "STR5_RBV", put_complete=True, auto_monitor=True, write_pv="STR5", name="str4", 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(EpicsString, "STR4_RBV", put_complete=True, string=True, auto_monitor=True, write_pv="STR4", name="str4", kind=Kind.config) + str5 = Component(EpicsString, "STR5_RBV", put_complete=True, string=True, auto_monitor=True, write_pv="STR5", name="str5", kind=Kind.config) @@ -735,33 +780,32 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): refer to Automation1's online manual. """ - def __init__(self, *args, **kwargs): - """ Member declarations in init""" + 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 - super().__init__(*args, **kwargs) # ######################################################################## # PSO high level interface - def configure(self, distance: Union[float, np.ndarray, list, tuple], wmode: str, - t_pulse: float=2000, w_pulse: float=5000, n_pulse: int=1, - posInput: int=None, pinOutput: int=None, **argv) -> None: + 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 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"]: raise RuntimeError(f"Unsupported distace triggering mode: {wmode}") - - # Set the position data source and output pin - if posInput is not None: - self.posInput.set(posInput).wait() - if pinOutput is not None: - self.pinOutput.set(pinOutput).wait() - + old = self.read_configuration() # Configure distance generator (also resets counter to 0) self._distanceValue = distance if isinstance(distance, (float, int)): @@ -783,10 +827,13 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): # 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() + # 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 @@ -800,12 +847,15 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): # Set PSO output data source self.outSource.set("Waveform").wait() + + new = self.read_configuration() + return (old, new) # ######################################################################## # Bluesky step scan interface def stage(self, settle_time=None): self.dstEventsEna.set("On").wait() - if isinstance(self._distanceValue, (np.ndarray, list, tuple)): + if hasattr(self, "_distanceValue") and isinstance(self._distanceValue, (np.ndarray, list, tuple)): self.dstArrayRearm.set(1).wait() self.dstCounterEna.set("On").wait() if settle_time is not None: @@ -822,21 +872,21 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): return super().unstage() # ######################################################################## # Bluesky flyer interface - def kickoff(self) -> StatusBase: + 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 = StatusBase() + status = DeviceStatus(self) status.set_finished() return status - def complete(self) -> StatusBase: + def complete(self) -> DeviceStatus: """ Bluesky flyer interface""" # Array mode waits until the buffer is empty - if isinstance(self._distanceValue, (np.ndarray, list, tuple)): + 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): @@ -850,7 +900,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) else: # In distance trigger mode there's no specific goal - status = StatusBase() + status = DeviceStatus(self) status.set_finished() return status @@ -887,34 +937,35 @@ class aa1AxisPsoWindow(aa1AxisPsoBase): For a more detailed description of additional signals and masking plase refer to Automation1's online manual. """ - def __init__(self, *args, **kwargs): - """ Member declarations in init""" + 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" - super().__init__(*args, **kwargs) # ######################################################################## # PSO high level interface - def configure(self, bounds: Union[np.ndarray, list, tuple], wmode: str, emode: str="Enter", - t_pulse: float=2000, w_pulse: float=5000, n_pulse: int=1, - posInput: int=None, pinOutput: int=None, **argv) -> None: + 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 - # Set the position data source and output pin - if posInput is not None: - self.posInput.set(posInput).wait() - if pinOutput is not None: - self.outPin.set(pinOutput).wait() + old = self.read_configuration() # Configure the window module # Set the window ranges (MUST be in start position) @@ -958,6 +1009,9 @@ class aa1AxisPsoWindow(aa1AxisPsoBase): 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]: @@ -1017,16 +1071,22 @@ class aa1AxisDriveDataCollection(Device): _buffer0 = Component(EpicsSignalRO, "BUFFER0", auto_monitor=True, kind=Kind.hinted) _buffer1 = Component(EpicsSignalRO, "BUFFER1", auto_monitor=True, kind=Kind.hinted) - def configure(self, npoints, - trigger: int=DriveDataCaptureTrigger.PsoOutput, - source0: int=DriveDataCaptureInput.PrimaryFeedback, - source1: int=DriveDataCaptureInput.PositionCommand): + 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): @@ -1038,15 +1098,16 @@ class aa1AxisDriveDataCollection(Device): 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) -> Status: + def complete(self, settle_time=0.1) -> DeviceStatus: """ DDC just reads back whatever is available in the buffers""" - status = Status(settle_time=settle_time) + status = DeviceStatus(self, settle_time=settle_time) status.set_finished() return status @@ -1100,11 +1161,22 @@ class aa1AxisDriveDataCollection(Device): + # Automatically start simulation if directly invoked if __name__ == "__main__": - + AA1_IOC_NAME = "X02DA-ES1-SMP1" + AA1_AXIS_NAME = "ROTY" # Drive data collection - tcDdc = aa1AxisDriveDataCollection("X02DA-ES1-SMP1:ROTY:DDC:", name="tcddc") - tcDdc.wait_for_connection() + 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/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 05a1546..62a5540 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -29,6 +29,6 @@ from .eiger9m_csaxs import Eiger9McSAXS from .pilatus_csaxs import PilatuscSAXS from .falcon_csaxs import FalconcSAXS from .delay_generator_csaxs import DelayGeneratorcSAXS -from .AerotechAutomation1 import aa1Controller, aa1Tasks +from .AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX # from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin From bb37167d33d65edb796a38deb2e696b23379602f Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Wed, 21 Feb 2024 16:52:43 +0100 Subject: [PATCH 03/16] Smaller fixes from cross testing --- ophyd_devices/epics/devices/AerotechAutomation1.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index 9b4d2ff..f1011c8 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -191,11 +191,11 @@ class aa1Tasks(Device): """ 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().astype(np.uint8).tobytes() + filebytes = self._fileRead.get() # C-strings terminate with trailing zero if filebytes[-1]==0: filebytes = filebytes[:-1] - filetext = filebytes.decode('ascii') + filetext = filebytes return filetext def writeFile(self, filename: str, filetext: str) -> None: @@ -206,14 +206,14 @@ class aa1Tasks(Device): 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) + self.configure({'text': filetext, 'filename': filename, 'taskIndex': taskIndex}) self.trigger().wait() 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.configure({'text': text, 'taskIndex': taskIndex, 'mode': mode}) self.trigger().wait() if mode in [0, "None", None]: @@ -248,7 +248,8 @@ class aa1Tasks(Device): if (filename is None) and (text not in [None, ""]): # Direct command execution print("Preparing for direct command execution") - self._executeMode.set(kwargs['mode']).wait() + 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 From 524ed777f0b6b5fbe2dd361bc0663c495f64131a Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Fri, 23 Feb 2024 16:15:09 +0100 Subject: [PATCH 04/16] Extended motor class --- .../epics/db/office_teststand_bec_config.yaml | 3 +- .../epics/devices/AerotechAutomation1.py | 46 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/ophyd_devices/epics/db/office_teststand_bec_config.yaml b/ophyd_devices/epics/db/office_teststand_bec_config.yaml index 77866eb..8d704d4 100644 --- a/ophyd_devices/epics/db/office_teststand_bec_config.yaml +++ b/ophyd_devices/epics/db/office_teststand_bec_config.yaml @@ -1,6 +1,7 @@ es1_roty: description: 'Test rotation stage' - deviceClass: EpicsMotor +# deviceClass: EpicsMotor + deviceClass: epics:devices:EpicsMotorX deviceConfig: {prefix: 'X02DA-ES1-SMP1:ROTY'} onFailure: buffer enabled: true diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index f1011c8..aa6a255 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -1,5 +1,6 @@ from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus +from ophyd.flyers import FlyerInterface from time import sleep import warnings import numpy as np @@ -14,16 +15,45 @@ from typing import Union from collections import OrderedDict -class EpicsMotorX(Device): - mot = Component(EpicsMotor, "", name='mot') - def move(position): - self.mot.move(position, wait=True) +#class EpicsMotorX(EpicsMotor): +# pass + + +class EpicsMotorX(EpicsMotor): + SUB_PROGRESS = "progress" + + def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + self._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'] + if "position" in d: + self._targetPosition = d['position'] + #super().configure(d) + + def kickoff(self): + self._startPosition = float( self.position) + return self.move(self._targetPosition, wait=False) + + 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)), ) + + - def goto(position, wait=True): - super().move(position, wait) - def sleep(time_ms): - sleep(time_ms) def epicsCharArray2String(raw: bytes) -> str: From 49da8b32da4ea5e70ec590f266a8c0133526a93f Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 27 Feb 2024 15:32:59 +0100 Subject: [PATCH 05/16] More cleanup with testing --- .../epics/devices/AerotechAutomation1.py | 2007 +++++++---------- 1 file changed, 845 insertions(+), 1162 deletions(-) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index aa6a255..d683640 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -1,1213 +1,896 @@ -from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal -from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus -from ophyd.flyers import FlyerInterface -from time import sleep -import warnings -import numpy as np -import time +# -*- coding: utf-8 -*- +""" +Created on Thu Nov 3 15:54:08 2022 -from .AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, - AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, - TaskDataSignal, SystemDataSignal, TomcatSequencerState) +@author: mohacsi_i +""" +import os +os.environ["EPICS_CA_ADDR_LIST"]="129.129.144.255" -from .AerotechAutomation1Enums import * -from typing import Union from collections import OrderedDict +import warnings +import numpy as np +from time import sleep, time +import unittest +from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO -#class EpicsMotorX(EpicsMotor): -# pass - - -class EpicsMotorX(EpicsMotor): - SUB_PROGRESS = "progress" - - def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): - super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) - self._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'] - if "position" in d: - self._targetPosition = d['position'] - #super().configure(d) - - def kickoff(self): - self._startPosition = float( self.position) - return self.move(self._targetPosition, wait=False) - - 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)), ) +try: + from AerotechAutomation1 import (aa1Controller, aa1Tasks, aa1TaskState, aa1DataAcquisition, + aa1GlobalVariables, aa1AxisIo, aa1AxisDriveDataCollection, + aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisPsoWindow) + from AerotechAutomation1Enums import AxisDataSignal, SystemDataSignal, DriveDataCaptureInput, DriveDataCaptureTrigger +except Exception: + from .AerotechAutomation1 import (aa1Controller, aa1Tasks, aa1TaskState, aa1DataAcquisition, + aa1GlobalVariables, aa1AxisIo, aa1AxisDriveDataCollection, + aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisPsoWindow) + from .AerotechAutomation1Enums import AxisDataSignal, SystemDataSignal, DriveDataCaptureInput, DriveDataCaptureTrigger +AA1_IOC_NAME = "X02DA-ES1-SMP1" +AA1_AXIS_NAME = "ROTY" +class AerotechAutomation1Test(unittest.TestCase): + _isConnected = False -def epicsCharArray2String(raw: bytes) -> str: - """ Ophyd might convert strings to uint16 vector...""" - if isinstance(raw, np.ndarray): - raw = raw[:-1] if raw[-1]==0 else raw - sReply = ''.join(str(s, encoding='ASCII') for s in raw.astype(np.uint8)) - #print(f"Raw reply: {raw}\nConverted reply: {sReply}") - return sReply - else: - return raw - -class EpicsSignalPassive(Device): - value = Component(EpicsSignalRO, "", kind=Kind.omitted) - proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True) - def get(self): - self.proc.set(1).wait() - return self.value.get() - - -class EpicsPassiveRO(EpicsSignalRO): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. - """ + @classmethod + def setUpClass(cls): + try: + # HIL testing only if there's hardware + dev = aa1Controller(AA1_IOC_NAME+":CTRL:", name="aa1") + dev.wait_for_connection() + cls._isConnected = True + print("\n") + except Exception as ex: + print(ex) - 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 test_00_Instantiation(self): + """ Test instantiation and interfaces - 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 EpicsStringPassiveRO(EpicsSignalRO): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. - """ - - 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() - raw = super().get(*args, **kwargs) - return epicsCharArray2String(raw) - - @property - def value(self): - raw = super().value - return epicsCharArray2String(raw) - - -class EpicsStringPassive(EpicsSignal): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. - """ - def __init__(self, read_pv, write_pv=None, *, put_complete=False, - string=False, limits=False, name=None, **kwargs): - super().__init__(read_pv, write_pv, put_complete, string, limits, 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() - raw = super().get(*args, **kwargs) - return epicsCharArray2String(raw) - - @property - def value(self): - raw = super().value - return epicsCharArray2String(raw) - - -class EpicsString(EpicsSignal): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. - """ - def get(self, *args, **kwargs): - raw = super().get(*args, **kwargs) - return epicsCharArray2String(raw) - - @property - def value(self): - raw = super().value - return epicsCharArray2String(raw) - - -class EpicsStringRO(EpicsSignalRO): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. - """ - def get(self, *args, **kwargs): - raw = super().get(*args, **kwargs) - return epicsCharArray2String(raw) - - @property - def value(self): - raw = super().value - return epicsCharArray2String(raw) - - -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. - - Execute 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. - """ - _failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) - errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) - warnStatus = Component(EpicsSignalRO, "WARNW", 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._currentTaskMonitor = None - self._textToExecute = None - self._isConfigured = False - - 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}) - self.trigger().wait() - - 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.trigger().wait() - - if mode in [0, "None", None]: - return None - else: - raw = self._executeReply.get() - return epicsCharArray2String(raw) - - def configure(self, d: dict={}) -> tuple: - """ Configuration interface for flying + This method tests the more abstract bluesky compatibility of the + aerotech ophyd devices. In particular it checks the existence of the + required schema functions (that are not called in other tests). """ - # 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 - - # 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") + # Throws if IOC is not available or PVs are incorrect + def classTestSequence(DeviceClass, prefix, config=None, setable=False, set_value=None, flyable=False): + dut = DeviceClass(prefix, name="dut") + dut.wait_for_connection() - # Common operations - old = self.read_configuration() - self.taskIndex.set(taskIndex).wait() - self._textToExecute = None - #self._currentTaskMonitor = aa1TaskState() + # Readable device (required) + dut.describe() # Describes read() + dut.read() # Read the device active values + dut.trigger() + dut.describe_configuration() # Describes read_configuration() + dut.read_configuration() # Reads device configuration + if config is not None: + try: + ret = dut.configure(config) + self.assertTrue(type(ret)==tuple, f"On device class {DeviceClass}: configure is expected to return a tuple") + self.assertTrue(isinstance(ret[0], OrderedDict), f"On device class {DeviceClass}: old configuration is expected to be an OrderedDict") + self.assertTrue(isinstance(ret[1], (dict, OrderedDict)), f"On device class {DeviceClass}: new configuration is expected to be dict or OrderedDict") + except Exception: + pass + # Readable device (optional) + dut.stage() + dut.unstage() + # Setable device + if setable and set_value is not None: + try: + st = dut.set(set_value) + #dut.stop() + dut.check_value(set_value) + dut.position + except Exception as ex: + warnings.warn(f"Exception occured while testing setable interface: {ex}") + if flyable: + try: + dut.configure(config) + st = dut.kickoff() + st = dut.complete() + ret = dut.describe_collect() + ret = dut.collect() + except Exception: + pass + + classDBase = [ + (aa1Controller, f"{AA1_IOC_NAME}:CTRL:"), + (aa1Tasks, f"{AA1_IOC_NAME}:TASK:"), + (aa1DataAcquisition, f"{AA1_IOC_NAME}:DAQ:"), + (aa1GlobalVariables, f"{AA1_IOC_NAME}:VAR:"), + (aa1GlobalVariableBindings, f"{AA1_IOC_NAME}:VAR:"), + + (EpicsMotor, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}"), + (aa1AxisIo, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:IO:"), + (aa1AxisDriveDataCollection, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:DDC:"), + (aa1AxisPsoDistance, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:PSO:"), + (aa1AxisPsoWindow, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:PSO:"), + ] - # 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") - self._isConfigured = True - - new = self.read_configuration() - return (old, new) - - ########################################################################## - # Bluesky stepper interface - def stage(self) -> None: - """ Default staging """ - print("Staging") - super().stage() - def unstage(self) -> None: - """ Default unstaging """ - print("Unstaging") - super().unstage() + print("BSKY: Testing basic Ophyd device class interfaces") + for DeviceClass, prefix in classDBase: + classTestSequence(DeviceClass, prefix) + print("BSKY: Done testing CTRL module!") - def trigger(self, settle_time=0.2) -> Status: - """ Execute the script on the configured task""" - print("Triggering") - if self._isConfigured: - if self._textToExecute is not None: - status = self._execute.set(self._textToExecute, settle_time=settle_time) - else: - status = self.switch.set("Run", settle_time=settle_time) - else: - status = Status() - status.set_finished() - return status - def stop(self): - """ Stop the currently selected task """ - print(type(self.switch)) - self.switch.set("Stop").wait() - - ########################################################################## - # Flyer interface - def kickoff(self) -> DeviceStatus: - """ Execute the script on the configured task""" - print("Kickoff") - if self._isConfigured: - if self._textToExecute is not None: - 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() - return status - - def complete(self) -> DeviceStatus: - """ Execute the script on the configured task""" - print("Complete") - #return self._currentTaskMonitor.complete() - 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 {self.name: dd} + def test_01_ControllerProxy(self): + # Throws if IOC is not available or PVs are incorrect + dev = aa1Controller(AA1_IOC_NAME+":CTRL:", name="aa1") + dev.wait_for_connection() - def collect(self) -> OrderedDict: - print("Collecting") - ret = OrderedDict() - ret["timestamps"] = {"success": time.time()} - ret["data"] = {"success": 1} - yield ret + print("CTRL: Testing basic controller readback") + # Check if controller info was loaded correctly + self.assertTrue(len(dev.controllername.get())>0, "Controller name seems invalid") + self.assertTrue(len(dev.serialnumber.get())>0, "Controller serial number seems invalid") + self.assertTrue(dev.axiscount.get()>0, "Maximum controller axis count seems invalid") + self.assertTrue(dev.taskcount.get()>0, "Maximum controller task count seems invalid") + print("CTRL: Done testing CTRL module!") + + + def test_02_TaskProxy(self): + # Throws if IOC is not available or PVs are incorrect + dev = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + dev.wait_for_connection() + + print("\nTASK: Testing the 'execute' command (direct string execution)") + # Attempt to run a short test script using the Execute interface (real) + text = "$rreturn[0]=3.141592" + rbv = dev.execute(text, mode="Double", taskIndex=3) + + self.assertTrue(dev._executeMode.get() in ["Double", 3], f"Expecting double return type for execution, but got: {dev._executeMode.get()}") + self.assertTrue(dev.taskIndex.get()==3, "Execution must happen on task 3") + self.assertTrue(rbv=="Result (double): 3.141592", f"Unexpected reply from execution: {rbv}") + # Attempt to run a short test script using the Execute interface (integer) + text = "$ireturn[0]=42" + rbv = dev.execute(text, mode="Int", taskIndex=4) + self.assertTrue(dev.taskIndex.get()==4, "Execution must happen on task 1") + self.assertTrue(rbv=="Result (int): 42", f"Unexpected reply from execution: {rbv}") + + print("TASK: Testing the 'file' interface") + # Write and read back a file + filename = "unittesting.ascript" + text = "program\nvar $positions[2] as real\nend" + if len(text)>40: + print(f"\tWARN: Program text is too long for EPICS string: {len(text)}") + + dev.writeFile(filename, text) + contents = dev.readFile(filename) + self.assertTrue(contents==text, "File contents changed on readback") + + print("TASK: Testing the 'run' command (run an existing file)") + # Send another empty program + filename = "unittesting2.ascript" + text = "program\nvar $positions[2] as real\nend" + dev.runScript(filename, taskIndex=3, filetext=text) + + print("TASK: Testing bluesky interface with free text)") + dev.configure({'text': "$rreturn[0]=137.036", 'taskIndex': 4, 'mode': "Double"}) + dev.kickoff().wait() + dev.complete().wait() + rbv = dev._executeReply.get() + self.assertTrue(dev._executeMode.get() in ["Double", 3], f"Expecting double return type for execution, but got: {dev._executeMode.get()}") + self.assertTrue(dev.taskIndex.get()==4, "Execution must happen on task 4") + self.assertTrue(rbv=="Result (double): 137.036000", f"Unexpected reply from execution: {rbv}") + print("TASK: Done testing TASK module!") -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""" - # 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 - return result + def test_03_DataCollectionProxy(self): + """ This tests the controller data collection - # 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. + Test for testing the controller data collection that occurs at a + fixed frequency. This interface is not so important if there's + hardware synchronization. """ - 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() + return + # Throws if IOC is not available or PVs are incorrect + dev = aa1DataAcquisition(AA1_IOC_NAME+":DAQ:", name="daq") + dev.wait_for_connection() - def stop(self) -> None: - """ Stop a running data collection """ - self._switch.set("STOP").wait() + print("\nDAQ: Testing the controller configuration") + self.assertTrue(dev.status.get()==0, "DAQ must be idle to start the tests") + self.assertTrue(dev.points_max.get()>30000, "The test requires at least 30000 point buffer") - 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) + ###################################################################### + # Test configuration 1 + print("DAQ: Running a short test run") + dev.startConfig(3000, "1kHz") + dev.addSystemSignal(SystemDataSignal.DataCollectionSampleTime) + dev.addAxisSignal(0, AxisDataSignal.PositionFeedback) + self.assertTrue(dev.signal_num.get()==2, "Expected 2 data sources") + # Run the data acquisition (run blocks and returns array) + _ = dev.run() + # Check post-run parameter values + self.assertTrue(dev.points_total.get()==3000, "Expected to have 3k points") + self.assertTrue(dev.points_collected.get()==3000, "Expected to record 3k points, did {dev.points_collected.value}") + + print("DAQ: Checking the returned array") + arr = dev.dataReadBack() + self.assertTrue(arr.size==6000, f"Unexpected data array size {arr.size}") + self.assertTrue(arr.shape==(3000,2), f"Unexpected data array shape {arr.size}") + + ###################################################################### + # Test manual configuration 2 + print("DAQ: Running a short test run") + dev.startConfig(2800, "1kHz") + dev.addSystemSignal(SystemDataSignal.DataCollectionSampleTime) + dev.addAxisSignal(0, AxisDataSignal.PositionFeedback) + dev.addAxisSignal(0, AxisDataSignal.VelocityFeedback) + self.assertTrue(dev.signal_num.get()==3, "Expected 3 data sources") + # Run the data acquisition (run blocks and returns array) + _ = dev.run() + # Check post-run parameter values + self.assertTrue(dev.points_total.get()==2800, f"Expected to have 2.8k points, got {dev.points_total.get()}") + self.assertTrue(dev.points_collected.get()==2800, f"Expected to record 2.8k points, got {dev.points_collected.get()}") + + print("DAQ: Checking the returned array") + arr = dev.dataReadBack() + self.assertTrue(arr.size==8400, f"Unexpected data array size {arr.size}") + self.assertTrue(arr.shape==(2800,3), f"Unexpected data array shape {arr.size}") + print("DAQ: Done testing DAQ module!") + + + def test_04_GlobalVariables1(self): + """Test the basic read/write global variable interface + """ + # Throws if IOC is not available or PVs are incorrect + dev = aa1GlobalVariables(AA1_IOC_NAME+":VAR:", name="gvar") + polled = aa1GlobalVariableBindings(AA1_IOC_NAME+":VAR:", name="pvar") + dev.wait_for_connection() + polled.wait_for_connection() + + print("\nVAR: Checking available memory") + self.assertTrue(dev.num_int.get()>=32, "At least 32 integer variables are needed for this test") + self.assertTrue(dev.num_real.get()>=32, "At least 32 real variables are needed for this test") + self.assertTrue(dev.num_string.get()>=32, "At least 32 string[256] variables are needed for this test") + + print("VAR: Setting and checking a few variables") + # Use random variables for subsequent runs + val_f = 100*np.random.random() + dev.writeFloat(11, val_f) + rb_f = dev.readFloat(11) + self.assertEqual(val_f, rb_f, f"Floating point readback value changed, read {rb_f} instead of {val_f}") + rb_f = dev.readFloat(10, 4) + self.assertEqual(val_f, rb_f[1], f"Floating point array readback value changed, read {rb_f[1]} instead of {val_f}") + + val_i = 1+int(100*np.random.random()) + dev.writeInt(13, val_i) + rb_i = dev.readInt(13) + self.assertEqual(val_i, rb_i, f"Integer readback value changed, read {rb_i} instead of {val_i}") + rb_i = dev.readInt(10, 4) + self.assertEqual(val_i, rb_i[3], f"Integer array readback value changed, read {rb_i[3]} instead of {val_i}") + + val_i =1+int(100*np.random.random()) + dev.writeInt(13, val_i) + rb_i = dev.readInt(13) + self.assertEqual(val_i, rb_i, f"Integer readback value changed, read {rb_i} instead of {val_i}") + rb_i = dev.readInt(10, 4) + self.assertEqual(val_i, rb_i[3], f"Integer array readback value changed, read {rb_i[3]} instead of {val_i}") + + val_s = np.random.choice(['f', 'o', 'o', 'b', 'a', 'r'], 6) + val_s = ''.join(val_s) + dev.writeString(7, val_s) + rb_s = dev.readString(7) + self.assertEqual(val_s, rb_s, f"String readback value changed, read {rb_s} instead of {val_s}") + print("VAR: Done testing VAR module basic functionality!") + + + def test_05_GlobalVariables2(self): + """Test the direct global variable bindings together with the task and + standard global variable interfaces. + """ + + # Throws if IOC is not available or PVs are incorrect + var = aa1GlobalVariables(AA1_IOC_NAME+":VAR:", name="gvar") + var.wait_for_connection() + dev = aa1GlobalVariableBindings(AA1_IOC_NAME+":VAR:", name="var") + dev.wait_for_connection() + tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk.wait_for_connection() + + print("\nVAR: Checking the direct global variable bindings") + # Use random variables for subsequent runs + val_f = 100*np.random.random() + dev.float16.set(val_f, settle_time=0.91).wait() # settle time seems to be ignored by ophyd + + rb_f = dev.float16.value + self.assertEqual(val_f, dev.float16.value, f"Floating point readback value changed, read {rb_f}, expected {val_f}") + #rb_f = dev.readFloat(10, 4) + #self.assertEqual(val_f, rb_f[1], f"Floating point array readback value changed, read {rb_f[1]} instead of {val_f}") + + val_i = 1+int(100*np.random.random()) + dev.int8.set(val_i, settle_time=0.91).wait() + rb_i = dev.int8.value + self.assertEqual(val_i, dev.int8.value, f"Integer readback value changed, read {rb_i}, expected {val_i}") + #rb_i = dev.readInt(10, 4) + #self.assertEqual(val_i, rb_i[3], f"Integer array readback value changed, read {rb_i[3]} instead of {val_i}") + + val_s = np.random.choice(['f', 'o', 'o', 'b', 'a', 'r'], 6) + val_s = ''.join(val_s) + dev.str4.set(val_s, settle_time=0.91).wait() + rb_s = dev.str4.value + self.assertEqual(val_s, rb_s, f"String readback value changed, read {rb_s} instead of {val_s}") + print("VAR: Done testing VAR module direct bindings!") + + + def test_06_MotorRecord(self): + """ Tests the basic move functionality of the MotorRecord + """ + + mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") + mot.wait_for_connection() + print("\nMR: Checking the standard EPICS motor record") + + # Four test with absolute moves + dist = 30 + 50.0 * np.random.random() + target = mot.position + dist + print(f"Absolute move to {target}, distance: {dist}") + mot.move(target, wait=True) + self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (1)") + + dist = 30 + 50.0 * np.random.random() + target = mot.position - dist + print(f"Absolute move to {target}, distance: {dist}") + mot.move(target, wait=True) + self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (2)") + + dist = 30 + 50.0 * np.random.random() + target = mot.position+dist + print(f"Absolute move to {target}, distance: {dist}") + mot.move(target, wait=True) + self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (3)") + + dist = 30 + 50.0 * np.random.random() + target = mot.position - dist + print(f"Absolute move to {target}, distance: {dist}") + mot.move(target, wait=True) + self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (4)") + print("MR: Done testing MotorRecord!") + + + def test_07_AxisIo(self): + # Throws if IOC is not available or PVs are incorrect + dev = aa1AxisIo(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":IO:", name="io") + dev.wait_for_connection() + dev.polllvl.set(1, settle_time=0.5).wait() + print("\nAX:IO: Checking axis IO") + + # Writing random values to analog/digital outputs + for ii in range(5): + val_f = np.random.random() + dev.setAnalog(0, val_f) + self.assertAlmostEqual(dev.ao.get(), val_f, 2, "Unexpected readback on analog output") + print("Digital") + for ii in range(5): + val_b = round(np.random.random()) + dev.setDigital(0, val_b) + self.assertEqual(dev.do.get(), val_b, "Unexpected readback on digital output") + print("IO: Done testing IO module!") + + + def test_08_AxisDdc(self): + """ Drive data collection + + This is the preliminary test case dor drive data collection using + internal PSO based triggers. It tests basic functionality, like + configuration, state monitoring and readback. + """ + # Throws if IOC is not available or PVs are incorrect + dev = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") + event = EpicsSignal(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:EVENT:SINGLE", put_complete=True) + dev.wait_for_connection() + event.wait_for_connection() + print("\nAX:DDC: Checking axis DDC") + + # Stop any running acquisition + dev.unstage(settle_time=0.1) + + print("AX:DDC: Running a partial acquisition (1/3)") + # Configure the DDC for 42 points and start waiting for triggers + dev.configure({'npoints': 42, 'trigger': DriveDataCaptureTrigger.PsoEvent}) + dev.kickoff().wait() + # Fire 12 triggers and stop the acquisition early + for ii in range(12): + event.set(1, settle_time=0.02).wait() + # Stop the data acquisition early + dev.unstage() - # 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: + # Wait for polling to catch up + sleep(1) + + # Do the readback and check final state + rbv = yield from dev.collect() + self.assertEqual(dev.nsamples_rbv.value, 12, f"Expected to collect 12 points by now, got {dev.nsamples_rbv.value}") + self.assertEqual(len(rbv["buffer0"]), 12, f"Expected to read back 12 points by now, got {len(rbv['buffer0'])}") + self.assertEqual(len(rbv["buffer1"]), 12, f"Expected to read back 12 points by now, got {len(rbv['buffer1'])}") + + + print("AX:DDC: Running an overruning acquisition (2/3)") + # Configure the DDC for 42 points and start waiting for triggers + dev.configure(16, trigger=DriveDataCaptureTrigger.PsoEvent) + dev.stage() + num = dev.nsamples_rbv.get() + self.assertEqual(num, 0, f"Stage should reset the number of DDC points but got: {num}") + + # Fire 20 triggers and stop the acquisition early + for ii in range(20): + event.set(1, settle_time=0.02).wait() + dev.unstage() + + # Wait for polling to catch up + sleep(1) + + # Do the readback and check final state + rbv = dev.collect() + self.assertEqual(dev.nsamples_rbv.value, 16, f"Expected to collect 16 points by now, got {dev.nsamples_rbv.value}") + self.assertEqual(len(rbv["buffer0"]), 16, f"Expected to read back 16 points by now, got {len(rbv['buffer0'])}") + self.assertEqual(len(rbv["buffer1"]), 16, f"Expected to read back 16 points by now, got {len(rbv['buffer1'])}") + + + print("AX:DDC: Trying to record some noise on the analog input (3/3)") + dev.configure(36, trigger=DriveDataCaptureTrigger.PsoEvent, source1=DriveDataCaptureInput.AnalogInput0) + dev.stage() + num = dev.nsamples_rbv.get() + self.assertEqual(num, 0, f"Stage should reset the number of DDC points but got: {num}") + # Fire 36 triggers + for ii in range(36): + event.set(1, settle_time=0.02).wait() + dev.unstage() + + # Wait for polling to catch up + sleep(1) + + # Do the readback and check final state + rbv = dev.collect() + self.assertEqual(dev.nsamples_rbv.value, 36, f"Expected to collect 12 points by now, got {dev.nsamples_rbv.value}") + self.assertEqual(len(rbv["buffer0"]), 36, f"Expected to read back 36 points by now, got {len(rbv['buffer0'])}") + self.assertEqual(len(rbv["buffer1"]), 36, f"Expected to read back 36 points by now, got {len(rbv['buffer1'])}") + self.assertTrue(np.std(rbv["buffer1"])>0.000001, f"Expected some noise on analog input, but std is: {np.std(rbv['buffer1'])}") + print("AX:DDC: Done testing DDC module!") + + + def test_09_AxisPsoDistance1(self): + """ Test basic PSO distance mode functionality + + This module tests basic PSO distance functionality, like events and + waveform generation. The goal is to ensure the basic operation of + the PSO module and the polled feedback. + """ + # Throws if IOC is not available or PVs are incorrect + dev = aa1AxisPsoDistance(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") + ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") + mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") + mot.wait_for_connection() + dev.wait_for_connection() + ddc.wait_for_connection() + print("\nAX:PSO: Checking axis PSO in distance mode (basic)") + if dev.output.value: + dev.toggle() + sleep(1) + + print("AX:PSO: Directly checking basic 'toggled' event generation (1/3)") + # Configure steps and move to middle of range + dev.configure({'distance': 5, 'wmode': "toggle"}) + mot.move(mot.position + 2.5) + dev.stage() + # Start moving in steps, while checking output + for ii in range(10): + last_pso = dev.output.value + mot.move(mot.position + 5) + sleep(1) + self.assertTrue(dev.output.value != last_pso, f"Expected to toggle the PSO output at step {ii}, got {dev.output.value}") + dev.unstage() + ddc.unstage() + + print("AX:PSO: Checking basic 'toggled' event generation with DDC (2/3)") + # Configure steps and move to middle of range + dev.configure({'distance': 2.0, 'wmode': "toggle"}) + dev.stage() + + # Configure DDC + npoints = 720 / 2.0 + ddc.configure({'npoints': npoints+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) + ddc.stage() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") + self.assertTrue(dev.output.value==0, "Expected to start from LOW state") + + # Start moving and wait for polling + mot.move(mot.position + 720) + sleep(0.5) + + # Evaluate results + npoints_rbv = ddc.nsamples_rbv.value + self.assertTrue(np.abs(npoints_rbv-720/4)<=1, f"Expected to record {720/4} points but got {npoints_rbv}") + dev.unstage() + ddc.unstage() + + + print("AX:PSO: Checking basic 'pulsed' event generation with DDC (3/5)") + # Configure steps and move to middle of range + dev.configure({'distance': 1.8, 'wmode': "pulsed", 'n_pulse': 3}) + dev.kickoff().wait() + + # Configure DDC + npoints = 3 * 720 / 1.8 + ddc.configure({'npoints': npoints+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) + ddc.stage() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") + self.assertTrue(dev.output.value==0, "Expected to start from LOW state") + + # Start moving and wait for polling + mot.move(mot.position + 720.9) + sleep(0.5) + + # Evaluate results + npoints_rbv = ddc.nsamples_rbv.value + self.assertTrue(np.abs(npoints_rbv-3*720/1.8)<=1, f"Expected to record {3*720/1.8} points but got {npoints_rbv}") + print("AX:PSO: Done testing basic PSO distance functionality!") + dev.unstage() + ddc.unstage() + + + def test_10_PsoIntegration01(self): + """ The first integration test aims to verify PSO functionality under + real life scenarios. It uses the DDC for collecting encoder signals + and compares them to the expected values. + """ + # Throws if IOC is not available or PVs are incorrect + dev = aa1AxisPsoDistance(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") + ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") + mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") + mot.wait_for_connection() + dev.wait_for_connection() + ddc.wait_for_connection() + print("\nAX:PSO: Integration test for vector style PSO triggering (advanced)") + + print("AX:PSO: Testing standard 'enable' style PSO triggering with DDC (1/3)") + # This is one line of the Tomcat sequence scan! + acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value + vec_dist = [acc_dist, 18, 0.01, 18, 0.01, 18, 0.01, 18, 0.01, 18, 0.01, 18] + dev.configure({'distance': vec_dist, 'wmode': "toggle"}) + if dev.output.value: + dev.toggle() + dev.stage() + + # Configure DDC + npoints_exp = len(vec_dist)/2 + ddc.configure({'npoints': npoints_exp+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) + ddc.stage() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") + self.assertTrue(dev.output.value==0, "Expected to start from LOW state") + + # Start moving and wait for polling + mot.move(mot.position + np.sum(vec_dist) + 10) + sleep(0.9) + + # Evaluate results + npoints_rbv = ddc.nsamples_rbv.value + self.assertTrue(np.abs(npoints_rbv-npoints_exp)<1, f"Expected to record {npoints_exp} points but got {npoints_rbv}") + self.assertTrue(dev.output.value==0, "Expected to finish in LOW state") + self.assertTrue(dev.dstArrayDepleted.value, "Expected to cover all pints in the distance array") + dev.unstage() + ddc.unstage() + + print("AX:PSO: Checking 'individual' style trigger signal with DDC (2/5)") + # This is practically golden ratio tomography! + acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value + vec_dist = [acc_dist] + vec_dist.extend([1.618]* 100) + dev.configure({'distance': vec_dist, 'wmode': "pulsed"}) + dev.stage() + + # Configure DDC + npoints_exp = len(vec_dist) + ddc.configure({'npoints': npoints_exp+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) + ddc.stage() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") + self.assertTrue(dev.output.value==0, "Expected to start from LOW state") + + # Start moving and wait for polling (move negative direction) + mot.move(mot.position - np.sum(vec_dist) - acc_dist) + sleep(0.9) + + # Evaluate results + npoints_rbv = ddc.nsamples_rbv.value + self.assertTrue(np.abs(npoints_rbv-npoints_exp)<1, f"Expected to record {npoints_exp} points but got {npoints_rbv}") + self.assertTrue(dev.output.value==0, "Expected to finish in LOW state") + self.assertTrue(dev.dstArrayDepleted.value, "Expected to cover all pints in the distance array") + print("AX:PSO: Done testing PSO module in distance mode!") + dev.unstage() + ddc.unstage() + + + def test_11_AxisPsoWindow1(self): + """ Test basic PSO window mode functionality + + This module tests basic PSO distance functionality, like events and + waveform generation. The goal is to ensure the basic operation of + the PSO module and the polled feedback. + """ + return + # Throws if IOC is not available or PVs are incorrect + dev = aa1AxisPsoWindow(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") + ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") + mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") + mot.wait_for_connection() + dev.wait_for_connection() + ddc.wait_for_connection() + print("\nAX:PSO: Checking axis PSO in window mode (basic)") + if dev.output.value: + dev.toggle() + + print("AX:PSO: Directly checking basic window output (1/3)") + # Configure steps and move to middle of range + dev.configure((5, 13), "output") + dev.stage() + sleep(1) + # Move half a step + mot.move(mot.position + 0.1) + dist = 0.1 + + # Start moving in steps, while checking output + for ii in range(42): + mot.move(mot.position + 0.2) + dist += 0.2 + sleep(1) + if 5 < dist < 13: + self.assertTrue(dev.output.value==1, f"Expected HIGH PSO output inside the window at distance {dist}") + else: + self.assertTrue(dev.output.value==0, f"Expected LOW PSO output outside the window at distance {dist}") + + print("AX:PSO: The following tests whould fail as multiple triggers/events are being emmitted when entering/exiting the window!") + dev.unstage() + return + + print("AX:PSO: Checking basic window event generation with DDC (2/3)") + print(" WARN: The encoder output is very noisy, one transition generates several events") + # Move to acceleration position + acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value + print(f"Acceleration distance: {acc_dist}") + start_position = mot.position + mot.move(start_position - acc_dist) + # Configure PSO in window mode + dev.configure((acc_dist, 30 + acc_dist), "pulsed", "Both", n_pulse=1) + # Configure the data capture (must be performed in start position) + ddc.configure(5*10, trigger=DriveDataCaptureTrigger.PsoOutput) + ddc.stage() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") + # Repeated line scan + for ii in range(10): + dev.stage() + mot.move(start_position + 30 + acc_dist) + dev.unstage() + mot.move(start_position - acc_dist) + sleep(0.5) + npoints_rbv = ddc.nsamples_rbv.value + self.assertTrue(np.abs(npoints_rbv-50)<1, f"Expected to record 10 points but got {npoints_rbv}") + + + print("AX:PSO: Checking basic 'toggled' event generation with DDC (3/3)") + print("Test removed as there's some heavy jitter on the PSO output flag") + # Move to acceleration position + acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value + print(f"Acceleration distance: {acc_dist}") + mot.move(mot.position - acc_dist) + # Configure PSO in window mode + dev.configure((acc_dist, 30 + acc_dist + 1), "output") + dev.stage() + # Configure the data capture + ddc.configure(10, trigger=DriveDataCaptureTrigger.PsoOutput) + ddc.stage() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") + # Snake scan + for ii in range(10): + if ii % 2 == 0: + mot.move(mot.position + 30+ 2*acc_dist + 0.1) + else: + mot.move(mot.position - 30 - 2*acc_dist - 0.1) + sleep(0.5) + npoints_rbv = ddc.nsamples_rbv.value + self.assertTrue(np.abs(npoints_rbv-10)<1, f"Expected to record 10 points but got {npoints_rbv}") + + + + def test_12_TomcatSequenceScan(self): + """ Test the zig-zag sequence scan from Tomcat (via BEC) + + This module tests basic PSO distance functionality, like events and + waveform generation. The goal is to ensure the basic operation of + the PSO module and the polled feedback. + """ + # Throws if IOC is not available or PVs are incorrect + pso = aa1AxisPsoDistance(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") + ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") + mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") + mot.wait_for_connection() + pso.wait_for_connection() + ddc.wait_for_connection() + t_start = time() + print("\nTOMCAT Sequeence scan (via Ophyd)") + + ScanStart = 42.0 + ScanRange = 180 + NumRepeat = 10 + ScanType = "PosNeg" + + # Dynamic properties + VelScan = 90 + AccScan = 500 + SafeDist = 10.0 + + ###################################################################### + print("\tConfigure") + # Scan range + AccDist = 0.5 * VelScan * VelScan / AccScan + SafeDist + if ScanType in ["PosNeg", "Pos"]: + PosStart = ScanStart - AccDist + PosEnd = ScanStart + ScanRange + AccDist + elif ScanType in ["NegPos", "Neg"]: + PosStart = ScanStart + AccDist + PosEnd = ScanStart - ScanRange - AccDist + else: + raise RuntimeError(f"Unexpected ScanType: {ScanType}") + + # Motor setup + mot.velocity.set(VelScan).wait() + mot.acceleration.set(VelScan/AccScan).wait() + mot.move(PosStart) + self.assertTrue(np.abs(mot.position-PosStart)<0.1, f"Expected to move to scan start position {PosStart}, found motor at {mot.position}") + + # PSO setup + print(f"Configuring PSO to: {[AccDist, ScanRange]}") + pso.configure({'distance': [AccDist, ScanRange], "wmode": "toggle"}) + + # DDC setup (Tomcat runs in ENABLE mode, so only one posedge) + ddc.configure({'npoints': NumRepeat}) + + print("\tStage") + mot.stage() + pso.stage() + ddc.stage() + + for ii in range(NumRepeat): + # No option to reset the index counter... + pso.dstArrayRearm.set(1).wait() + + if ScanType in ["Pos", "Neg"]: + mot.move(PosEnd) + self.assertTrue(np.abs(mot.position-PosEnd)<1.0, f"Expected to reach {PosEnd}, motor is at {mot.position} on iteration {ii}") + mot.move(PosStart) + self.assertTrue(np.abs(mot.position-PosStart)<1.0, f"Expected to rewind to {PosStart}, motor is at {mot.position} on iteration {ii}") + + if ScanType in ["PosNeg", "NegPos"]: + if ii % 2 == 0: + mot.move(PosEnd) + self.assertTrue(np.abs(mot.position-PosEnd)<1.0, f"Expected to reach {PosEnd}, motor is at {mot.position} on iteration {ii}") + else: + mot.move(PosStart) + self.assertAlmostEqual(mot.position, PosStart, 1, f"Expected to reach {PosStart}, motor is at {mot.position} on iteration {ii}") + 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(EpicsSignalPassive, "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 - + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, ii+1, f"Expected to record {ii+1} DDC points, but got: {npoints_rbv} on iteration {ii}") + print("\tUnstage") + mot.unstage() + ddc.unstage() + pso.unstage() -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 - """ - # 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)}") + t_end = time() + t_elapsed = t_end - t_start + print(f"Elapsed scan time: {t_elapsed}") + + print("Evaluate final state") + sleep(3) + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, NumRepeat, f"Expected to record {NumRepeat} DDC points, but got: {npoints_rbv}") - 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 + def test_13_TomcatSequenceScan(self): + """ Test the zig-zag sequence scan from Tomcat (via AeroScript) + + This module tests basic PSO distance functionality, like events and + waveform generation. The goal is to ensure the basic operation of + the PSO module and the polled feedback. """ - 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)}") - + # Throws if IOC is not available or PVs are incorrect + dev = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk4 = aa1TaskState(AA1_IOC_NAME+ ":TASK:T4:", name="tsk4") + dev.wait_for_connection() + tsk4.wait_for_connection() -class aa1GlobalVariableBindings(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 - """ - 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(EpicsString, "STR4_RBV", put_complete=True, string=True, auto_monitor=True, write_pv="STR4", name="str4", kind=Kind.config) - str5 = Component(EpicsString, "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 IO pin is polled simultaneously! - """ - polllvl = Component(EpicsSignal, "POLLLVL", put_complete=True, kind=Kind.config) - - 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() + t_start = time() + print("\nTOMCAT Sequeence scan (via AeroScript)") + filename = "test/testScan02.ascript" + with open(filename) as f: + text = f.read() + # Copy file to controller and run it + dev.runScript("tcatZigZagSequence2.ascript", taskIndex=4, filetext=text) + sleep(0.5) + tsk4.complete().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) - 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 --> 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. - """ - - 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 + t_end = time() + t_elapsed = t_end - t_start + print(f"Elapsed scan time: {t_elapsed}") - # ######################################################################## - # 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 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"]: - 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() - return (old, new) - - # ######################################################################## - # Bluesky step scan interface - def stage(self, settle_time=None): - self.dstEventsEna.set("On").wait() - if hasattr(self, "_distanceValue") and isinstance(self._distanceValue, (np.ndarray, list, tuple)): - self.dstArrayRearm.set(1).wait() - self.dstCounterEna.set("On").wait() - if settle_time is not None: - sleep(settle_time) - return super().stage() - - def trigger(self): - return super().trigger() + # Do the readback and check final state + ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") + ddc.wait_for_connection() + rbv = yield from ddc.collect() + npoints_rbv = ddc.nsamples_rbv.value + self.assertEqual(npoints_rbv, 10, f"Expected to collect 10 points by now, got {npoints_rbv}") + self.assertEqual(len(rbv["buffer0"]), 10, f"Expected to read back 10 points by now, got {len(rbv['buffer0'])}") + self.assertEqual(len(rbv["buffer1"]), 10, f"Expected to read back 10 points by now, got {len(rbv['buffer1'])}") + self.assertTrue(np.std(rbv["buffer0"])>5, f"Expected some noise on analog input, but std is: {np.std(rbv['buffer1'])}") + self.assertTrue(np.std(rbv["buffer1"])>5, f"Expected some noise on analog input, but std is: {np.std(rbv['buffer1'])}") + print("AX:DDC: Done testing DDC module!") - 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() - 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) - 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 test_14_JinjaTomcatSequenceScan(self): + return + # Load the test file + filename = "test/testSequenceScanTemplate.ascript" + with open(filename) as f: + templatetext = f.read() - 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 + import jinja2 + scanconfig = {'startpos': 42, 'scanrange': 180, 'nrepeat': 10, 'scandir': 'NegPos', 'scanvel': 90, 'scanacc': 500} + tm = jinja2.Template(templatetext) + text = tm.render(scan=scanconfig) + with open("test/tcatSequenceScan.ascript", "w") as text_file: + text_file.write(text) - 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) + # Throws if IOC is not available or PVs are incorrect + dev = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk4 = aa1TaskState(AA1_IOC_NAME+ ":TASK:T4:", name="tsk4") + dev.wait_for_connection() + tsk4.wait_for_connection() + print("\nTOMCAT Sequeence scan (via Jinjad AeroScript)") - 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() + # Copy file to controller and run it + t_start = time() + dev.runScript("tcatSequenceScan.ascript", taskIndex=4, filetext=text) + sleep(0.5) + tsk4.complete().wait() + t_end = time() + t_elapsed = t_end - t_start + print(f"Elapsed scan time: {t_elapsed}") - - - -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). - """ - - # ######################################################################## - # General module status - 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) - - 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): - super().stage() - self._switch.set("Start", settle_time=0.5).wait() - status = Status(timeout=0.1, settle_time=settle_time).set_finished() - return status - - 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""" - status = DeviceStatus(self, settle_time=settle_time) - 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() + unittest.main() + + + + + + + + + + + + + + + + + + + + + From e44ca3d48090347a0918c223cf20d48809cb7333 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 27 Feb 2024 15:33:36 +0100 Subject: [PATCH 06/16] More cleanup with testing --- .../epics/devices/AerotechAutomation1.py | 1949 +++++++++-------- 1 file changed, 1093 insertions(+), 856 deletions(-) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index d683640..b2c4560 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -1,896 +1,1133 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Nov 3 15:54:08 2022 - -@author: mohacsi_i -""" -import os -os.environ["EPICS_CA_ADDR_LIST"]="129.129.144.255" - -from collections import OrderedDict +from ophyd import Device, Component, EpicsMotor, EpicsSignal, EpicsSignalRO, Kind, DerivedSignal +from ophyd.status import Status, SubscriptionStatus, StatusBase, DeviceStatus +from ophyd.flyers import FlyerInterface +from time import sleep import warnings - import numpy as np -from time import sleep, time -import unittest -from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO +import time -try: - from AerotechAutomation1 import (aa1Controller, aa1Tasks, aa1TaskState, aa1DataAcquisition, - aa1GlobalVariables, aa1AxisIo, aa1AxisDriveDataCollection, - aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisPsoWindow) - from AerotechAutomation1Enums import AxisDataSignal, SystemDataSignal, DriveDataCaptureInput, DriveDataCaptureTrigger -except Exception: - from .AerotechAutomation1 import (aa1Controller, aa1Tasks, aa1TaskState, aa1DataAcquisition, - aa1GlobalVariables, aa1AxisIo, aa1AxisDriveDataCollection, - aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisPsoWindow) - from .AerotechAutomation1Enums import AxisDataSignal, SystemDataSignal, DriveDataCaptureInput, DriveDataCaptureTrigger +from AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, + AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, + TaskDataSignal, SystemDataSignal, TomcatSequencerState) + +from AerotechAutomation1Enums import * +from typing import Union +from collections import OrderedDict - -AA1_IOC_NAME = "X02DA-ES1-SMP1" -AA1_AXIS_NAME = "ROTY" +#class EpicsMotorX(EpicsMotor): +# pass -class AerotechAutomation1Test(unittest.TestCase): - _isConnected = False +class EpicsMotorX(EpicsMotor): + SUB_PROGRESS = "progress" - @classmethod - def setUpClass(cls): - try: - # HIL testing only if there's hardware - dev = aa1Controller(AA1_IOC_NAME+":CTRL:", name="aa1") - dev.wait_for_connection() - cls._isConnected = True - print("\n") - except Exception as ex: - print(ex) + 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'] + if "position" in d: + self._targetPosition = d['position'] + #super().configure(d) + + def kickoff(self): + self._startPosition = float( self.position) + return self.move(self._targetPosition, wait=False) + + 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 EpicsSignalPassive(Device): + value = Component(EpicsSignalRO, "", kind=Kind.omitted) + proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True) + def get(self): + self.proc.set(1).wait() + return self.value.get() + + +class EpicsPassiveRO(EpicsSignalRO): + """Special helper class to work around a bug in ophyd (caproto backend) + that reads CHAR array strigs as uint16 arrays. + """ - def test_00_Instantiation(self): - """ Test instantiation and interfaces + 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) - This method tests the more abstract bluesky compatibility of the - aerotech ophyd devices. In particular it checks the existence of the - required schema functions (that are not called in other tests). - """ - # Throws if IOC is not available or PVs are incorrect - def classTestSequence(DeviceClass, prefix, config=None, setable=False, set_value=None, flyable=False): - dut = DeviceClass(prefix, name="dut") - dut.wait_for_connection() - - # Readable device (required) - dut.describe() # Describes read() - dut.read() # Read the device active values - dut.trigger() - dut.describe_configuration() # Describes read_configuration() - dut.read_configuration() # Reads device configuration - if config is not None: - try: - ret = dut.configure(config) - self.assertTrue(type(ret)==tuple, f"On device class {DeviceClass}: configure is expected to return a tuple") - self.assertTrue(isinstance(ret[0], OrderedDict), f"On device class {DeviceClass}: old configuration is expected to be an OrderedDict") - self.assertTrue(isinstance(ret[1], (dict, OrderedDict)), f"On device class {DeviceClass}: new configuration is expected to be dict or OrderedDict") - except Exception: - pass - # Readable device (optional) - dut.stage() - dut.unstage() - # Setable device - if setable and set_value is not None: - try: - st = dut.set(set_value) - #dut.stop() - dut.check_value(set_value) - dut.position - except Exception as ex: - warnings.warn(f"Exception occured while testing setable interface: {ex}") - if flyable: - try: - dut.configure(config) - st = dut.kickoff() - st = dut.complete() - ret = dut.describe_collect() - ret = dut.collect() - except Exception: - pass + def wait_for_connection(self, *args, **kwargs): + super().wait_for_connection(*args, **kwargs) + self._proc.wait_for_connection(*args, **kwargs) - classDBase = [ - (aa1Controller, f"{AA1_IOC_NAME}:CTRL:"), - (aa1Tasks, f"{AA1_IOC_NAME}:TASK:"), - (aa1DataAcquisition, f"{AA1_IOC_NAME}:DAQ:"), - (aa1GlobalVariables, f"{AA1_IOC_NAME}:VAR:"), - (aa1GlobalVariableBindings, f"{AA1_IOC_NAME}:VAR:"), - - (EpicsMotor, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}"), - (aa1AxisIo, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:IO:"), - (aa1AxisDriveDataCollection, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:DDC:"), - (aa1AxisPsoDistance, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:PSO:"), - (aa1AxisPsoWindow, f"{AA1_IOC_NAME}:{AA1_AXIS_NAME}:PSO:"), - ] - - print("BSKY: Testing basic Ophyd device class interfaces") - for DeviceClass, prefix in classDBase: - classTestSequence(DeviceClass, prefix) - print("BSKY: Done testing CTRL module!") - + 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 - def test_01_ControllerProxy(self): - # Throws if IOC is not available or PVs are incorrect - dev = aa1Controller(AA1_IOC_NAME+":CTRL:", name="aa1") - dev.wait_for_connection() + 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. - print("CTRL: Testing basic controller readback") - # Check if controller info was loaded correctly - self.assertTrue(len(dev.controllername.get())>0, "Controller name seems invalid") - self.assertTrue(len(dev.serialnumber.get())>0, "Controller serial number seems invalid") - self.assertTrue(dev.axiscount.get()>0, "Maximum controller axis count seems invalid") - self.assertTrue(dev.taskcount.get()>0, "Maximum controller task count seems invalid") - print("CTRL: Done testing CTRL module!") + Execute 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. + """ + _failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) + errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) + warnStatus = Component(EpicsSignalRO, "WARNW", 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._currentTaskMonitor = None + self._textToExecute = None + self._isConfigured = False + self._isStepConfig = False + + 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 test_02_TaskProxy(self): - # Throws if IOC is not available or PVs are incorrect - dev = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") - dev.wait_for_connection() + 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""" - print("\nTASK: Testing the 'execute' command (direct string execution)") - # Attempt to run a short test script using the Execute interface (real) - text = "$rreturn[0]=3.141592" - rbv = dev.execute(text, mode="Double", taskIndex=3) - - self.assertTrue(dev._executeMode.get() in ["Double", 3], f"Expecting double return type for execution, but got: {dev._executeMode.get()}") - self.assertTrue(dev.taskIndex.get()==3, "Execution must happen on task 3") - self.assertTrue(rbv=="Result (double): 3.141592", f"Unexpected reply from execution: {rbv}") - # Attempt to run a short test script using the Execute interface (integer) - text = "$ireturn[0]=42" - rbv = dev.execute(text, mode="Int", taskIndex=4) - self.assertTrue(dev.taskIndex.get()==4, "Execution must happen on task 1") - self.assertTrue(rbv=="Result (int): 42", f"Unexpected reply from execution: {rbv}") - - print("TASK: Testing the 'file' interface") - # Write and read back a file - filename = "unittesting.ascript" - text = "program\nvar $positions[2] as real\nend" - if len(text)>40: - print(f"\tWARN: Program text is too long for EPICS string: {len(text)}") - - dev.writeFile(filename, text) - contents = dev.readFile(filename) - self.assertTrue(contents==text, "File contents changed on readback") - - print("TASK: Testing the 'run' command (run an existing file)") - # Send another empty program - filename = "unittesting2.ascript" - text = "program\nvar $positions[2] as real\nend" - dev.runScript(filename, taskIndex=3, filetext=text) - - print("TASK: Testing bluesky interface with free text)") - dev.configure({'text': "$rreturn[0]=137.036", 'taskIndex': 4, 'mode': "Double"}) - dev.kickoff().wait() - dev.complete().wait() - rbv = dev._executeReply.get() - self.assertTrue(dev._executeMode.get() in ["Double", 3], f"Expecting double return type for execution, but got: {dev._executeMode.get()}") - self.assertTrue(dev.taskIndex.get()==4, "Execution must happen on task 4") - self.assertTrue(rbv=="Result (double): 137.036000", f"Unexpected reply from execution: {rbv}") - print("TASK: Done testing TASK module!") - - - def test_03_DataCollectionProxy(self): - """ This tests the controller data collection - - Test for testing the controller data collection that occurs at a - fixed frequency. This interface is not so important if there's - hardware synchronization. - """ - return - # Throws if IOC is not available or PVs are incorrect - dev = aa1DataAcquisition(AA1_IOC_NAME+":DAQ:", name="daq") - dev.wait_for_connection() - - print("\nDAQ: Testing the controller configuration") - self.assertTrue(dev.status.get()==0, "DAQ must be idle to start the tests") - self.assertTrue(dev.points_max.get()>30000, "The test requires at least 30000 point buffer") - - ###################################################################### - # Test configuration 1 - print("DAQ: Running a short test run") - dev.startConfig(3000, "1kHz") - dev.addSystemSignal(SystemDataSignal.DataCollectionSampleTime) - dev.addAxisSignal(0, AxisDataSignal.PositionFeedback) - self.assertTrue(dev.signal_num.get()==2, "Expected 2 data sources") - # Run the data acquisition (run blocks and returns array) - _ = dev.run() - # Check post-run parameter values - self.assertTrue(dev.points_total.get()==3000, "Expected to have 3k points") - self.assertTrue(dev.points_collected.get()==3000, "Expected to record 3k points, did {dev.points_collected.value}") - - print("DAQ: Checking the returned array") - arr = dev.dataReadBack() - self.assertTrue(arr.size==6000, f"Unexpected data array size {arr.size}") - self.assertTrue(arr.shape==(3000,2), f"Unexpected data array shape {arr.size}") - - ###################################################################### - # Test manual configuration 2 - print("DAQ: Running a short test run") - dev.startConfig(2800, "1kHz") - dev.addSystemSignal(SystemDataSignal.DataCollectionSampleTime) - dev.addAxisSignal(0, AxisDataSignal.PositionFeedback) - dev.addAxisSignal(0, AxisDataSignal.VelocityFeedback) - self.assertTrue(dev.signal_num.get()==3, "Expected 3 data sources") - # Run the data acquisition (run blocks and returns array) - _ = dev.run() - # Check post-run parameter values - self.assertTrue(dev.points_total.get()==2800, f"Expected to have 2.8k points, got {dev.points_total.get()}") - self.assertTrue(dev.points_collected.get()==2800, f"Expected to record 2.8k points, got {dev.points_collected.get()}") - - print("DAQ: Checking the returned array") - arr = dev.dataReadBack() - self.assertTrue(arr.size==8400, f"Unexpected data array size {arr.size}") - self.assertTrue(arr.shape==(2800,3), f"Unexpected data array shape {arr.size}") - print("DAQ: Done testing DAQ module!") - - - def test_04_GlobalVariables1(self): - """Test the basic read/write global variable interface - """ - # Throws if IOC is not available or PVs are incorrect - dev = aa1GlobalVariables(AA1_IOC_NAME+":VAR:", name="gvar") - polled = aa1GlobalVariableBindings(AA1_IOC_NAME+":VAR:", name="pvar") - dev.wait_for_connection() - polled.wait_for_connection() - - print("\nVAR: Checking available memory") - self.assertTrue(dev.num_int.get()>=32, "At least 32 integer variables are needed for this test") - self.assertTrue(dev.num_real.get()>=32, "At least 32 real variables are needed for this test") - self.assertTrue(dev.num_string.get()>=32, "At least 32 string[256] variables are needed for this test") - - print("VAR: Setting and checking a few variables") - # Use random variables for subsequent runs - val_f = 100*np.random.random() - dev.writeFloat(11, val_f) - rb_f = dev.readFloat(11) - self.assertEqual(val_f, rb_f, f"Floating point readback value changed, read {rb_f} instead of {val_f}") - rb_f = dev.readFloat(10, 4) - self.assertEqual(val_f, rb_f[1], f"Floating point array readback value changed, read {rb_f[1]} instead of {val_f}") - - val_i = 1+int(100*np.random.random()) - dev.writeInt(13, val_i) - rb_i = dev.readInt(13) - self.assertEqual(val_i, rb_i, f"Integer readback value changed, read {rb_i} instead of {val_i}") - rb_i = dev.readInt(10, 4) - self.assertEqual(val_i, rb_i[3], f"Integer array readback value changed, read {rb_i[3]} instead of {val_i}") - - val_i =1+int(100*np.random.random()) - dev.writeInt(13, val_i) - rb_i = dev.readInt(13) - self.assertEqual(val_i, rb_i, f"Integer readback value changed, read {rb_i} instead of {val_i}") - rb_i = dev.readInt(10, 4) - self.assertEqual(val_i, rb_i[3], f"Integer array readback value changed, read {rb_i[3]} instead of {val_i}") - - val_s = np.random.choice(['f', 'o', 'o', 'b', 'a', 'r'], 6) - val_s = ''.join(val_s) - dev.writeString(7, val_s) - rb_s = dev.readString(7) - self.assertEqual(val_s, rb_s, f"String readback value changed, read {rb_s} instead of {val_s}") - print("VAR: Done testing VAR module basic functionality!") - - - def test_05_GlobalVariables2(self): - """Test the direct global variable bindings together with the task and - standard global variable interfaces. - """ - - # Throws if IOC is not available or PVs are incorrect - var = aa1GlobalVariables(AA1_IOC_NAME+":VAR:", name="gvar") - var.wait_for_connection() - dev = aa1GlobalVariableBindings(AA1_IOC_NAME+":VAR:", name="var") - dev.wait_for_connection() - tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") - tsk.wait_for_connection() - - print("\nVAR: Checking the direct global variable bindings") - # Use random variables for subsequent runs - val_f = 100*np.random.random() - dev.float16.set(val_f, settle_time=0.91).wait() # settle time seems to be ignored by ophyd - - rb_f = dev.float16.value - self.assertEqual(val_f, dev.float16.value, f"Floating point readback value changed, read {rb_f}, expected {val_f}") - #rb_f = dev.readFloat(10, 4) - #self.assertEqual(val_f, rb_f[1], f"Floating point array readback value changed, read {rb_f[1]} instead of {val_f}") - - val_i = 1+int(100*np.random.random()) - dev.int8.set(val_i, settle_time=0.91).wait() - rb_i = dev.int8.value - self.assertEqual(val_i, dev.int8.value, f"Integer readback value changed, read {rb_i}, expected {val_i}") - #rb_i = dev.readInt(10, 4) - #self.assertEqual(val_i, rb_i[3], f"Integer array readback value changed, read {rb_i[3]} instead of {val_i}") - - val_s = np.random.choice(['f', 'o', 'o', 'b', 'a', 'r'], 6) - val_s = ''.join(val_s) - dev.str4.set(val_s, settle_time=0.91).wait() - rb_s = dev.str4.value - self.assertEqual(val_s, rb_s, f"String readback value changed, read {rb_s} instead of {val_s}") - print("VAR: Done testing VAR module direct bindings!") - + self.configure({'text': filetext, 'filename': filename, 'taskIndex': taskIndex}) + print("Runscript configured") + self.trigger().wait() + print("Runscript waited") - def test_06_MotorRecord(self): - """ Tests the basic move functionality of the MotorRecord - """ + def execute(self, text: str, taskIndex: int=3, mode: str=0, settle_time=0.5): + """ Run a short text command on the Automation1 controller""" - mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") - mot.wait_for_connection() - print("\nMR: Checking the standard EPICS motor record") - - # Four test with absolute moves - dist = 30 + 50.0 * np.random.random() - target = mot.position + dist - print(f"Absolute move to {target}, distance: {dist}") - mot.move(target, wait=True) - self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (1)") - - dist = 30 + 50.0 * np.random.random() - target = mot.position - dist - print(f"Absolute move to {target}, distance: {dist}") - mot.move(target, wait=True) - self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (2)") - - dist = 30 + 50.0 * np.random.random() - target = mot.position+dist - print(f"Absolute move to {target}, distance: {dist}") - mot.move(target, wait=True) - self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (3)") - - dist = 30 + 50.0 * np.random.random() - target = mot.position - dist - print(f"Absolute move to {target}, distance: {dist}") - mot.move(target, wait=True) - self.assertTrue(np.abs(target-mot.position)<0.1, f"Final position {mot.position} is too far from target {target} (4)") - print("MR: Done testing MotorRecord!") - - - def test_07_AxisIo(self): - # Throws if IOC is not available or PVs are incorrect - dev = aa1AxisIo(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":IO:", name="io") - dev.wait_for_connection() - dev.polllvl.set(1, settle_time=0.5).wait() - print("\nAX:IO: Checking axis IO") + print(f"Executing program on task: {taskIndex}") + self.configure({'text': text, 'taskIndex': taskIndex, 'mode': mode}) + self.kickoff().wait() - # Writing random values to analog/digital outputs - for ii in range(5): - val_f = np.random.random() - dev.setAnalog(0, val_f) - self.assertAlmostEqual(dev.ao.get(), val_f, 2, "Unexpected readback on analog output") - print("Digital") - for ii in range(5): - val_b = round(np.random.random()) - dev.setDigital(0, val_b) - self.assertEqual(dev.do.get(), val_b, "Unexpected readback on digital output") - print("IO: Done testing IO module!") - - - def test_08_AxisDdc(self): - """ Drive data collection - - This is the preliminary test case dor drive data collection using - internal PSO based triggers. It tests basic functionality, like - configuration, state monitoring and readback. - """ - # Throws if IOC is not available or PVs are incorrect - dev = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") - event = EpicsSignal(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:EVENT:SINGLE", put_complete=True) - dev.wait_for_connection() - event.wait_for_connection() - print("\nAX:DDC: Checking axis DDC") - - # Stop any running acquisition - dev.unstage(settle_time=0.1) - - print("AX:DDC: Running a partial acquisition (1/3)") - # Configure the DDC for 42 points and start waiting for triggers - dev.configure({'npoints': 42, 'trigger': DriveDataCaptureTrigger.PsoEvent}) - dev.kickoff().wait() - # Fire 12 triggers and stop the acquisition early - for ii in range(12): - event.set(1, settle_time=0.02).wait() - # Stop the data acquisition early - dev.unstage() - - # Wait for polling to catch up - sleep(1) - - # Do the readback and check final state - rbv = yield from dev.collect() - self.assertEqual(dev.nsamples_rbv.value, 12, f"Expected to collect 12 points by now, got {dev.nsamples_rbv.value}") - self.assertEqual(len(rbv["buffer0"]), 12, f"Expected to read back 12 points by now, got {len(rbv['buffer0'])}") - self.assertEqual(len(rbv["buffer1"]), 12, f"Expected to read back 12 points by now, got {len(rbv['buffer1'])}") - - - print("AX:DDC: Running an overruning acquisition (2/3)") - # Configure the DDC for 42 points and start waiting for triggers - dev.configure(16, trigger=DriveDataCaptureTrigger.PsoEvent) - dev.stage() - num = dev.nsamples_rbv.get() - self.assertEqual(num, 0, f"Stage should reset the number of DDC points but got: {num}") - - # Fire 20 triggers and stop the acquisition early - for ii in range(20): - event.set(1, settle_time=0.02).wait() - dev.unstage() - - # Wait for polling to catch up - sleep(1) - - # Do the readback and check final state - rbv = dev.collect() - self.assertEqual(dev.nsamples_rbv.value, 16, f"Expected to collect 16 points by now, got {dev.nsamples_rbv.value}") - self.assertEqual(len(rbv["buffer0"]), 16, f"Expected to read back 16 points by now, got {len(rbv['buffer0'])}") - self.assertEqual(len(rbv["buffer1"]), 16, f"Expected to read back 16 points by now, got {len(rbv['buffer1'])}") - - - print("AX:DDC: Trying to record some noise on the analog input (3/3)") - dev.configure(36, trigger=DriveDataCaptureTrigger.PsoEvent, source1=DriveDataCaptureInput.AnalogInput0) - dev.stage() - num = dev.nsamples_rbv.get() - self.assertEqual(num, 0, f"Stage should reset the number of DDC points but got: {num}") - # Fire 36 triggers - for ii in range(36): - event.set(1, settle_time=0.02).wait() - dev.unstage() - - # Wait for polling to catch up - sleep(1) - - # Do the readback and check final state - rbv = dev.collect() - self.assertEqual(dev.nsamples_rbv.value, 36, f"Expected to collect 12 points by now, got {dev.nsamples_rbv.value}") - self.assertEqual(len(rbv["buffer0"]), 36, f"Expected to read back 36 points by now, got {len(rbv['buffer0'])}") - self.assertEqual(len(rbv["buffer1"]), 36, f"Expected to read back 36 points by now, got {len(rbv['buffer1'])}") - self.assertTrue(np.std(rbv["buffer1"])>0.000001, f"Expected some noise on analog input, but std is: {np.std(rbv['buffer1'])}") - print("AX:DDC: Done testing DDC module!") - - - def test_09_AxisPsoDistance1(self): - """ Test basic PSO distance mode functionality - - This module tests basic PSO distance functionality, like events and - waveform generation. The goal is to ensure the basic operation of - the PSO module and the polled feedback. - """ - # Throws if IOC is not available or PVs are incorrect - dev = aa1AxisPsoDistance(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") - ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") - mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") - mot.wait_for_connection() - dev.wait_for_connection() - ddc.wait_for_connection() - print("\nAX:PSO: Checking axis PSO in distance mode (basic)") - if dev.output.value: - dev.toggle() - sleep(1) - - print("AX:PSO: Directly checking basic 'toggled' event generation (1/3)") - # Configure steps and move to middle of range - dev.configure({'distance': 5, 'wmode': "toggle"}) - mot.move(mot.position + 2.5) - dev.stage() - # Start moving in steps, while checking output - for ii in range(10): - last_pso = dev.output.value - mot.move(mot.position + 5) - sleep(1) - self.assertTrue(dev.output.value != last_pso, f"Expected to toggle the PSO output at step {ii}, got {dev.output.value}") - dev.unstage() - ddc.unstage() - - print("AX:PSO: Checking basic 'toggled' event generation with DDC (2/3)") - # Configure steps and move to middle of range - dev.configure({'distance': 2.0, 'wmode': "toggle"}) - dev.stage() - - # Configure DDC - npoints = 720 / 2.0 - ddc.configure({'npoints': npoints+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) - ddc.stage() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") - self.assertTrue(dev.output.value==0, "Expected to start from LOW state") - - # Start moving and wait for polling - mot.move(mot.position + 720) - sleep(0.5) - - # Evaluate results - npoints_rbv = ddc.nsamples_rbv.value - self.assertTrue(np.abs(npoints_rbv-720/4)<=1, f"Expected to record {720/4} points but got {npoints_rbv}") - dev.unstage() - ddc.unstage() - - - print("AX:PSO: Checking basic 'pulsed' event generation with DDC (3/5)") - # Configure steps and move to middle of range - dev.configure({'distance': 1.8, 'wmode': "pulsed", 'n_pulse': 3}) - dev.kickoff().wait() - - # Configure DDC - npoints = 3 * 720 / 1.8 - ddc.configure({'npoints': npoints+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) - ddc.stage() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") - self.assertTrue(dev.output.value==0, "Expected to start from LOW state") - - # Start moving and wait for polling - mot.move(mot.position + 720.9) - sleep(0.5) - - # Evaluate results - npoints_rbv = ddc.nsamples_rbv.value - self.assertTrue(np.abs(npoints_rbv-3*720/1.8)<=1, f"Expected to record {3*720/1.8} points but got {npoints_rbv}") - print("AX:PSO: Done testing basic PSO distance functionality!") - dev.unstage() - ddc.unstage() - - - def test_10_PsoIntegration01(self): - """ The first integration test aims to verify PSO functionality under - real life scenarios. It uses the DDC for collecting encoder signals - and compares them to the expected values. - """ - # Throws if IOC is not available or PVs are incorrect - dev = aa1AxisPsoDistance(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") - ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") - mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") - mot.wait_for_connection() - dev.wait_for_connection() - ddc.wait_for_connection() - print("\nAX:PSO: Integration test for vector style PSO triggering (advanced)") - - print("AX:PSO: Testing standard 'enable' style PSO triggering with DDC (1/3)") - # This is one line of the Tomcat sequence scan! - acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value - vec_dist = [acc_dist, 18, 0.01, 18, 0.01, 18, 0.01, 18, 0.01, 18, 0.01, 18] - dev.configure({'distance': vec_dist, 'wmode': "toggle"}) - if dev.output.value: - dev.toggle() - dev.stage() - - # Configure DDC - npoints_exp = len(vec_dist)/2 - ddc.configure({'npoints': npoints_exp+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) - ddc.stage() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") - self.assertTrue(dev.output.value==0, "Expected to start from LOW state") - - # Start moving and wait for polling - mot.move(mot.position + np.sum(vec_dist) + 10) - sleep(0.9) - - # Evaluate results - npoints_rbv = ddc.nsamples_rbv.value - self.assertTrue(np.abs(npoints_rbv-npoints_exp)<1, f"Expected to record {npoints_exp} points but got {npoints_rbv}") - self.assertTrue(dev.output.value==0, "Expected to finish in LOW state") - self.assertTrue(dev.dstArrayDepleted.value, "Expected to cover all pints in the distance array") - dev.unstage() - ddc.unstage() - - print("AX:PSO: Checking 'individual' style trigger signal with DDC (2/5)") - # This is practically golden ratio tomography! - acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value - vec_dist = [acc_dist] - vec_dist.extend([1.618]* 100) - dev.configure({'distance': vec_dist, 'wmode': "pulsed"}) - dev.stage() - - # Configure DDC - npoints_exp = len(vec_dist) - ddc.configure({'npoints': npoints_exp+100, 'trigger': DriveDataCaptureTrigger.PsoOutput}) - ddc.stage() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") - self.assertTrue(dev.output.value==0, "Expected to start from LOW state") - - # Start moving and wait for polling (move negative direction) - mot.move(mot.position - np.sum(vec_dist) - acc_dist) - sleep(0.9) - - # Evaluate results - npoints_rbv = ddc.nsamples_rbv.value - self.assertTrue(np.abs(npoints_rbv-npoints_exp)<1, f"Expected to record {npoints_exp} points but got {npoints_rbv}") - self.assertTrue(dev.output.value==0, "Expected to finish in LOW state") - self.assertTrue(dev.dstArrayDepleted.value, "Expected to cover all pints in the distance array") - print("AX:PSO: Done testing PSO module in distance mode!") - dev.unstage() - ddc.unstage() - - - def test_11_AxisPsoWindow1(self): - """ Test basic PSO window mode functionality - - This module tests basic PSO distance functionality, like events and - waveform generation. The goal is to ensure the basic operation of - the PSO module and the polled feedback. - """ - return - # Throws if IOC is not available or PVs are incorrect - dev = aa1AxisPsoWindow(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") - ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") - mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") - mot.wait_for_connection() - dev.wait_for_connection() - ddc.wait_for_connection() - print("\nAX:PSO: Checking axis PSO in window mode (basic)") - if dev.output.value: - dev.toggle() - - print("AX:PSO: Directly checking basic window output (1/3)") - # Configure steps and move to middle of range - dev.configure((5, 13), "output") - dev.stage() - sleep(1) - # Move half a step - mot.move(mot.position + 0.1) - dist = 0.1 - - # Start moving in steps, while checking output - for ii in range(42): - mot.move(mot.position + 0.2) - dist += 0.2 - sleep(1) - if 5 < dist < 13: - self.assertTrue(dev.output.value==1, f"Expected HIGH PSO output inside the window at distance {dist}") - else: - self.assertTrue(dev.output.value==0, f"Expected LOW PSO output outside the window at distance {dist}") - - print("AX:PSO: The following tests whould fail as multiple triggers/events are being emmitted when entering/exiting the window!") - dev.unstage() - return - - print("AX:PSO: Checking basic window event generation with DDC (2/3)") - print(" WARN: The encoder output is very noisy, one transition generates several events") - # Move to acceleration position - acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value - print(f"Acceleration distance: {acc_dist}") - start_position = mot.position - mot.move(start_position - acc_dist) - # Configure PSO in window mode - dev.configure((acc_dist, 30 + acc_dist), "pulsed", "Both", n_pulse=1) - # Configure the data capture (must be performed in start position) - ddc.configure(5*10, trigger=DriveDataCaptureTrigger.PsoOutput) - ddc.stage() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") - # Repeated line scan - for ii in range(10): - dev.stage() - mot.move(start_position + 30 + acc_dist) - dev.unstage() - mot.move(start_position - acc_dist) - sleep(0.5) - npoints_rbv = ddc.nsamples_rbv.value - self.assertTrue(np.abs(npoints_rbv-50)<1, f"Expected to record 10 points but got {npoints_rbv}") - - - print("AX:PSO: Checking basic 'toggled' event generation with DDC (3/3)") - print("Test removed as there's some heavy jitter on the PSO output flag") - # Move to acceleration position - acc_dist = 0.5 * mot.velocity.value * mot.acceleration.value - print(f"Acceleration distance: {acc_dist}") - mot.move(mot.position - acc_dist) - # Configure PSO in window mode - dev.configure((acc_dist, 30 + acc_dist + 1), "output") - dev.stage() - # Configure the data capture - ddc.configure(10, trigger=DriveDataCaptureTrigger.PsoOutput) - ddc.stage() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 0, f"DDC.stage should reset the number of DDC points but got: {npoints_rbv}") - # Snake scan - for ii in range(10): - if ii % 2 == 0: - mot.move(mot.position + 30+ 2*acc_dist + 0.1) - else: - mot.move(mot.position - 30 - 2*acc_dist - 0.1) - sleep(0.5) - npoints_rbv = ddc.nsamples_rbv.value - self.assertTrue(np.abs(npoints_rbv-10)<1, f"Expected to record 10 points but got {npoints_rbv}") - - - - def test_12_TomcatSequenceScan(self): - """ Test the zig-zag sequence scan from Tomcat (via BEC) - - This module tests basic PSO distance functionality, like events and - waveform generation. The goal is to ensure the basic operation of - the PSO module and the polled feedback. - """ - # Throws if IOC is not available or PVs are incorrect - pso = aa1AxisPsoDistance(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":PSO:", name="pso1") - ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") - mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") - mot.wait_for_connection() - pso.wait_for_connection() - ddc.wait_for_connection() - t_start = time() - print("\nTOMCAT Sequeence scan (via Ophyd)") - - ScanStart = 42.0 - ScanRange = 180 - NumRepeat = 10 - ScanType = "PosNeg" - - # Dynamic properties - VelScan = 90 - AccScan = 500 - SafeDist = 10.0 - - ###################################################################### - print("\tConfigure") - # Scan range - AccDist = 0.5 * VelScan * VelScan / AccScan + SafeDist - if ScanType in ["PosNeg", "Pos"]: - PosStart = ScanStart - AccDist - PosEnd = ScanStart + ScanRange + AccDist - elif ScanType in ["NegPos", "Neg"]: - PosStart = ScanStart + AccDist - PosEnd = ScanStart - ScanRange - AccDist + if mode in [0, "None", None]: + return None else: - raise RuntimeError(f"Unexpected ScanType: {ScanType}") + raw = self._executeReply.get() + return raw - # Motor setup - mot.velocity.set(VelScan).wait() - mot.acceleration.set(VelScan/AccScan).wait() - mot.move(PosStart) - self.assertTrue(np.abs(mot.position-PosStart)<0.1, f"Expected to move to scan start position {PosStart}, found motor at {mot.position}") - - # PSO setup - print(f"Configuring PSO to: {[AccDist, ScanRange]}") - pso.configure({'distance': [AccDist, ScanRange], "wmode": "toggle"}) - - # DDC setup (Tomcat runs in ENABLE mode, so only one posedge) - ddc.configure({'npoints': NumRepeat}) - - print("\tStage") - mot.stage() - pso.stage() - ddc.stage() - - for ii in range(NumRepeat): - # No option to reset the index counter... - pso.dstArrayRearm.set(1).wait() - - if ScanType in ["Pos", "Neg"]: - mot.move(PosEnd) - self.assertTrue(np.abs(mot.position-PosEnd)<1.0, f"Expected to reach {PosEnd}, motor is at {mot.position} on iteration {ii}") - mot.move(PosStart) - self.assertTrue(np.abs(mot.position-PosStart)<1.0, f"Expected to rewind to {PosStart}, motor is at {mot.position} on iteration {ii}") - - if ScanType in ["PosNeg", "NegPos"]: - if ii % 2 == 0: - mot.move(PosEnd) - self.assertTrue(np.abs(mot.position-PosEnd)<1.0, f"Expected to reach {PosEnd}, motor is at {mot.position} on iteration {ii}") - else: - mot.move(PosStart) - self.assertAlmostEqual(mot.position, PosStart, 1, f"Expected to reach {PosStart}, motor is at {mot.position} on iteration {ii}") - - sleep(0.5) - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, ii+1, f"Expected to record {ii+1} DDC points, but got: {npoints_rbv} on iteration {ii}") - - print("\tUnstage") - mot.unstage() - ddc.unstage() - pso.unstage() - - t_end = time() - t_elapsed = t_end - t_start - print(f"Elapsed scan time: {t_elapsed}") - - print("Evaluate final state") - sleep(3) - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, NumRepeat, f"Expected to record {NumRepeat} DDC points, but got: {npoints_rbv}") - - - def test_13_TomcatSequenceScan(self): - """ Test the zig-zag sequence scan from Tomcat (via AeroScript) - - This module tests basic PSO distance functionality, like events and - waveform generation. The goal is to ensure the basic operation of - the PSO module and the polled feedback. + def configure(self, d: dict={}) -> tuple: + """ Configuration interface for flying """ - # Throws if IOC is not available or PVs are incorrect - dev = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") - tsk4 = aa1TaskState(AA1_IOC_NAME+ ":TASK:T4:", name="tsk4") + # 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 - dev.wait_for_connection() - tsk4.wait_for_connection() - - t_start = time() - print("\nTOMCAT Sequeence scan (via AeroScript)") + # 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._currentTaskMonitor = aa1TaskState() + + # 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") + self._isConfigured = True + new = self.read_configuration() + return (old, new) + + ########################################################################## + # Bluesky stepper interface + def stage(self) -> None: + """ Default staging """ + super().stage() - filename = "test/testScan02.ascript" - with open(filename) as f: - text = f.read() - # Copy file to controller and run it - dev.runScript("tcatZigZagSequence2.ascript", taskIndex=4, filetext=text) - sleep(0.5) - tsk4.complete().wait() + def unstage(self) -> None: + """ Default unstaging """ + super().unstage() - t_end = time() - t_elapsed = t_end - t_start - print(f"Elapsed scan time: {t_elapsed}") + 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""" + #return self._currentTaskMonitor.complete() + 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 {self.name: dd} - # Do the readback and check final state - ddc = aa1AxisDriveDataCollection(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME + ":DDC:", name="ddc") - ddc.wait_for_connection() - rbv = yield from ddc.collect() - npoints_rbv = ddc.nsamples_rbv.value - self.assertEqual(npoints_rbv, 10, f"Expected to collect 10 points by now, got {npoints_rbv}") - self.assertEqual(len(rbv["buffer0"]), 10, f"Expected to read back 10 points by now, got {len(rbv['buffer0'])}") - self.assertEqual(len(rbv["buffer1"]), 10, f"Expected to read back 10 points by now, got {len(rbv['buffer1'])}") - self.assertTrue(np.std(rbv["buffer0"])>5, f"Expected some noise on analog input, but std is: {np.std(rbv['buffer1'])}") - self.assertTrue(np.std(rbv["buffer1"])>5, f"Expected some noise on analog input, but std is: {np.std(rbv['buffer1'])}") - print("AX:DDC: Done testing DDC module!") + 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""" + # 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 + return result + + # Subscribe and wait for update + status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) + return status - def test_14_JinjaTomcatSequenceScan(self): - return - # Load the test file - filename = "test/testSequenceScanTemplate.ascript" - with open(filename) as f: - templatetext = f.read() + 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 - import jinja2 - scanconfig = {'startpos': 42, 'scanrange': 180, 'nrepeat': 10, 'scandir': 'NegPos', 'scanvel': 90, 'scanacc': 500} - tm = jinja2.Template(templatetext) - text = tm.render(scan=scanconfig) - with open("test/tcatSequenceScan.ascript", "w") as text_file: - text_file.write(text) + def collect(self) -> OrderedDict: + ret = OrderedDict() + ret["timestamps"] = {"success": time.time()} + ret["data"] = {"success": 1} + yield ret + - # Throws if IOC is not available or PVs are incorrect - dev = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") - tsk4 = aa1TaskState(AA1_IOC_NAME+ ":TASK:T4:", name="tsk4") - dev.wait_for_connection() - tsk4.wait_for_connection() - print("\nTOMCAT Sequeence scan (via Jinjad AeroScript)") +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() - # Copy file to controller and run it - t_start = time() - dev.runScript("tcatSequenceScan.ascript", taskIndex=4, filetext=text) - sleep(0.5) - tsk4.complete().wait() - t_end = time() - t_elapsed = t_end - t_start - print(f"Elapsed scan time: {t_elapsed}") + 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(EpicsSignalPassive, "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 + """ + # 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): + """ 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 + """ + 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 IO pin is polled simultaneously! + """ + 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) + 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 --> 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. + """ + + 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 + + # ######################################################################## + # 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() + return (old, new) + + # ######################################################################## + # Bluesky step scan interface + def stage(self, settle_time=None): + self.dstEventsEna.set("On").wait() + if hasattr(self, "_distanceValue") and isinstance(self._distanceValue, (np.ndarray, list, tuple)): + self.dstArrayRearm.set(1).wait() + self.dstCounterEna.set("On").wait() + if settle_time is not None: + sleep(settle_time) + return super().stage() + + 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() + 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) + 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). + """ + + # ######################################################################## + # General module status + 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) + + 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): + super().stage() + self._switch.set("Start", settle_time=0.5).wait() + status = Status(timeout=0.1, settle_time=settle_time).set_finished() + return status + + 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""" + status = DeviceStatus(self, settle_time=settle_time) + 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__": - unittest.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() From 1f3ef42a9de547c4f9eab6488e3fcd8dc42222e1 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Fri, 1 Mar 2024 16:54:13 +0100 Subject: [PATCH 07/16] Scans are still hanging... --- .../epics/devices/AerotechAutomation1.py | 89 +++++++++++++++---- 1 file changed, 73 insertions(+), 16 deletions(-) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index b2c4560..c21ff3b 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -6,11 +6,17 @@ import warnings import numpy as np import time -from AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, - AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, - TaskDataSignal, SystemDataSignal, TomcatSequencerState) +try: + from .AerotechAutomation1Enums import * + from .AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, + AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, + TaskDataSignal, SystemDataSignal, TomcatSequencerState) +except: + from AerotechAutomation1Enums import * + from AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, + AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, + TaskDataSignal, SystemDataSignal, TomcatSequencerState) -from AerotechAutomation1Enums import * from typing import Union from collections import OrderedDict @@ -31,14 +37,20 @@ class EpicsMotorX(EpicsMotor): def configure(self, d: dict): if "target" in d: self._targetPosition = d['target'] + del d['target'] if "position" in d: self._targetPosition = d['position'] - #super().configure(d) + 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): @@ -663,6 +675,7 @@ class aa1AxisPsoBase(Device): 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) @@ -730,12 +743,31 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): For a more detailed description of additional signals and masking plase refer to Automation1's online manual. """ + 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: @@ -800,18 +832,17 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): self.outSource.set("Waveform").wait() new = self.read_configuration() + print("PSO configured") return (old, new) # ######################################################################## # Bluesky step scan interface - def stage(self, settle_time=None): - self.dstEventsEna.set("On").wait() - if hasattr(self, "_distanceValue") and isinstance(self._distanceValue, (np.ndarray, list, tuple)): - self.dstArrayRearm.set(1).wait() - self.dstCounterEna.set("On").wait() - if settle_time is not None: - sleep(settle_time) - return super().stage() + 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() @@ -832,6 +863,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): self.dstCounterEna.set("On").wait() status = DeviceStatus(self) status.set_finished() + print("PSO kicked off") return status def complete(self) -> DeviceStatus: @@ -848,7 +880,10 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): return result # Subscribe and wait for update - status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) + #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) @@ -1007,6 +1042,7 @@ class aa1AxisDriveDataCollection(Device): # ######################################################################## # 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) @@ -1022,6 +1058,26 @@ class aa1AxisDriveDataCollection(Device): _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 @@ -1058,7 +1114,8 @@ class aa1AxisDriveDataCollection(Device): def complete(self, settle_time=0.1) -> DeviceStatus: """ DDC just reads back whatever is available in the buffers""" - status = DeviceStatus(self, settle_time=settle_time) + sleep(settle_time) + status = DeviceStatus(self) status.set_finished() return status From 16f033a8338be8df620622d298bb4ed425b8f2a2 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 5 Mar 2024 16:47:23 +0100 Subject: [PATCH 08/16] Added templated scan and required status updates --- .../epics/devices/AerotechAutomation1.py | 39 ++++++++- .../AerotechSnapAndStepTemplate.ascript | 87 +++++++++++++++++++ 2 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index c21ff3b..fc14f26 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -119,6 +119,7 @@ class aa1Tasks(Device): it and run it directly on a certain thread. But there's no way to retrieve the source code. """ + SUB_PROGRESS = "progress" _failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) warnStatus = Component(EpicsSignalRO, "WARNW", auto_monitor=True, kind=Kind.hinted) @@ -139,7 +140,23 @@ class aa1Tasks(Device): 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... @@ -198,7 +215,8 @@ class aa1Tasks(Device): old = self.read_configuration() self.taskIndex.set(taskIndex).wait() self._textToExecute = None - #self._currentTaskMonitor = aa1TaskState() + self._currentTaskMonitor = aa1TaskState(prefix=self.prefix+f"T{taskIndex}:", name="taskmon") + self._currentTaskMonitor.wait_for_connection() # Choose the right execution mode if (filename is None) and (text not in [None, ""]): @@ -270,11 +288,22 @@ class aa1Tasks(Device): def complete(self) -> DeviceStatus: """ Execute the script on the configured task""" + print("Called aa1Task.complete()") #return self._currentTaskMonitor.complete() - status = DeviceStatus(self) - status.set_finished() + #status = DeviceStatus(self) + #status.set_finished() + #return status + 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._currentTaskMonitor.status, notRunning, 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} @@ -300,12 +329,14 @@ class aa1TaskState(Device): 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 diff --git a/ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript b/ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript new file mode 100644 index 0000000..c7e01c8 --- /dev/null +++ b/ophyd_devices/epics/devices/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 + From 174e5fdb6120a7da70dabc42fb9ee66da612042b Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Wed, 6 Mar 2024 17:05:08 +0100 Subject: [PATCH 09/16] New complete with added IOC functionality --- .../epics/devices/AerotechAutomation1.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/AerotechAutomation1.py index fc14f26..de53e05 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/AerotechAutomation1.py @@ -123,6 +123,7 @@ class aa1Tasks(Device): _failure = Component(EpicsSignalRO, "FAILURE", 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) @@ -136,7 +137,7 @@ class aa1Tasks(Device): 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._currentTaskMonitor = None + self._currentTask = None self._textToExecute = None self._isConfigured = False self._isStepConfig = False @@ -215,8 +216,7 @@ class aa1Tasks(Device): old = self.read_configuration() self.taskIndex.set(taskIndex).wait() self._textToExecute = None - self._currentTaskMonitor = aa1TaskState(prefix=self.prefix+f"T{taskIndex}:", name="taskmon") - self._currentTaskMonitor.wait_for_connection() + self._currentTask = taskIndex # Choose the right execution mode if (filename is None) and (text not in [None, ""]): @@ -289,19 +289,17 @@ class aa1Tasks(Device): def complete(self) -> DeviceStatus: """ Execute the script on the configured task""" print("Called aa1Task.complete()") - #return self._currentTaskMonitor.complete() - #status = DeviceStatus(self) - #status.set_finished() - #return status timestamp_ = 0 - def notRunning(*args, old_value, value, timestamp, **kwargs): + taskIdx = int(self.taskIndex.get()) + def notRunning2(*args, old_value, value, timestamp, **kwargs): nonlocal timestamp_ - result = False if (timestamp_== 0) else (value not in ["Running", 4]) + result = False if value[taskIdx] in ["Running", 4] else True timestamp_ = timestamp print(result) - return result + return result + # Subscribe and wait for update - status = SubscriptionStatus(self._currentTaskMonitor.status, notRunning, settle_time=0.5) + status = SubscriptionStatus(self.taskStates, notRunning2, settle_time=0.5) return status def describe_collect(self) -> OrderedDict: From 3fd19f85bab3b4ad4b5e18a5eef8237e1d478653 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Thu, 7 Mar 2024 10:58:06 +0100 Subject: [PATCH 10/16] Added simplified scripted sequence scan --- .../AerotechSimpleSequenceTemplate.ascript | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript diff --git a/ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript b/ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript new file mode 100644 index 0000000..94d4caa --- /dev/null +++ b/ophyd_devices/epics/devices/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 + From 413de89a83e7311aada61e7fc51da1e9b62d5e7d Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Fri, 8 Mar 2024 14:20:11 +0100 Subject: [PATCH 11/16] Moved aerotech to its own directory --- ophyd_devices/epics/devices/__init__.py | 2 +- .../{ => aerotech}/AerotechAutomation1.py | 49 +++-- .../AerotechAutomation1Enums.py | 0 .../AerotechSimpleSequenceTemplate.ascript | 0 .../AerotechSnapAndStepTemplate.ascript | 0 .../epics/devices/aerotech/README.md | 167 ++++++++++++++++++ 6 files changed, 198 insertions(+), 20 deletions(-) rename ophyd_devices/epics/devices/{ => aerotech}/AerotechAutomation1.py (97%) rename ophyd_devices/epics/devices/{ => aerotech}/AerotechAutomation1Enums.py (100%) rename ophyd_devices/epics/devices/{ => aerotech}/AerotechSimpleSequenceTemplate.ascript (100%) rename ophyd_devices/epics/devices/{ => aerotech}/AerotechSnapAndStepTemplate.ascript (100%) create mode 100644 ophyd_devices/epics/devices/aerotech/README.md diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 62a5540..35aa147 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -29,6 +29,6 @@ from .eiger9m_csaxs import Eiger9McSAXS from .pilatus_csaxs import PilatuscSAXS from .falcon_csaxs import FalconcSAXS from .delay_generator_csaxs import DelayGeneratorcSAXS -from .AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX +from .aerotech.AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX # from .psi_detector_base import PSIDetectorBase, CustomDetectorMixin diff --git a/ophyd_devices/epics/devices/AerotechAutomation1.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py similarity index 97% rename from ophyd_devices/epics/devices/AerotechAutomation1.py rename to ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py index de53e05..7d9a7a4 100644 --- a/ophyd_devices/epics/devices/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py @@ -21,11 +21,10 @@ from typing import Union from collections import OrderedDict -#class EpicsMotorX(EpicsMotor): -# pass - class EpicsMotorX(EpicsMotor): + """ Special motor class that provides flyer interface and progress bar. + """ SUB_PROGRESS = "progress" def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): @@ -64,19 +63,9 @@ class EpicsMotorX(EpicsMotor): value=int(100*progress), max_value=max_value, done=int(np.isclose(max_value, progress, 1e-3)), ) -class EpicsSignalPassive(Device): - value = Component(EpicsSignalRO, "", kind=Kind.omitted) - proc = Component(EpicsSignal, ".PROC", kind=Kind.omitted, put_complete=True) - def get(self): - self.proc.set(1).wait() - return self.value.get() - - class EpicsPassiveRO(EpicsSignalRO): - """Special helper class to work around a bug in ophyd (caproto backend) - that reads CHAR array strigs as uint16 arrays. + """ Small helper class to read PVs that need to be processed first. """ - def __init__(self, read_pv, *, string=False, name=None, **kwargs): super().__init__(read_pv, string=string, name=name, **kwargs) self._proc = EpicsSignal(read_pv+".PROC", kind=Kind.omitted, put_complete=True) @@ -113,14 +102,33 @@ class aa1Tasks(Device): The place to manage tasks and AeroScript user files on the controller. You can read/write/compile/execute AeroScript files and also retrieve - saved data files from the controller. + saved data files from the controller. It will also work around an ophyd + bug that swallows failures. - Execute does not require to store the script in a file, it will compile + Execution does not require to store the script in a file, it will compile it and run it directly on a certain thread. But there's no way to retrieve the source code. + + Write a text into a file on the aerotech controller and execute it with kickoff. + ''' + script="var $axis as axis = ROTY\\nMoveAbsolute($axis, 42, 90)" + tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk.wait_for_connection() + tsk.configure({'text': script, 'filename': "foobar.ascript", 'taskIndex': 4}) + tsk.kickoff().wait() + ''' + + Just execute an ascript file already on the aerotech controller. + ''' + tsk = aa1Tasks(AA1_IOC_NAME+":TASK:", name="tsk") + tsk.wait_for_connection() + tsk.configure({'filename': "foobar.ascript", 'taskIndex': 4}) + tsk.kickoff().wait() + ''' + """ SUB_PROGRESS = "progress" - _failure = Component(EpicsSignalRO, "FAILURE", kind=Kind.hinted) + _failure = Component(EpicsSignalRO, "FAILURE", auto_monitor=True, kind=Kind.hinted) errStatus = Component(EpicsSignalRO, "ERRW", auto_monitor=True, kind=Kind.hinted) warnStatus = Component(EpicsSignalRO, "WARNW", auto_monitor=True, kind=Kind.hinted) taskStates = Component(EpicsSignalRO, "STATES-RBV", auto_monitor=True, kind=Kind.hinted) @@ -239,8 +247,11 @@ class aa1Tasks(Device): self._textToExecute = None else: raise RuntimeError("Unsupported filename-text combo") + + if self._failure.value: + raise RuntimeError("Failed to launch task, please check the Aerotech IOC") + self._isConfigured = True - new = self.read_configuration() return (old, new) @@ -455,7 +466,7 @@ class aa1DataAcquisition(Device): return data # DAQ data readback - data_rb = Component(EpicsSignalPassive, "DATA", kind=Kind.hinted) + data_rb = Component(EpicsPassiveRO, "DATA", kind=Kind.hinted) data_rows = Component(EpicsSignalRO, "DATA_ROWS", auto_monitor=True, kind=Kind.hinted) data_cols = Component(EpicsSignalRO, "DATA_COLS", auto_monitor=True, kind=Kind.hinted) data_stat = Component(EpicsSignalRO, "DATA_AVG", auto_monitor=True, kind=Kind.hinted) diff --git a/ophyd_devices/epics/devices/AerotechAutomation1Enums.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py similarity index 100% rename from ophyd_devices/epics/devices/AerotechAutomation1Enums.py rename to ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py diff --git a/ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript b/ophyd_devices/epics/devices/aerotech/AerotechSimpleSequenceTemplate.ascript similarity index 100% rename from ophyd_devices/epics/devices/AerotechSimpleSequenceTemplate.ascript rename to ophyd_devices/epics/devices/aerotech/AerotechSimpleSequenceTemplate.ascript diff --git a/ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript b/ophyd_devices/epics/devices/aerotech/AerotechSnapAndStepTemplate.ascript similarity index 100% rename from ophyd_devices/epics/devices/AerotechSnapAndStepTemplate.ascript rename to ophyd_devices/epics/devices/aerotech/AerotechSnapAndStepTemplate.ascript diff --git a/ophyd_devices/epics/devices/aerotech/README.md b/ophyd_devices/epics/devices/aerotech/README.md new file mode 100644 index 0000000..99283e4 --- /dev/null +++ b/ophyd_devices/epics/devices/aerotech/README.md @@ -0,0 +1,167 @@ + + + + + + + + +## Integration log for the Ophyd integration for the Aerotech Automation1 EPICS IOC + + +## Avoid the safespace API!!! + +The properly documented beamline scripting interface was meant for exernal users. Hence, it is idiotproof and only offers a very limited functionality, making it completely unsuitable for serious development. Anyhing more complicated should go through the undocumented scan API under 'bec/scan_server/scan_plugins'. The interface is a bit inconcvenient and there's no documentation, but there are sufficient code examples to get going, but it's quite picky about some small details (that are not documented). Note that the scan plugins will probably migrate to beamline repositories in the future. + +## Differences to vanilla Bluesky + +Unfortunately the BEC is not 100% compatible with Bluesky, thus some changes are also required from the ophyd layer. + +### Event model + +The BEC has it's own event model, that's different from vanilla Bluesky. Particularly every standard scan is framed between **stage --> ... --> complete --> unstage**. So: + + - **Bluesky stepper**: configure --> stage --> Nx(trigger+read) --> unstage + - **Bluesky flyer**: configure --> kickoff --> complete --> collect + - **BEC stepper**: stage --> configure --> Nx(trigger+ read) --> complete --> unstage + - **BEC flyer**: stage --> configure --> kickoff --> complete --> complete --> unstage + +What's more is that unless it's explicitly specified in the scan, **ALL** ophyd devices (even listeners) get staged and unstaged for every scan. This either makes device management mandatory or raises the need to explicitly prevent this in custom scans. + +### Scan server hangs + +Unfortunately a common behavior. + +### Class + +The DeviceServer's instantiates the ophyd devices from a dictionary. Therefore all arguments of '__init__(self, ...)' must be explicity named, you can't use the shorthand '__init__(self, *args, **kwargs)'. + +'''python +# Wrong example +#class aa1Controller(Device): +# def __init__(self, *args, **kwargs): +# super().__init__(*args, **kwargs) +# self._foo = "bar" + +# Right example +class aa1Controller(Device): + def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + self._foo = "bar" +''' + +### Status objects (futures) + +This is a major time sink. Bluesky usually just calls 'StatusBase.wait()', and it doesn't even check the type, i.e. it works with anything that resembles an ophyd future. However the BEC adds it's own subscription that has some inconveniences... + +#### DeviceStatus must have a device +Since the BEC wants to subscribe to it, it needs a device to subscribe. +'''python +# Wrong example +# def complete(self): +# status = Status() +# return status + +# Right example + def complete(self): + status = Status(self) + return status +''' + +#### The device of the status shouldn't be a dynamically created object +For some reason it can't be a dynamically created ophyd object. +'''python +# Wrong example +# def complete(self): +# self.mon = EpicsSignal("PV-TO-MONITOR") +# status = SubscriptionStatus(self.mon, self._mon_cb, ...) +# return status + +# Right example + def complete(self): + status = SubscriptionStatus(self.mon, self._mon_cb, ...) + return status +''' + + +### Scans + +Important to know that 'ScanBase.num_pos' must be set to 1 at the end of the scan. Otherwise the bec_client will just hang. + + + + +'''python +class AeroScriptedSequence(FlyScanBase): + def __init__(self, *args, parameter: dict = None, **kwargs): + super().__init__(parameter=parameter, **kwargs) + self.num_pos = 0 + + def cleanup(self): + self.num_pos = 1 + return super().cleanup() +''' + +Note that is often set from a device progress that requires additional capability from the Ophyd device to report it's current progress. +''' +class ProggressMotor(EpicsMotor): + SUB_PROGRESS = "progress" + + def __init__(self, prefix="", *, name, kind=None, read_attrs=None, configuration_attrs=None, parent=None, **kwargs): + super().__init__(prefix=prefix, name=name, kind=kind, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) + self.subscribe(self._progress_update, run=False) + + def _progress_update(self, value, **kwargs) -> None: + """Progress update on the scan""" + if not self.moving: + self._run_subs( sub_type="progress", value=1, max_value=1, done=1, ) + else: + progress = np.abs( (value-self._startPosition)/(self._targetPosition-self._startPosition) ) + self._run_subs(sub_type="progress", value=progress, max_value=1, done=np.isclose(1, progress, 1e-3) ) +''' + +calculated + +Otherwise + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 7a486a17da95b87113aa7487c3436d17bfb34008 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Fri, 8 Mar 2024 15:57:19 +0100 Subject: [PATCH 12/16] Basic examples --- .../devices/aerotech/AerotechAutomation1.py | 51 ++++++++++++++----- .../epics/devices/aerotech/README.md | 5 ++ 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py index 7d9a7a4..5bf804d 100644 --- a/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py @@ -496,6 +496,14 @@ class aa1GlobalVariables(Device): 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) @@ -607,14 +615,11 @@ class aa1GlobalVariables(Device): class aa1GlobalVariableBindings(Device): - """ Global variables + """ Polled 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 + 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) @@ -647,7 +652,8 @@ class aa1AxisIo(Device): 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 IO pin is polled simultaneously! + 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) @@ -774,7 +780,7 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): 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 + 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 @@ -782,7 +788,20 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): 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): @@ -805,9 +824,6 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): 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: @@ -1078,6 +1094,15 @@ class aa1AxisDriveDataCollection(Device): 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() """ # ######################################################################## diff --git a/ophyd_devices/epics/devices/aerotech/README.md b/ophyd_devices/epics/devices/aerotech/README.md index 99283e4..89302d0 100644 --- a/ophyd_devices/epics/devices/aerotech/README.md +++ b/ophyd_devices/epics/devices/aerotech/README.md @@ -83,6 +83,11 @@ For some reason it can't be a dynamically created ophyd object. return status ''' +### Collect does not yield + +In Bluesky collect can be a generator, in BEC it should return the result. + + ### Scans From 172d735672c5115ffdf6ca2a2653aa9bea75934a Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 12 Mar 2024 10:45:25 +0100 Subject: [PATCH 13/16] Update office_teststand_bec_config.yaml --- .../epics/db/office_teststand_bec_config.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/ophyd_devices/epics/db/office_teststand_bec_config.yaml b/ophyd_devices/epics/db/office_teststand_bec_config.yaml index 8d704d4..f5eb81f 100644 --- a/ophyd_devices/epics/db/office_teststand_bec_config.yaml +++ b/ophyd_devices/epics/db/office_teststand_bec_config.yaml @@ -15,7 +15,6 @@ es1_aa1: enabled: true readoutPriority: monitored - es1_aa1Tasks: description: 'AA1 task management interface' deviceClass: epics:devices:aa1Tasks @@ -55,18 +54,3 @@ es1_ddaq: onFailure: buffer enabled: true readoutPriority: monitored - - - - - - - - - - - - - - - From 1b328d31de0575aeb56ec58ef8e50aac878d537e Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 12 Mar 2024 10:49:41 +0100 Subject: [PATCH 14/16] Update README.md --- .../epics/devices/aerotech/README.md | 49 ++----------------- 1 file changed, 4 insertions(+), 45 deletions(-) diff --git a/ophyd_devices/epics/devices/aerotech/README.md b/ophyd_devices/epics/devices/aerotech/README.md index 89302d0..ebe2806 100644 --- a/ophyd_devices/epics/devices/aerotech/README.md +++ b/ophyd_devices/epics/devices/aerotech/README.md @@ -1,13 +1,6 @@ - - - - - - - - ## Integration log for the Ophyd integration for the Aerotech Automation1 EPICS IOC +This is a short collection of reverse engineered wisdom from integrating the first complex device to BEC. The integration involved both the ophyd part and the corresponding scan API. It might not be fully accurate and some issues are probably misdiagnosed. ## Avoid the safespace API!!! @@ -28,9 +21,10 @@ The BEC has it's own event model, that's different from vanilla Bluesky. Particu What's more is that unless it's explicitly specified in the scan, **ALL** ophyd devices (even listeners) get staged and unstaged for every scan. This either makes device management mandatory or raises the need to explicitly prevent this in custom scans. -### Scan server hangs +### The BEC hangs or deadlocks + +Unfortunately a common behavior that manifests in . Common causes include the described issue with num_pos or raising an exception from the Ophyd layer. I'll expand this later. -Unfortunately a common behavior. ### Class @@ -135,38 +129,3 @@ Otherwise - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 1dda0e04ba263fe541e33ea05108343064138e5f Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 12 Mar 2024 10:56:30 +0100 Subject: [PATCH 15/16] Flaking --- .../devices/aerotech/AerotechAutomation1.py | 837 ++++++++++++------ .../aerotech/AerotechAutomation1Enums.py | 561 ++++++------ 2 files changed, 828 insertions(+), 570 deletions(-) diff --git a/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py index 5bf804d..4f297ec 100644 --- a/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py +++ b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1.py @@ -8,85 +8,126 @@ import time try: from .AerotechAutomation1Enums import * - from .AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, - AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, - TaskDataSignal, SystemDataSignal, TomcatSequencerState) + from .AerotechAutomation1Enums import ( + DataCollectionMode, + DataCollectionFrequency, + AxisDataSignal, + PsoWindowInput, + DriveDataCaptureInput, + DriveDataCaptureTrigger, + TaskDataSignal, + SystemDataSignal, + TomcatSequencerState, + ) except: from AerotechAutomation1Enums import * - from AerotechAutomation1Enums import (DataCollectionMode, DataCollectionFrequency, - AxisDataSignal, PsoWindowInput, DriveDataCaptureInput, DriveDataCaptureTrigger, - TaskDataSignal, SystemDataSignal, TomcatSequencerState) + from AerotechAutomation1Enums import ( + DataCollectionMode, + DataCollectionFrequency, + AxisDataSignal, + PsoWindowInput, + DriveDataCaptureInput, + DriveDataCaptureTrigger, + TaskDataSignal, + SystemDataSignal, + TomcatSequencerState, + ) from typing import Union from collections import OrderedDict - 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) + 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'] + self._targetPosition = d["target"] + del d["target"] if "position" in d: - self._targetPosition = d['position'] - del d['position'] + self._targetPosition = d["position"] + del d["position"] return super().configure(d) def kickoff(self): - self._startPosition = float( self.position) + 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) + 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, ) + 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) ) + 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)), ) + 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): + + 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): + 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 + + @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) @@ -94,8 +135,28 @@ class aa1Controller(Device): 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) + + 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 @@ -127,34 +188,57 @@ class aa1Tasks(Device): ''' """ + 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) + 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) + _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): + _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) + 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._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""" + """Progress update on the scan""" value = self.progress() - self._run_subs( sub_type=self.SUB_PROGRESS, value=value, max_value=1, done=1, ) + self._run_subs( + sub_type=self.SUB_PROGRESS, value=value, max_value=1, done=1, + ) def _progress(self) -> None: """Progress update on the scan""" @@ -172,60 +256,60 @@ class aa1Tasks(Device): self.fileName.set(filename).wait() filebytes = self._fileRead.get() # C-strings terminate with trailing zero - if filebytes[-1]==0: + 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: + 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}) + + 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): + + 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}) + + 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: + 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 + 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.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 @@ -240,7 +324,7 @@ class aa1Tasks(Device): 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.taskIndex.set(taskIndex).wait() self.fileName.set(filename).wait() self._fileWrite.set(text).wait() self.switch.set("Load").wait() @@ -254,9 +338,9 @@ class aa1Tasks(Device): self._isConfigured = True new = self.read_configuration() return (old, new) - + ########################################################################## - # Bluesky stepper interface + # Bluesky stepper interface def stage(self) -> None: """ Default staging """ super().stage() @@ -275,10 +359,10 @@ class aa1Tasks(Device): if settle_time is not None: sleep(settle_time) return status - + def stop(self): """ Stop the currently selected task """ - self.switch.set("Stop").wait() + self.switch.set("Stop").wait() ########################################################################## # Flyer interface @@ -302,35 +386,44 @@ class aa1Tasks(Device): 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 + nonlocal timestamp_ + result = False if value[taskIdx] in ["Running", 4] else True timestamp_ = timestamp print(result) - return result + return result # Subscribe and wait for update - status = SubscriptionStatus(self.taskStates, notRunning2, settle_time=0.5) + 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} + 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) @@ -341,34 +434,42 @@ class aa1TaskState(Device): 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]) + 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) + 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} + 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 @@ -381,21 +482,22 @@ class aa1DataAcquisition(Device): 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_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) + 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() @@ -408,7 +510,7 @@ class aa1DataAcquisition(Device): 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. @@ -432,31 +534,31 @@ class aa1DataAcquisition(Device): # 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() + 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() + self._switch.set("START").wait() # Wait for finishing acquisition # Note: this is very bad blocking sleep - while self.status.value!=0: + 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: + if len(data) == 0 or rows == 0 or cols == 0: sleep(0.5) data = self.data_rb.get() rows = self.data_rows.get() @@ -470,13 +572,13 @@ class aa1DataAcquisition(Device): 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: + if len(data) == 0 or rows == 0 or cols == 0: sleep(0.2) data = self.data_rb.get() rows = self.data_rows.get() @@ -486,7 +588,6 @@ class aa1DataAcquisition(Device): return data - class aa1GlobalVariables(Device): """ Global variables @@ -505,34 +606,35 @@ class aa1GlobalVariables(Device): 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) - + 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) - + 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) - + 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: + + 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() @@ -540,12 +642,12 @@ class aa1GlobalVariables(Device): 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() @@ -559,12 +661,11 @@ class aa1GlobalVariables(Device): else: raise RuntimeError("Unsupported integer value type: {type(value)}") - - def readFloat(self, address: int, size: int=None) -> float: + 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() @@ -577,7 +678,7 @@ class aa1GlobalVariables(Device): """ 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() @@ -597,7 +698,7 @@ class aa1GlobalVariables(Device): """ 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() @@ -605,7 +706,7 @@ class aa1GlobalVariables(Device): """ 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() @@ -613,7 +714,6 @@ class aa1GlobalVariables(Device): raise RuntimeError("Unsupported string value type: {type(value)}") - class aa1GlobalVariableBindings(Device): """ Polled global variables @@ -621,30 +721,124 @@ class aa1GlobalVariableBindings(Device): 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) + 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) + 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): @@ -655,6 +849,7 @@ class aa1AxisIo(Device): 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) @@ -666,7 +861,7 @@ class aa1AxisIo(Device): 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) @@ -684,13 +879,12 @@ class aa1AxisIo(Device): 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): @@ -707,45 +901,48 @@ class aa1AxisPsoBase(Device): 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) + _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) + 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) + 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 + # 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) - + # 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) + # 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) @@ -754,7 +951,7 @@ class aa1AxisPsoBase(Device): # ######################################################################## # PSO output module outPin = Component(EpicsSignal, "PIN", put_complete=True, kind=Kind.omitted) - outSource = Component(EpicsSignal, "SOURCE", 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)""" @@ -767,9 +964,6 @@ class aa1AxisPsoBase(Device): self.waveMode.set(orig_waveMode).wait() - - - class aa1AxisPsoDistance(aa1AxisPsoBase): """ Position Sensitive Output - Distance mode @@ -802,11 +996,30 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): 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): + + 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) + 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) @@ -814,7 +1027,9 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): """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, ) + self._run_subs( + sub_type=self.SUB_PROGRESS, value=1, max_value=1, done=1, + ) return progress = 1 @@ -822,23 +1037,25 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): 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)), ) + 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: + 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 - + 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"]: @@ -851,21 +1068,21 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): 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.dstCounterEna.set("Off").wait() self.dstEventsEna.set("Off").wait() - - # Configure the pulsed/toggled waveform + + # 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() + + 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() @@ -878,86 +1095,97 @@ class aa1AxisPsoDistance(aa1AxisPsoBase): # Enabling PSO waveform outputs self.waveEnable.set("On").wait() else: - raise RuntimeError(f"Unsupported waveform mode: {wmode}") - + raise RuntimeError(f"Unsupported waveform mode: {wmode}") + # Ensure output is set to low if self.output.value: - self.toggle() - + self.toggle() + # Set PSO output data source - self.outSource.set("Waveform").wait() + 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 = DeviceStatus(self) status.set_finished() return status - def trigger(self): - return super().trigger() - + def trigger(self): + return super().trigger() + def unstage(self): # Turn off counter monitoring self.dstEventsEna.set("Off").wait() - self.dstCounterEna.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)): + 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() + 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)): + 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) + 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) + # status = SubscriptionStatus(self.status, notRunning, settle_time=0.5) # Data capture can be stopped any time status = DeviceStatus(self) - status.set_finished() + 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} + 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 } + ret["data"] = {"index": self.dstCounterVal.value} yield ret - - class aa1AxisPsoWindow(aa1AxisPsoBase): @@ -978,35 +1206,54 @@ class aa1AxisPsoWindow(aa1AxisPsoBase): 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): + """ + + 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) + 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: + # 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 + 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 @@ -1014,8 +1261,8 @@ class aa1AxisPsoWindow(aa1AxisPsoBase): if len(bounds) == 2: self.winCounter.set(0).wait() self._winLower.set(bounds[0]).wait() - self._winUpper.set(bounds[1]).wait() - + self._winUpper.set(bounds[1]).wait() + elif isinstance(bounds, np.ndarray): # ToDo... pass @@ -1024,16 +1271,16 @@ class aa1AxisPsoWindow(aa1AxisPsoBase): self.winOutput.set("Off").wait() self.winEvents.set("Off").wait() - # Configure the pulsed/toggled waveform + # 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.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() @@ -1044,47 +1291,42 @@ class aa1AxisPsoWindow(aa1AxisPsoBase): elif wmode in ["output", "flag"]: self.waveEnable.set("Off").wait() else: - raise RuntimeError(f"Unsupported window mode: {wmode}") + raise RuntimeError(f"Unsupported window mode: {wmode}") # Set PSO output data source if wmode in ["toggle", "toggled", "pulse", "pulsed"]: - self.outSource.set("Waveform").wait() + self.outSource.set("Waveform").wait() elif wmode in ["output", "flag"]: - self.outSource.set("Window").wait() + 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: + 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): + def kickoff(self, settle_time=None): if self.outSource.get() in ["Window", 2]: self.winOutput.set("On").wait() - else: + else: self.winEvents.set(self._eventMode).wait() if settle_time is not None: - sleep(settle_time) - + 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) + sleep(settle_time) return super().unstage() - - - - - class aa1AxisDriveDataCollection(Device): """ Axis data collection @@ -1104,7 +1346,7 @@ class aa1AxisDriveDataCollection(Device): ... ret = yield from ddc.collect() """ - + # ######################################################################## # General module status state = Component(EpicsSignalRO, "STATE", auto_monitor=True, kind=Kind.hinted) @@ -1119,35 +1361,56 @@ class aa1AxisDriveDataCollection(Device): _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) + 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, ) + 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)), ) + 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 + 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() @@ -1159,11 +1422,11 @@ class aa1AxisDriveDataCollection(Device): new = self.read_configuration() return (old, new) - - # Bluesky step scanning interface + + # Bluesky step scanning interface def stage(self, settle_time=0.1): super().stage() - self._switch.set("Start", settle_time=0.5).wait() + self._switch.set("Start", settle_time=0.5).wait() status = Status(timeout=0.1, settle_time=settle_time).set_finished() return status @@ -1171,19 +1434,19 @@ class aa1AxisDriveDataCollection(Device): self._switch.set("Stop", settle_time=settle_time).wait() super().unstage() print(f"Recorded samples: {self.nsamples_rbv.value}") - - # Bluesky flyer interface + + # Bluesky flyer interface def kickoff(self, settle_time=0.1) -> Status: - status = self._switch.set("Start", settle_time=settle_time) + 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 = DeviceStatus(self) status.set_finished() return status - + def _collect(self, index=0): """ Force a readback of the data buffer @@ -1191,34 +1454,48 @@ class aa1AxisDriveDataCollection(Device): 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}") + 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: + + if index == 0: status = SubscriptionStatus(self._readstatus0, negEdge, settle_time=0.5) self._readback0.set(1).wait() - elif index==1: + 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} + 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() @@ -1227,12 +1504,9 @@ class aa1AxisDriveDataCollection(Device): b0 = self._buffer0.value b1 = self._buffer1.value ret = OrderedDict() - ret["timestamps"] = {"buffer0": time.time(), "buffer1": time.time() } - ret["data"] = {"buffer0": b0, "buffer1": b1 } + ret["timestamps"] = {"buffer0": time.time(), "buffer1": time.time()} + ret["data"] = {"buffer0": b0, "buffer1": b1} yield ret - - - # Automatically start simulation if directly invoked @@ -1240,16 +1514,13 @@ 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 = 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 = aa1GlobalVariableBindings(AA1_IOC_NAME + ":VAR:", name="globb") globb.wait_for_connection() globb.describe() - mot = EpicsMotor(AA1_IOC_NAME+ ":" + AA1_AXIS_NAME, name="x") + mot = EpicsMotor(AA1_IOC_NAME + ":" + AA1_AXIS_NAME, name="x") mot.wait_for_connection() - - - diff --git a/ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py index ac0f981..ce75eba 100644 --- a/ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py +++ b/ophyd_devices/epics/devices/aerotech/AerotechAutomation1Enums.py @@ -269,237 +269,235 @@ class AxisDataSignal: 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 - - - + 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 + 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 + 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 - - + 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, + 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 - + DedicatedOutput = 111 + AuxiliaryMarkerDifferential = 112 + AuxiliaryMarkerSingleEnded = 113 + + class XC4PsoOutputPin: - DedicatedOutput = 108 - AuxiliaryMarkerDifferential = 109 - AuxiliaryMarkerSingleEnded = 110 - - + DedicatedOutput = 108 + AuxiliaryMarkerDifferential = 109 + AuxiliaryMarkerSingleEnded = 110 + + """ # @brief Specifies the PSO distance input settings for each drive. class Automation1PsoDistanceInput: @@ -834,81 +832,70 @@ class Automation1PsoOutputPin: class DriveDataCaptureInput: - PositionCommand = 0 - PrimaryFeedback = 1 - AuxiliaryFeedback = 2 - AnalogInput0 = 3 - AnalogInput1 = 4 - AnalogInput2 = 5 - AnalogInput3 = 6 + 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 - - + 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, - - - - - - - - - + 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,) From 65a05562434c92a44227fb6d3ade051423f533e1 Mon Sep 17 00:00:00 2001 From: mohacsi_i Date: Tue, 12 Mar 2024 11:18:00 +0100 Subject: [PATCH 16/16] Update __init__.py --- ophyd_devices/epics/devices/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ophyd_devices/epics/devices/__init__.py b/ophyd_devices/epics/devices/__init__.py index 7d06b31..a64a5bc 100644 --- a/ophyd_devices/epics/devices/__init__.py +++ b/ophyd_devices/epics/devices/__init__.py @@ -29,7 +29,15 @@ from .specMotors import ( PmMonoBender, ) -from .aerotech.AerotechAutomation1 import aa1Controller, aa1Tasks, aa1GlobalVariables, aa1GlobalVariableBindings, aa1AxisPsoDistance, aa1AxisDriveDataCollection, EpicsMotorX +from .aerotech.AerotechAutomation1 import ( + aa1Controller, + aa1Tasks, + aa1GlobalVariables, + aa1GlobalVariableBindings, + aa1AxisPsoDistance, + aa1AxisDriveDataCollection, + EpicsMotorX, +) from .SpmBase import SpmBase from .XbpmBase import XbpmBase, XbpmCsaxsOp