class SimpleIOC(PVGroup): """ An IOC with three uncoupled read/writable PVs Scalar PVs ---------- A (int) B (float) Vectors PVs ----------- C (vector of int) """ A = pvproperty(value=1, doc='An integer') B = pvproperty(value=2.0, doc='A float') C = pvproperty(value=[1, 2, 3], doc='An array of integers')
def create_pvproperty(attr, record): startup_fields[attr] = {} default_value = default_values.get(record.record_type, 0) if record.record_type == 'waveform': ftvl = record.fields.get('FTVL', 'SHORT') nelm = int(record.fields.get('NELM', 1)) default_value = default_value[ftvl] if ftvl != 'CHAR': default_value = default_value * nelm if 'VAL' in record.fields: value = type(default_value)(record.fields['VAL']) else: value = default_value prop = pvproperty( name=record.pvname, value=value, record=record.record_type, ) for field_name, value in record.fields.items(): if field_name != 'VAL': startup_fields[attr][field_name] = value for alias_idx, alias in enumerate(record.aliases): logger.debug('TODO - aliases: %s %s', record, alias) class_dict[attr] = prop return prop
class IOInterruptIOC(PVGroup): keypress = pvproperty(value=['']) # NOTE the decorator used here: @keypress.startup async def keypress(self, instance, async_lib): # This method will be called when the server starts up. print('* keypress method called at server startup') queue = async_lib.ThreadsafeQueue() # Start a separate thread that monitors keyboard input, telling it to # put new values into our async-friendly queue thread = threading.Thread(target=start_io_interrupt_monitor, daemon=True, kwargs=dict(new_value_callback=queue.put)) thread.start() # Loop and grab items from the queue one at a time while True: value = await queue.async_get() print(f'Saw new value on async side: {value!r}') # Propagate the keypress to the EPICS PV, triggering any monitors # along the way await self.keypress.write(str(value))
class FakeMotor(PVGroup): motor = pvproperty(value=0.0, name='', record='motor', precision=3) def __init__(self, *args, velocity=0.1, precision=3, acceleration=1.0, resolution=1e-6, tick_rate_hz=10., **kwargs): super().__init__(*args, **kwargs) self._have_new_position = False self.tick_rate_hz = tick_rate_hz self.defaults = { 'velocity': velocity, 'precision': precision, 'acceleration': acceleration, 'resolution': resolution, } @motor.startup async def motor(self, instance, async_lib): # Start the simulator: await motor_record_simulator( self.motor, async_lib, tick_rate_hz=self.tick_rate_hz, )
class RandomWalkIOC(PVGroup): dt = pvproperty(value=3.0) x = pvproperty(value=0.0) @x.startup async def x(self, instance, async_lib): 'Periodically update the value' while True: # compute next value x = self.x.value + 2 * random.random() - 1 # update the ChannelData instance and notify any subscribers await instance.write(value=x) # Let the async library wait for the next iteration await async_lib.library.sleep(self.dt.value)
class DynamicLVGroup(LVGroup): device_list_message_cls = None device_cls = None devices = pvproperty(value=[], dtype=ChannelType.STRING, max_length=10000) async def update(self): device_names = (await self.parent.get(self.device_list_message_cls())).data newpvs = {} for name in device_names: device = self.device_cls(name, parent=self) newpvs.update({ f'{self.prefix}{name}.{value.pvspec.name}': value for key, value in device.attr_pvdb.items() if f'{self.prefix}{name}.{key}' not in self.pvdb }) # newpvs.update({f'{self.prefix}{name}.{key}': value for key, value in device.attr_pvdb.items() # if f'{self.prefix}{name}.{key}' not in self.pvdb}) self.pvdb.update(newpvs) return newpvs @devices.getter async def devices(self, instance): await self.update() return list(self.pvdb.keys())
class Motor(PVGroup): motor = pvproperty(value=0.0, name='', record='motor', precision=3) def __init__(self, *args, position=0.0, velocity=1.0, precision=3, acceleration=1.0, resolution=1e-6, user_limits=(0.0, 100.0), tick_rate_hz=10., **kwargs): super().__init__(*args, **kwargs) self.tick_rate_hz = tick_rate_hz self.defaults = { "position": position, "velocity": velocity, "precision": precision, "acceleration": acceleration, "resolution": resolution, "user_limits": tuple(user_limits), } @motor.startup async def motor(self, instance, async_lib): # Start the simulator: await motor_record_simulator( self.motor, async_lib, self.defaults, tick_rate_hz=self.tick_rate_hz, )
class _JitterDetector(PVGroup): det = pvproperty(value=[0], dtype=float, read_only=True) @det.getter async def det(self, instance): return (await self._read(instance)) mtr = pvproperty(value=[0], dtype=float) exp = pvproperty(value=[1], dtype=float) @exp.putter async def exp(self, instance, value): value = np.clip(value, a_min=0, a_max=None) return value
class WorkerThreadIOC(PVGroup): request = pvproperty(value=0, max_length=1) # NOTE the decorator used here: @request.startup async def request(self, instance, async_lib): # This method will be called when the server starts up. print('* request method called at server startup') self.request_queue = async_lib.ThreadsafeQueue() self.Event = async_lib.Event # Start a separate thread that consumes requests. thread = threading.Thread( target=worker, daemon=True, kwargs=dict(request_queue=self.request_queue)) thread.start() @request.putter async def request(self, instance, value): print(f'Sending the request {value} to the worker.') event = self.Event() await self.request_queue.async_put((event, value)) # The worker calls Event.set() when the work is done. await event.wait() return value
class CustomWrite(PVGroup): """ When a PV is written to, write the new value into a file as a string. """ DIRECTORY = temp_path async def my_write(self, instance, value): # Compose the filename based on whichever PV this is. pv_name = instance.pvspec.attr # 'A' or 'B', for this IOC with open(self.DIRECTORY / pv_name, 'w') as f: f.write(str(value)) print(f'Wrote {value} to {self.DIRECTORY / pv_name}') return value A = pvproperty(put=my_write, value=[0]) B = pvproperty(put=my_write, value=[0])
class SimpleIOC(PVGroup): """ An IOC with three uncoupled PVs Scalar PVs ---------- running (int) rbv (int) val (int) """ running = pvproperty(value=0) rbv = pvproperty(value=0) val = pvproperty(value=0) @running.startup async def running(self, instance, async_lib): 'Periodically update the value' await self.running.write(1) self.put_queue = async_lib.ThreadsafeQueue() self.get_queue = async_lib.ThreadsafeQueue() while True: entry = await self.put_queue.async_get() pv = entry['pv'] value = entry['value'] if pv == 'rbv': print("process a 'rbv' put request from a device") if pv == 'val': print("process a 'val' put request from a device") @val.putter async def val(self, instance, value): """ called when the a new value is written into "val" PV """ print( f"Server: 'rbv' Got 'put' request from outside: new value is {value} and type {type(value)}" ) print('responding to the put request...') @val.getter async def val(self, instance): """ called when the a new value is readby a client """ print(f"Server: 'rbv' Got 'get' request from outside:") print('responding to the get request...')
class PointDetector(PVGroup): """ A coupled motor and point detector. The measurement is a noise-free Gaussian centered around 0 with a σ of 5 exp controls how long the 'exposure' takes Readonly PVs ------------ det -> the detector value Settable PVs ------------ mtr -> motor position exp -> exposure time """ mtr = pvproperty(value=0, dtype=float) exp = pvproperty(value=1, dtype=float) @exp.putter async def exp(self, instance, value): value = np.clip(value, a_min=0, a_max=None) return value det = pvproperty(value=0, dtype=float, read_only=True) @det.getter async def det(self, instance): exposure_time = self.exp.value sigma = 5 center = 0 c = -1 / (2 * sigma * sigma) m = self.mtr.value return exposure_time * np.exp((c * (m - center)**2)) acq = pvproperty(value=0, dtype=float) busy = pvproperty(value=0, read_only=True) @acq.putter async def acq(self, instance, value): await self.busy.write(1) await asyncio.sleep(self.exp.value) await self.busy.write(0) return 0
class MCAROIGroup(PVGroup): label = pvproperty(value='label', name='NM') count = pvproperty(value=1, name='', read_only=True) net_count = pvproperty(name='N', dtype=unknown, read_only=True) preset_count = pvproperty(name='P', dtype=unknown) is_preset = pvproperty(name='IP', dtype=unknown) bkgnd_chans = pvproperty(name='BG', dtype=unknown) hi_chan = pvproperty(name='HI', dtype=unknown) lo_chan = pvproperty(name='LO', dtype=unknown)
class FakePMPSGroup(PVGroup): """ Fake PV group for simulating incoming PMPS commands. """ t_des = pvproperty(value=0.1, name='T_DES', record='ao', upper_ctrl_limit=1.0, lower_ctrl_limit=0.0, doc='PMPS requested transmission') run = pvproperty(value='False', name='RUN', record='bo', enum_strings=['False', 'True'], doc='PMPS Change transmission command', dtype=ChannelType.ENUM)
class IOInterruptIOC(PVGroup): t1 = pvproperty(value=2.0) image = pvproperty(value=np.random.randint(0, 255, image_shape, dtype='uint8').flatten(), dtype=bytes) @t1.startup async def t1(self, instance, async_lib): # Loop and grab items from the queue one at a time while True: await self.t1.write(time.monotonic()) await self.image.write( np.random.randint(0, 255, image_shape, dtype='uint8').flatten()) await async_lib.library.sleep(0.1)
class SpeechIOC(PVGroup): language = pvproperty(value=['en-US'], doc='Language to use', dtype=caproto.ChannelType.ENUM, enum_strings=list(speech.get_languages()), string_encoding='utf-8') speak = pvproperty(value=['text'], doc='Text to speak', string_encoding='utf-8') rate = pvproperty(value=[0.5], doc='Normalized speech rate') speaking = pvproperty(value=[0]) @speak.startup async def speak(self, instance, async_lib): self.voices = AVSpeechSynthesisVoice.speechVoices() self.synthesizer = AVSpeechSynthesizer.new() @speak.putter async def speak(self, instance, value): if isinstance(value, (list, tuple)): value, = value language = self.language.value[0] voice = AVSpeechSynthesisVoice.voiceWithLanguage_(language) for voice in self.voices: if (f'Language: {language}' in str(voice) and 'compact' not in str(voice)): print('Chose voice:', voice) break self.voice = voice utterance = AVSpeechUtterance.speechUtteranceWithString_(value) rate = self.rate.value[0] utterance.rate = rate utterance.voice = self.voice utterance.useCompactVoice = False print(f'Saying {value!r} in {language} at rate {rate}') self.synthesizer.speakUtterance_(utterance) @speaking.startup async def speaking(self, instance, async_lib): while True: await self.speaking.write(value=[speech.is_speaking()]) await async_lib.library.sleep(0.1)
class UndulatorPV(PVGroup): useg_proc = pvproperty(value=0, name=':ConvertK2Gap.PROC') #Go command #gapact = pvproperty(value=0.0, name=':GapAct') #gapdes = pvproperty(value=0.0, name=':GapDes') kact = pvproperty(value=0.0, name=':KAct', read_only=True) kdes = pvproperty(value=0.0, name=':KDes') taper_des = pvproperty(value=0.0, name=':TaperDes') taper_act = pvproperty(value=0.0, name=':TaperAct', read_only=True) symm_act = pvproperty(value=0.0, name=':SymmetryAct', read_only=True) serial_n = pvproperty(value=0.0, name=':SerialNum') def __init__(self, device_name, element_name, change_callback, initial_values, *args, **kwargs): super().__init__(*args, **kwargs) self.device_name = device_name self.element_name = element_name self.kact._data['value'] = float(initial_values['kact']) self.kdes._data['value'] = float(initial_values['kact']) self.taper_des._data['value'] = 0 self.symm_act._data['value'] = 0 self.change_callback = change_callback @useg_proc.putter async def useg_proc(self, instance, value): ioc = instance.group await asyncio.sleep(0.2) await ioc.kact.write(ioc.kdes.value) await self.change_callback(self, ioc.kact.value)
class TwinCATStatePositioner(PVGroup): _delay = 0.2 state_get = pvproperty(value=0, name='GET_RBV') state_set = pvproperty(value=0, name='SET') error = pvproperty(value=0.0, name='ERR_RBV') error_id = pvproperty(value=0, name='ERRID_RBV') error_message = pvproperty(dtype=str, name='ERRMSG_RBV') busy = pvproperty(value=0, name='BUSY_RBV') done = pvproperty(value=0, name='DONE_RBV') reset_cmd = pvproperty_with_rbv(dtype=int, name='RESET') config = SubGroup(TwinCATStateConfigAll, prefix='') @state_set.startup async def state_set(self, instance, async_lib): self.async_lib = async_lib # Start as "out" and not unknown await self.state_get.write(1) @state_set.putter async def state_set(self, instance, value): await self.busy.write(1) await self.state_get.write(0) await self.async_lib.library.sleep(self._delay) await self.state_get.write(value) await self.busy.write(0) await self.done.write(1)
class XpsMotorFields(MotorFields): _record_type = 'xps8p' stop_pause_go = pvproperty(name='SPG', value='GO', dtype=caproto.ChannelType.ENUM, enum_strings=['STOP', 'PAUSE', 'GO'], doc='PCDS stop-pause-go variant of SPMG', read_only=False)
class IOInterruptIOC(PVGroup): t1 = pvproperty(value=2.0) image = pvproperty( value=np.random.randint(0, 256, image_shape, dtype=np.uint8).flatten(), dtype=bytes, ) @t1.scan(period=0.1) async def t1(self, instance, async_lib): # Loop and grab items from the queue one at a time await self.t1.write(time.monotonic()) value = np.random.randint(0, 256, image_shape, dtype=np.uint8).flatten() # caproto will not perform a copy in preprocess_value if you mark # the array as read-only by way of flags: value.flags.writeable = False await self.image.write(value=value)
class _JitterDetector(PVGroup): det = pvproperty(value=0, dtype=float, read_only=True) @det.getter async def det(self, instance): return (await self._read(instance)) mtr = pvproperty(value=0, dtype=float, precision=3, record='ai') exp = pvproperty(value=1, dtype=float) vel = pvproperty(value=1, dtype=float) mtr_tick_rate = pvproperty(value=10, dtype=float, units='Hz') @exp.putter async def exp(self, instance, value): value = np.clip(value, a_min=0, a_max=None) return value @mtr.startup async def mtr(self, instance, async_lib): instance.ev = async_lib.library.Event() instance.async_lib = async_lib @mtr.putter @no_reentry async def mtr(self, instance, value): # "tick" at 10Hz dwell = 1 / self.mtr_tick_rate.value disp = (value - instance.value) # compute the total movement time based an velocity total_time = abs(disp / self.vel.value) # compute how many steps, should come up short as there will # be a final write of the return value outside of this call N = int(total_time // dwell) for j in range(N): # hide a possible divide by 0 step_size = disp / N await instance.write(instance.value + step_size) await instance.async_lib.library.sleep(dwell) return value
class CudKlys(PVGroup): """ Represents the PVs used by the Klystron CUD. Every PV in here is just a static value, driven by the Klystron CUD MATLAB process. """ onbeam1 = pvproperty(value=0.0, name=':ONBEAM1') status = pvproperty(value=0.0, name=':STATUS') statusdesc = pvproperty(value='None', name=':STATUS.DESC', dtype=ChannelType.STRING) def __init__(self, device_name, element_name, initial_value, *args, **kwargs): super().__init__(*args, **kwargs) self.device_name = device_name self.element_name = element_name self.onbeam1._data['value'] = initial_value self.status._data['value'] = initial_value
class EnumIOC(PVGroup): """ An IOC with some enums. Each property here presents itself as a record with the expected fields over Channel Access. For ``bi`` and ``bo``, the ZNAM and ONAM fields hold the string equivalent values for 0 and 1. These are derived from the ``enum_strings`` keyword argument. That is, ``bo.ZNAM`` is "Zero Value", ``bo.ONAM`` is ``"One Value"``, such that ``caput bo 1`` would show it being set to ``"One Value"``. For the mbbi record, the ``ZRST`` (zero string) field, ``ONST`` (one string) field, and so on (up to 15), are similarly respected and mapped from the ``enum_strings`` keyword argument. Scalar PVs ---------- bo (enum) - a binary output (bo) record bi (enum) - a binary input (bi) record mbbi (enum) - a multi-bit binary input (mbbi) record """ bo = pvproperty(value='One Value', enum_strings=['Zero Value', 'One Value'], record='bo', dtype=ChannelType.ENUM) bi = pvproperty(value='a', enum_strings=['a', 'b'], record='bi', dtype=ChannelType.ENUM) mbbi = pvproperty(value='one', enum_strings=['zero', 'one', 'two', 'three', 'four'], record='mbbi', dtype=ChannelType.ENUM) # A new, easier syntax: enum_class = pvproperty( value=MyEnum.on, record='mbbi', )
class Motors(LVGroup): names = alsdac.ListMotors() for name in names: locals()[name] = SubGroup(Motor, prefix=name + '.') devices = pvproperty(value=[], dtype=str) @devices.getter async def devices(self, instance): return list(alsdac.ListMotors())
class DigitalInputOutputs(LVGroup): names = alsdac.ListDIOs() for name in names: locals()[name] = SubGroup(DigitalInputOutput, prefix=name + '.') devices = pvproperty(value=[], dtype=str) @devices.getter async def devices(self, instance): return list(alsdac.ListDIOs())
class SimpleIOC(PVGroup): """ An IOC with three uncoupled read/writable PVs. Scalar PVs ---------- A (int) B (float) Array PVs --------- C (array of int) """ A = pvproperty( value=1, doc='An integer', ) B = pvproperty(value=2.0, doc='A float') C = pvproperty(value=[1, 2, 3], doc='An array of integers (max length 3)')
class HeaterPV(PVGroup): power = pvproperty(value=0.0, name=':POWER', precision=1) def __init__(self, change_callback, initial_values, *args, **kwargs): super().__init__(*args, **kwargs) cav_gradient = initial_values[0] #Necessary? Might only need it for the cryo model self.power._data['value'] = initial_values[1] @power.putter async def power(self, instance, value)
class ReadingCounter(PVGroup): """ Count the number of times that a PV is read. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.tallies = collections.Counter() async def my_read(self, instance): pv_name = instance.pvspec.attr self.tallies.update({pv_name: 1}) print('tallies:', self.tallies) # The act of reading this PV changes its value! # Weird but sort of interesting. await instance.write(self.tallies[pv_name]) return instance.value A = pvproperty(get=my_read, value=[0]) B = pvproperty(get=my_read, value=[0])
class CameraIOC(PVGroup): acquire = pvproperty(value=[0], doc='Process to acquire an image', mock_record='bo') shape = pvproperty(value=[image_width, image_height], doc='Image dimensions', read_only=True) image = pvproperty(value=[0] * (image_width * image_height), doc='Image data', read_only=True) @acquire.putter async def acquire(self, instance, value): image = photos.capture_image() # resize to (width, height) image = image.resize((image_width, image_height)) # and convert to grayscale image_array = np.dot(np.asarray(image)[..., :3], [0.299, 0.587, 0.114]) await self.image.write(image_array.flatten().astype(np.uint32))
class StatsPlugin(PVGroup): """ Minimal AreaDetector stats plugin stand-in. BTPS will check the array counter update rate and centroid values. """ enable = pvproperty(value=1, name="SimEnable") array_counter = pvproperty(value=0, name="ArrayCounter_RBV") centroid_x = pvproperty(value=0.0, name="CentroidX_RBV") centroid_y = pvproperty(value=0.0, name="CentroidY_RBV") @enable.scan(period=1) async def enable(self, instance, async_lib): if self.enable.value == 0: return await self.array_counter.write(value=self.array_counter.value + 1) await self.centroid_x.write(value=random.random()) await self.centroid_y.write(value=random.random())