class ZebraINP(Device): use = FC(EpicsSignal, '{self.prefix}_ENA:B{self._bindex}') source_addr = FC(EpicsSignalWithRBV, '{self.prefix}_INP{self.index}') source_str = FC(EpicsSignalRO, '{self.prefix}_INP{self.index}:STR', string=True) source_status = FC(EpicsSignalRO, '{self.prefix}_INP{self.index}:STA') invert = FC(EpicsSignal, '{self.prefix}_INV:B{self._bindex}') def __init__(self, prefix, *, index, read_attrs=None, configuration_attrs=None, **kwargs): if read_attrs is None: read_attrs = [] if configuration_attrs is None: configuration_attrs = [ 'use', 'source_addr', 'source_str', 'invert' ] self.index = index self._bindex = index - 1 super().__init__(prefix, read_attrs=read_attrs, configuration_attrs=configuration_attrs, **kwargs)
class ZebraPositionCaptureArm(ZebraPositionCaptureDeviceBase): class ZebraArmSignalWithRBV(EpicsSignal): def __init__(self, prefix, **kwargs): super().__init__(prefix + 'ARM_OUT', write_pv=prefix + 'ARM', **kwargs) class ZebraDisarmSignalWithRBV(EpicsSignal): def __init__(self, prefix, **kwargs): super().__init__(prefix + 'ARM_OUT', write_pv=prefix + 'DISARM', **kwargs) arm = FC(ZebraArmSignalWithRBV, '{self._parent_prefix}') disarm = FC(ZebraDisarmSignalWithRBV, '{self._parent_prefix}') def __init__(self, prefix, *, parent=None, configuration_attrs=None, read_attrs=None, **kwargs): self._parent_prefix = parent.prefix super().__init__(prefix, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs)
class ZebraEncoder(Device): motor_pos = FC(EpicsSignalRO, '{self._zebra_prefix}M{self.index}:RBV') zebra_pos = FC(EpicsSignal, '{self._zebra_prefix}POS{self.index}_SET') encoder_res = FC(EpicsSignal, '{self._zebra_prefix}M{self.index}:MRES') encoder_off = FC(EpicsSignal, '{self._zebra_prefix}M{self.index}:OFF') _copy_pos_signal = FC(EpicsSignal, '{self._zebra_prefix}M{self.index}:SETPOS.PROC') def __init__(self, prefix, *, index=None, parent=None, configuration_attrs=None, read_attrs=None, **kwargs): if read_attrs is None: read_attrs = [] if configuration_attrs is None: configuration_attrs = ['encoder_res', 'encoder_off'] self.index = index self._zebra_prefix = parent.prefix super().__init__(prefix, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs) def copy_position(self): self._copy_pos_signal.put(1, wait=True)
class SlitPositioner(FltMvInterface, PVPositioner, Device): """ Abstraction of Slit Axis Each adjustable parameter of the slit (center, width) can be modeled as a motor in itself, even though each controls two different actual motors in reality, this gives a convienent interface for adjusting the aperture size and location with out backwards calculating motor positions Parameters ---------- prefix : ``str`` The prefix location of the slits, i.e MFX:DG2 slit_type : ``'XWIDTH'``, ``'YWIDTH'``, ``'XCENTER'``, ``'YCENTER'`` The aspect of the slit position you would like to control with this specific motor name : ``str`` Alias for the axis limits : ``tuple``, optional Limits on the motion of the positioner. By default, the limits on the setpoint PV are used if None is given. See Also -------- ``ophyd.PVPositioner`` ``SlitPositioner`` inherits directly from ``PVPositioner``. """ setpoint = FC(EpicsSignal, "{self.prefix}:{self._dirshort}_REQ") readback = FC(EpicsSignalRO, "{self.prefix}:ACTUAL_{self._dirlong}") done = C(EpicsSignalRO, ":DMOV") _default_read_attrs = ['readback'] def __init__(self, prefix, *, slit_type="", name=None, limits=None, **kwargs): # Private PV names to deal with complex naming schema self._dirlong = slit_type self._dirshort = slit_type[:4] # Initalize PVPositioner super().__init__(prefix, limits=limits, name=name, **kwargs) @property def egu(self): """Engineering units""" return self._egu or self.readback._read_pv.units def _setup_move(self, position): # This is subclassed because we need `wait` to be set to False unlike # the default PVPositioner method. `wait` set to True will not return # until the move has completed logger.debug('%s.setpoint = %s', self.name, position) self.setpoint.put(position, wait=False)
class LaserShutter(InOutPositioner): """Controls shutter controlled by Analog Output""" # EpicsSignals voltage = C(EpicsSignal, '') state = FC(AttributeSignal, 'voltage_check') # Constants out_voltage = 5.0 in_voltage = 0.0 barrier_voltage = 1.4 @property def voltage_check(self): """Return the position we believe shutter based on the channel""" if self.voltage.get() >= self.barrier_voltage: return 'OUT' else: return 'IN' def _do_move(self, state): """Override to just put to the channel""" if state.name == 'IN': self.voltage.put(self.in_voltage) elif state.name == 'OUT': self.voltage.put(self.out_voltage) else: raise ValueError("%s is in an invalid state", state)
def mps_factory(clsname, cls, *args, mps_prefix, veto=False, **kwargs): """ Create a new object of arbitrary class capable of storing MPS information A new class identical to the provided one is created, but with additional attribute ``mps`` that relies upon the provided ``mps_prefix``. All other information is passed through to the class constructor as args and kwargs Parameters ---------- clsname : ``str`` Name of new class to create cls : ``type`` Device class to add ``mps`` mps_prefix : ``str`` Prefix for MPS subcomponent veto : ``bool``, optional Whether the MPS bit is capable of veto args : Passed to device constructor kwargs: Passed to device constructor """ comp = FC(MPS, mps_prefix, veto=veto) cls = type(clsname, (cls, ), {'mps': comp}) return cls(*args, **kwargs)
class ZebraPulse(Device): width = Cpt(ZebraSignalWithRBV, 'WID') input_addr = Cpt(ZebraSignalWithRBV, 'INP') input_str = Cpt(EpicsSignalRO, 'INP:STR', string=True) input_status = Cpt(EpicsSignalRO, 'INP:STA') delay = Cpt(ZebraSignalWithRBV, 'DLY') delay_sync = Cpt(EpicsSignal, 'DLY:SYNC') time_units = Cpt(ZebraSignalWithRBV, 'PRE', string=True) output = Cpt(EpicsSignal, 'OUT') input_edge = FC(EpicsSignal, '{self._zebra_prefix}POLARITY:{self._edge_addr}') _edge_addrs = {1: 'BC', 2: 'BD', 3: 'BE', 4: 'BF', } def __init__(self, prefix, *, index=None, parent=None, configuration_attrs=None, read_attrs=None, **kwargs): if read_attrs is None: read_attrs = ['input_status', 'output'] if configuration_attrs is None: configuration_attrs = list(self.component_names) + ['input_edge'] zebra = parent self.index = index self._zebra_prefix = zebra.prefix self._edge_addr = self._edge_addrs[index] super().__init__(prefix, configuration_attrs=configuration_attrs, read_attrs=read_attrs, parent=parent, **kwargs)
class ZebraGateInput(Device): addr = Cpt(ZebraSignalWithRBV, '') string = Cpt(EpicsSignalRO, ':STR', string=True) status = Cpt(EpicsSignalRO, ':STA') sync = Cpt(EpicsSignal, ':SYNC') write_input = Cpt(EpicsSignal, ':SET') # Input edge index depends on the gate number (these are set in __init__) edge = FC(EpicsSignal, '{self._zebra_prefix}POLARITY:B{self._input_edge_idx}') def __init__(self, prefix, *, index=None, parent=None, configuration_attrs=None, read_attrs=None, **kwargs): if read_attrs is None: read_attrs = ['status'] if configuration_attrs is None: configuration_attrs = ['addr', 'edge'] gate = parent zebra = gate.parent self.index = index self._zebra_prefix = zebra.prefix self._input_edge_idx = gate._input_edge_idx[self.index] super().__init__(prefix, read_attrs=read_attrs, configuration_attrs=configuration_attrs, parent=parent, **kwargs)
class Gantry(OMMotor): """ Gantry Axis The horizontal and vertical motion of the OffsetMirror are controlled by two coupled stepper motors. Instructions are sent to both by simply requesting a move on the primary so they are represented here as a single motor with additional diagnostics and interlock Parameters ---------- prefix : str Base prefix for both stepper motors i.e XRT:M1H. Do not include the "P" or "S" to indicate primary or secondary steppers gantry_prefix : str, optional Prefix for the shared gantry diagnostics if it is different than the stepper motor prefix """ # Readbacks for gantry information gantry_difference = FC(EpicsSignalRO, "{self.gantry_prefix}:GDIF") decoupled = FC(EpicsSignalRO, "{self.gantry_prefix}:DECOUPLE") # Readbacks for the secondary motor follower_readback = FC(EpicsSignalRO, "{self.follow_prefix}:RBV") follower_low_limit_switch = FC(EpicsSignalRO, "{self.follow_prefix}:LLS") follower_high_limit_switch = FC(EpicsSignalRO, "{self.follow_prefix}:HLS") _default_read_attrs = ['readback', 'setpoint', 'gantry_difference'] def __init__(self, prefix, *, gantry_prefix=None, **kwargs): self.gantry_prefix = gantry_prefix or 'GANTRY:' + prefix self.follow_prefix = prefix + ':S' super().__init__(prefix + ':P', **kwargs) def check_value(self, pos): """ Add additional check for the gantry coupling This is not a safety measure, but instead just here largely for bookkeeping and to give the operator further feedback on why the requested move is not completed. """ # Check that the gantry is not decoupled if self.decoupled.get(): raise PermissionError("The gantry is not currently coupled") # Allow OMMotor to check the value super().check_value(pos)
class MockDevice(Device): # Device signals readback = C(RandomSignal) noise = C(RandomSignal) transmorgifier = C(SignalRO, value=4) setpoint = C(Signal, value=0) velocity = C(Signal, value=1) flux = C(RandomSignal) modified_flux = C(RandomSignal) capacitance = C(RandomSignal) acceleration = C(Signal, value=3) limit = C(Signal, value=4) inductance = C(RandomSignal) transformed_inductance = C(SignalRO, value=3) core_temperature = C(RandomSignal) resolution = C(Signal, value=5) duplicator = C(Signal, value=6) # Component Motors x = FC(ConfiguredSynAxis, name='X Axis') y = FC(ConfiguredSynAxis, name='Y Axis') z = FC(ConfiguredSynAxis, name='Z Axis') # Default Signal Sorting _default_read_attrs = [ 'readback', 'setpoint', 'transmorgifier', 'noise', 'inductance' ] _default_configuration_attrs = [ 'flux', 'modified_flux', 'capacitance', 'velocity', 'acceleration' ] def insert(self, width: float = 2.0, height: float = 2.0, fast_mode: bool = False): """Fake insert function to display""" pass def remove(self, height: float, fast_mode: bool = False): """Fake remove function to display""" pass @property def hints(self): return {'fields': [self.name + '_readback']}
class Pitch(OMMotor): """ HOMS Pitch Mechanism The axis is actually a piezo actuator and a stepper motor in series, and this is reflected in the PV naming """ __doc__ += basic_positioner_init piezo_volts = FC(EpicsSignalRO, "{self._piezo}:VRBV") stop_signal = FC(EpicsSignal, "{self._piezo}:STOP") # TODO: Limits will be added soon, but not present yet def __init__(self, prefix, **kwargs): # Predict the prefix of all piezo pvs self._piezo = prefix.replace('MIRR', 'PIEZO') super().__init__(prefix, **kwargs)
class MockDevice(Device): # Device signals readback = Cpt(RandomSignal, kind='normal') noise = Cpt(RandomSignal, kind='normal') transmorgifier = Cpt(SignalRO, value=4, kind='normal') setpoint = Cpt(Signal, value=0, kind='normal') velocity = Cpt(Signal, value=1, kind='config') flux = Cpt(RandomSignal, kind='config') modified_flux = Cpt(RandomSignal, kind='config') capacitance = Cpt(RandomSignal, kind='config') acceleration = Cpt(Signal, value=3, kind='config') limit = Cpt(Signal, value=4, kind='config') inductance = Cpt(RandomSignal, kind='normal') transformed_inductance = Cpt(SignalRO, value=3, kind='omitted') core_temperature = Cpt(RandomSignal, kind='omitted') resolution = Cpt(Signal, value=5, kind='omitted') duplicator = Cpt(Signal, value=6, kind='omitted') # Component Motors x = FC(ConfiguredSynAxis, name='X Axis') y = FC(ConfiguredSynAxis, name='Y Axis') z = FC(ConfiguredSynAxis, name='Z Axis') def insert(self, width: float = 2.0, height: float = 2.0, fast_mode: bool = False): """Fake insert function to display""" pass def remove(self, height: float, fast_mode: bool = False): """Fake remove function to display""" pass @property def hints(self): return {'fields': [self.name + '_readback']}
class MockDevice(Device): # Device signals read1 = C(EpicsSignalRO, ':READ1') read2 = C(EpicsSignalRO, ':READ2') read3 = C(EpicsSignalRO, ':READ3') read4 = C(EpicsSignal, ':READ4') read5 = C(EpicsSignal, ':READ5', write_pv=':WRITE5') config1 = C(EpicsSignalRO, ':CFG1') config2 = C(EpicsSignalRO, ':CFG2') config3 = C(EpicsSignalRO, ':CFG3') config4 = C(EpicsSignal, ':CFG4') config5 = C(EpicsSignal, ':CFG5', write_pv=':CFGWRITE5') misc1 = C(EpicsSignalRO, ':MISC1') misc2 = C(EpicsSignalRO, ':MISC2') misc3 = C(EpicsSignalRO, ':MISC3') misc4 = C(EpicsSignal, ':MISC4') misc5 = C(EpicsSignal, ':MISC5', write_pv=':MISCWRITE5') # Component Motors x = FC(EpicsMotor, 'Tst:MMS:X', name='X Axis') y = FC(EpicsMotor, 'Tst:MMS:Y', name='Y Axis') z = FC(EpicsMotor, 'Tst:MMS:Z', name='Z Axis') # Default Signal Sorting _default_read_attrs = ['read1', 'read2', 'read3', 'read4', 'read5'] _default_configuration_attrs = [ 'config1', 'config2', 'config3', 'config4', 'config5' ] def insert(self, width: float = 2.0, height: float = 2.0, fast_mode: bool = False): """Fake insert function to display""" pass def remove(self, height: float, fast_mode: bool = False): """Fake remove function to display""" pass
class Lens(InOutRecordPositioner): """ Data structure for basic Lens object Parameters ---------- prefix : str Name of the state record that controls the PV prefix_lens : str Prefix for the PVs that contain focusing information """ _sig_radius = FC(EpicsSignalRO, "{self.prefix_lens}:RADIUS", auto_monitor=True) _sig_z = FC(EpicsSignalRO, "{self.prefix_lens}:Z", auto_monitor=True) _sig_focus = FC(EpicsSignalRO, "{self.prefix_lens}:FOCUS", auto_monitor=True) # Default configuration attributes. Read attributes are set correctly by # InOutRecordPositioner _default_configuration_attrs = ['_sig_radius', '_sig_z'] def __init__(self, prefix, prefix_lens, **kwargs): self.prefix_lens = prefix_lens super().__init__(prefix, **kwargs) @property def radius(self): """ Method converts the EPICS lens radius signal into a float that can be used for calculations. Returns ------- float Returns the radius of the lens """ return self._sig_radius.value @property def z(self): """ Method converts the z position EPICS signal into a float. Returns ------- float Returns the z position of the lens in meters along the beamline """ return self._sig_z.value @property def focus(self): """ Method converts the EPICS focal length signal of the lens into a float Returns ------- float Returns the focal length of the lens in meters """ return self._sig_focus.value def image_from_obj(self, z_obj): """ Method calculates the image distance in meters along the beam pipeline from a point of origin given the focal length of the lens, location of lens, and location of object. Parameters ---------- z_obj Location of object along the beamline in meters (m) Returns ------- image Returns the distance z_im of the image along the beam pipeline from a point of origin in meters (m) Note ---- If the location of the object (z_obj) is equal to the focal length of the lens, this function will return infinity. """ # Check if the lens object is at the focal length # If this happens, then the image location will be infinity. # Note, this should not effect the recursive calculations that occur # later in the code if z_obj == self.focus: return np.inf # Find the object location for the lens obj = self.z - z_obj # Calculate the location of the focal plane plane = 1/(1/self.focus - 1/obj) # Find the position in accelerator coordinates return plane + self.z
class DualAdcFS(TriggerAdc): ''' Adc Device, when read, returns references to data in filestore. This is for a dual device. It defines one ADC which uses a shared triggering mechanism for file writing. The adc is either a 'master' or 'slave'. If 'master', then kickoff() should trigger the 'Ena-Sel' PV (which starts collecting data to file). If 'slave', then it should check that the 'Ena-Sel' PV is set. TODO : Need to add a good waiting mechanism for this. ''' # these are for the dual ADC FS # column is the column and enable_sel is what triggers the collection # rename because of existing children pv's chunk_size = 2**20 write_path_template = '/nsls2/xf07bm/data/pizza_box_data/%Y/%m/%d/' volt = FC(EpicsSignal, '{self._adc_read}}}E-I') offset = FC(EpicsSignal, '{self._adc_read}}}Offset') dev_name = FC(EpicsSignal, '{self._adc_read}}}DevName') def __init__(self, *args, adc_column, adc_read_name, twin_adc=None, reg, **kwargs): ''' This is for a dual ADC system. adc_column : the column adc_trigger: the adc pv used for triggering for this dual channel mode : {'master', 'slave'} The mode. Master will create a new file and resource during kickoff Slave assumes the new file and resource are already created. Slave should check if acquisition has been triggered first, else return an error. Notes: The adc master and adc for triggering are not necessarily the same (for ex, adc6 and adc7 triggered using adc1, but adc6 is what will put to adc1's trigger) ''' self._twin_adc = twin_adc self._reg = reg self._column = adc_column self._adc_read = adc_read_name self._staged_adc = False self._kickoff_adc = False self._complete_adc = False super().__init__(*args, **kwargs) def stage(self): "Set the filename and record it in a 'resource' document in the filestore database." if self.connected: # NOTE: master MUST be done before slave. need to fix this later if self._twin_adc is None: raise ValueError("Error a twin ADC must be given") # if twin didnt stage yet, stage if not self._twin_adc._staged_adc: self._staged_adc = True print(self.name, 'stage') DIRECTORY = datetime.now().strftime(self.write_path_template) #DIRECTORY = "/nsls2/xf07bm/data/pb_data" filename = 'an_' + str(uuid.uuid4())[:6] self._full_path = os.path.join( DIRECTORY, filename) # stash for future reference print(">>>>>>>>>>>>>>> writing to {}".format(self._full_path)) self.filepath.put(self._full_path) self.resource_uid = self._reg.register_resource( 'PIZZABOX_AN_FILE_TXT', DIRECTORY, self._full_path, {'chunk_size': self.chunk_size}) super().stage() else: # don't stage, use twin's info self.resource_uid = self._twin_adc.resource_uid self._full_path = self._twin_adc._full_path # reset twin self._twin_adc._staged_adc = False print( "ACD {}'s twin {} already staged. File path already set to {}" .format(self.name, self._twin_adc.name, self.filepath.get())) else: msg = "Error, adc {} not ready for acquiring\n".format(self.name) raise ValueError(msg) time.sleep(.1) def unstage(self): if (self.connected): set_and_wait(self.enable_sel, 1) # either master or slave can unstage if needed, safer self._staged_adc = False self._kickoff_adc = False self._complete_adc = False return super().unstage() def kickoff(self): print('kickoff', self.name) if self._twin_adc is None: raise ValueError("ADC must have a twin") if self._twin_adc._kickoff_adc is False: self._ready_to_collect = True "Start writing data into the file." # set_and_wait(self.enable_sel, 0) st = self.enable_sel.set(0) self._kickoff_adc = True return st else: print("ADC {} was kicked off by {} already".format( self.name, self._twin_adc.name)) self._ready_to_collect = True #reset kickoff self._kickoff_adc = False #reset twin self._twin_adc._kickoff_adc = False st = Status() st._finished() return st def complete(self): print('complete', self.name, '| filepath', self._full_path) if not self._ready_to_collect: raise RuntimeError( "must called kickoff() method before calling complete()") if self._twin_adc._complete_adc is False: # Stop adding new data to the file. #set_and_wait(self.enable_sel, 1) self.enable_sel.put(1) self._complete_adc = True else: print("Device already stopped by {}".format(self._twin_adc.name)) self._twin_adc._complete_adc = False self._complete_adc = False return NullStatus() def collect(self): """ Record a 'datum' document in the filestore database for each encoder. Return a dictionary with references to these documents. """ print('collect', self.name) self._ready_to_collect = False # Create an Event document and a datum record in filestore for each line # in the text file. now = ttime.time() ttime.sleep(1) # wait for file to be written by pizza box if os.path.isfile(self._full_path): with open(self._full_path, 'r') as f: linecount = 0 for ln in f: linecount += 1 chunk_count = linecount // self.chunk_size + int( linecount % self.chunk_size != 0) for chunk_num in range(chunk_count): datum_uid = self._reg.register_datum(self.resource_uid, { 'chunk_num': chunk_num, 'column': self._column }) data = {self.name: datum_uid} yield { 'data': data, 'timestamps': {key: now for key in data}, 'time': now } else: print('collect {}: File was not created'.format(self.name)) print("filename : {}".format(self._full_path)) def describe_collect(self): # TODO Return correct shape (array dims) now = ttime.time() return { self.name: { self.name: { 'filename': self._full_path, 'devname': self.dev_name.value, 'source': 'pizzabox-adc-file', 'external': 'FILESTORE:', 'shape': [ 5, ], 'dtype': 'array' } } }
class Xspress3ROI(ADBase): '''A configurable Xspress3 EPICS ROI''' # prefix: C{channel}_ MCA_ROI{self.roi_num} bin_low = FC(SignalWithRBV, '{self.channel.prefix}{self.bin_suffix}_LLM') bin_high = FC(SignalWithRBV, '{self.channel.prefix}{self.bin_suffix}_HLM') # derived from the bin signals, low and high electron volt settings: ev_low = Cpt(EvSignal, parent_attr='bin_low') ev_high = Cpt(EvSignal, parent_attr='bin_high') # C{channel}_ ROI{self.roi_num} value = Cpt(EpicsSignalRO, 'Value_RBV') value_sum = Cpt(EpicsSignalRO, 'ValueSum_RBV') enable = Cpt(SignalWithRBV, 'EnableCallbacks') # ad_plugin = Cpt(Xspress3ROISettings, '') @property def ad_root(self): root = self.parent while True: if not isinstance(root.parent, ADBase): return root root = root.parent def __init__(self, prefix, *, roi_num=0, use_sum=False, read_attrs=None, configuration_attrs=None, parent=None, bin_suffix=None, **kwargs): if read_attrs is None: if use_sum: read_attrs = ['value_sum'] else: read_attrs = ['value', 'value_sum'] if configuration_attrs is None: configuration_attrs = ['ev_low', 'ev_high', 'enable'] rois = parent channel = rois.parent self._channel = channel self._roi_num = roi_num self._use_sum = use_sum self._ad_plugin = getattr(rois, 'ad_attr{:02d}'.format(roi_num)) if bin_suffix is None: bin_suffix = 'MCA_ROI{}'.format(roi_num) self.bin_suffix = bin_suffix super().__init__(prefix, parent=parent, read_attrs=read_attrs, configuration_attrs=configuration_attrs, **kwargs) @property def settings(self): '''Full areaDetector settings''' return self._ad_plugin @property def channel(self): '''The Xspress3Channel instance associated with the ROI''' return self._channel @property def channel_num(self): '''The channel number associated with the ROI''' return self._channel.channel_num @property def roi_num(self): '''The ROI number''' return self._roi_num def clear(self): '''Clear and disable this ROI''' self.configure(0, 0) def configure(self, ev_low, ev_high): '''Configure the ROI with low and high eV Parameters ---------- ev_low : int low electron volts for ROI ev_high : int high electron volts for ROI ''' ev_low = int(ev_low) ev_high = int(ev_high) enable = 1 if ev_high > ev_low else 0 changed = any([ self.ev_high.get() != ev_high, self.ev_low.get() != ev_low, self.enable.get() != enable ]) if not changed: return logger.debug( 'Setting up EPICS ROI: name=%s ev=(%s, %s) ' 'enable=%s prefix=%s channel=%s', self.name, ev_low, ev_high, enable, self.prefix, self._channel) if ev_high <= self.ev_low.get(): self.ev_low.put(0) self.ev_high.put(ev_high) self.ev_low.put(ev_low) self.enable.put(enable)
class BlockWatch(Device): """ Class to monitor Sequencer and Analog Input The BlockWatch has a constant monitor callback on both the sequencer and the analog input calling :meth:`.process_event` whenever one of these changes. The handling of these signals is as follows; if the analog input is below a certain threshold and the sequencer is off a soft trip will be reported. This is simply to alert the operator, but no preventative measure will be taken. If the sequencer is running, and the signal is below the :attr:`.threshold`, we report a hard trip, and close the filter to protect our detector. In order to give the operator some control and readback, the status of the watcher is reported into a few soft PVS. In addition to the soft and hard trips, there is a string pv so that the status can be displayed. Finally, there is a quick enable toggle. If this is set to False, the logic will process the same, but motion of the attenuator will be prevented Parameters ---------- prefix : str Base of the PV Notepad PVs ai : str Analog input channel of RoadRunner monitor fltr : str Base name of attenuator filter sequencer : str Base name of sequencer threshold : float, optional Minimum threshold to allow RoadRunner signal before tripping the watch """ #Soft PVs soft_trip = C(EpicsSignal, ':SOFT_TRIP') hard_trip = C(EpicsSignal, ':HARD_TRIP') enabled = C(EpicsSignalRO, ':ENABLE', auto_monitor=True) #Real PVs seq_run = FC(EpicsSignalRO, '{self._sequencer}:PLSTAT', auto_monitor=True) seq_cnt = FC(EpicsSignalRO, '{self._sequencer}:CURSTP', auto_monitor=True) ai = FC(EpicsSignal, '{self._ai}', auto_monitor=True) #Blocker blocker = FC(Filter, '{self._filter}') def __init__(self, prefix, ai, fltr, sequencer, threshold=1): self.threshold = threshold #Store EPICS prefixes self._filter = fltr self._ai = ai self._sequencer = sequencer #Initialize ophyd device super().__init__(prefix) @property def sequencer_running(self): """ Return whether the sequencer is running The play status of the sequencer is modified as well as the current step. This is done to avoid race conditions with the start of the sequencer and the beginning of the roadrunnner scan """ return self.seq_run.value == 2 and self.seq_cnt.value > 25 @property def signal_present(self): """ Assert whether we have a signal over :attr:`.threshold` """ return self.ai.value > self.threshold def remove(self): """ Remove the blocker if it is not removed from the beam """ if self.enabled.value == 1: self.blocker.move_out() def block(self): """ Block the beam if the filter is not already inserted """ if self.enabled.value == 1: self.blocker.move_in() def process_event(self, *args, **kwargs): """ Process a change in state if either the sequencer or ai changes state """ #If we are not receiving signal if not self.signal_present: #If the sequencer is running, hard-fault if self.sequencer_running: self.hard_trip.put(1) self.block() #Otherwise, alert the soft trip else: self.soft_trip.put(1) #Otherwise if we are seeing a signal else: #Remove our blocker self.remove() #Reset our faults self.soft_trip.put(0) self.hard_trip.put(0) def run(self): """ Repeatedly run :func:`.process_event` """ while True: try: self.process_event() except KeyboardInterrupt: print("Watch has been manually interrupted!") break
class OffsetMirror(Device): """ X-Ray offset mirror class. This is for each individual mirror system used in the FEE and XRT. Controls for the pitch, and primary gantry x and y motors are included. When controlling the pitch motor, if the piezo is set to 'PID' mode, then the pitch mechanism is setup to first move the stepper as close to the desired position, then the piezo will kick in to constantly try and correct any positional changes. Parameters ---------- prefix : str The EPICS base PV of the pitch motor prefix_xy : str The EPICS base PV of the gantry x and y gantry motors xgantry_prefix : str The name of the horizontal gantry if not identical to the prefix name : str The name of the offset mirror """ # Pitch Motor pitch = FC(Pitch, "MIRR:{self.prefix}") # Gantry motors xgantry = FC(Gantry, "{self._prefix_xy}:X", gantry_prefix="{self._xgantry}", add_prefix=['suffix', 'gantry_prefix']) ygantry = FC(Gantry, "{self._prefix_xy}:Y", gantry_prefix='GANTRY:{self.prefix}:Y', add_prefix=['suffix', 'gantry_prefix']) # Transmission for Lightpath Interface transmission = 1.0 _default_read_attrs = [ 'pitch.readback', 'xgantry.readback', 'xgantry.gantry_difference' ] _default_configuration_attrs = ['ygantry.setpoint'] def __init__(self, prefix, *, prefix_xy=None, xgantry_prefix=None, **kwargs): # Handle prefix mangling self._prefix_xy = prefix_xy or prefix self._xgantry = xgantry_prefix or 'GANTRY:' + prefix + ':X' super().__init__(prefix, **kwargs) @property def inserted(self): """ Treat OffsetMirror as always inserted """ return True @property def removed(self): """ Treat OffsetMirror as always inserted """ return False