From 4ecee902b12d09eb5b8c6a1ac50f0c95e0993b0d Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Thu, 16 May 2019 10:56:00 -0400 Subject: [PATCH 1/4] added motor_group_ioc_sim --- .travis.yml | 2 + nslsii/iocs/epics_motor_record.py | 166 ++++++++++++++++++ nslsii/iocs/motor_group_ioc_sim.py | 44 +++++ nslsii/tests/temperature_controllers_test.py | 4 +- .../{iocs => }/tests/test_epstwostate_ioc.py | 1 - nslsii/tests/test_motorgroup_ioc.py | 82 +++++++++ 6 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 nslsii/iocs/epics_motor_record.py create mode 100644 nslsii/iocs/motor_group_ioc_sim.py rename nslsii/{iocs => }/tests/test_epstwostate_ioc.py (98%) create mode 100644 nslsii/tests/test_motorgroup_ioc.py diff --git a/.travis.yml b/.travis.yml index 6f4c2e21..f8c98fc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ env: - secure: "FhNkkbod0Wc/zUf9cTvwziAYHcjfte2POf+hoVSmC+v/RcYKCNCo+mGGMhF9F4KyC2nzvulfzow7YXoswZqav4+TEEu+mpuPaGlf9aqp8V61eij8MVTwonzQEYmHAy3KatwXxyvvhQpfj3gOuDVolfOg2MtNZi6QERES4E1sjOn714fx2HkVxqH2Y8/PF/FzzGeJaRlVaVci0EdIJ5Ss5c5SjO6JGgxj4hzhTPHjTaLjdLHlVhuB9Yatl80zbhGriljLcDQTHmoSODwBpAh5YLDUZq6B9vomaNB9Hb3e0D5gItjOdj53v6AsHU8LkncZMvsgJgh2sZZqMO6nkpHcYPwJgbPbKd3RtVlk6Kg/tvKQk0rMcxl5fFFeD2i9POnANg/xJsKN6yAEY3kaRwQtajQmlcicSa/wdwv9NhUTtBmA/mnyzxHbQXrB0bEc2P2QVu7U8en6dWaOAqc1VCMrWIhp2ADNWb7JZhYj70TgmExIU3UH8qlMb6dyx50SJUE9waJj3fiiZVkjh+E568ZRSMvL9n+bLlFt4uDT4AysSby6cj+zjfNViKFstTAqjyd5VJEvCoUu73vNzWEiWFtEvKKVL1P3pbLN/G3aSSJMa5fc1o+2lRUwdwNNOOdH6iKBDZGNpE8nGDlTP2b2dhFyEt8nICKJhbgU208jhyyH8Vk=" script: + - export OPHYD_CONTROL_LAYER=caproto + - python -m nslsii.iocs.motor_group_ioc_sim - coverage run -m pytest # Run the tests and check for test coverage. - coverage report -m # Generate test coverage report. - codecov # Upload the report to codecov. diff --git a/nslsii/iocs/epics_motor_record.py b/nslsii/iocs/epics_motor_record.py new file mode 100644 index 00000000..683a48ad --- /dev/null +++ b/nslsii/iocs/epics_motor_record.py @@ -0,0 +1,166 @@ +from caproto.server import pvproperty, PVGroup +from caproto import ChannelType + +from threading import Lock + + +class EpicsMotorRecord(PVGroup): + """ + Simulates EPICS motor record. + """ + + def __init__(self, prefix, *, ioc, **kwargs): + super().__init__(prefix, **kwargs) + self.ioc = ioc + + _dir_states = ['neg', 'pos'] + _false_true_states = ['False', 'True'] + + _step_size = 0.1 + + # position + + _upper_alarm_limit = 10.0 + _lower_alarm_limit = -10.0 + + _upper_warning_limit = 9.0 + _lower_warning_limit = -9.0 + + _upper_ctrl_limit = 11.0 + _lower_ctrl_limit = -11.0 + + _egu = 'mm' + + _precision = 3 + + user_readback = pvproperty(value=0.0, read_only=True, + dtype=ChannelType.DOUBLE, + upper_alarm_limit=_upper_alarm_limit, + lower_alarm_limit=_lower_alarm_limit, + upper_warning_limit=_upper_warning_limit, + lower_warning_limit=_lower_warning_limit, + upper_ctrl_limit=_upper_ctrl_limit, + lower_ctrl_limit=_lower_ctrl_limit, + units=_egu, + precision=_precision, + name='.RBV') + user_setpoint = pvproperty(value=0.0, + dtype=ChannelType.DOUBLE, + upper_alarm_limit=_upper_alarm_limit, + lower_alarm_limit=_lower_alarm_limit, + upper_warning_limit=_upper_warning_limit, + lower_warning_limit=_lower_warning_limit, + upper_ctrl_limit=_upper_ctrl_limit, + lower_ctrl_limit=_lower_ctrl_limit, + units=_egu, + precision=_precision, + name='.VAL') + + putter_lock = Lock() + + # calibration dial <--> user + + user_offset = pvproperty(value=0.0, read_only=True, + dtype=ChannelType.DOUBLE, + name='.OFF') + + user_offset_dir = pvproperty(value=_dir_states[1], + enum_strings=_dir_states, + dtype=ChannelType.ENUM, + name='.DIR') + + offset_freeze_switch = pvproperty(value=_false_true_states[0], + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, + name='.FOFF') + set_use_switch = pvproperty(value=_false_true_states[0], + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, + name='.SET') + + # configuration + + _velocity = 1. + _acceleration = 3. + + velocity = pvproperty(value=_velocity, read_only=True, + dtype=ChannelType.DOUBLE, + name='.VELO') + acceleration = pvproperty(value=_acceleration, read_only=True, + dtype=ChannelType.DOUBLE, + name='.ACCL') + motor_egu = pvproperty(value=_egu, read_only=True, + dtype=ChannelType.STRING, + name='.EGU') + + # motor status + + motor_is_moving = pvproperty(value='False', read_only=True, + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, + name='.MOVN') + motor_done_move = pvproperty(value='False', read_only=False, + enum_strings=_false_true_states, + dtype=ChannelType.ENUM, + name='.DMOV') + + high_limit_switch = pvproperty(value=0, read_only=True, + dtype=ChannelType.INT, + name='.HLS') + low_limit_switch = pvproperty(value=0, read_only=True, + dtype=ChannelType.INT, + name='.LLS') + + direction_of_travel = pvproperty(value=_dir_states[1], + enum_strings=_dir_states, + dtype=ChannelType.ENUM, + name='.TDIR') + + # commands + + _cmd_states = ['False', 'True'] + + motor_stop = pvproperty(value=_cmd_states[0], + enum_strings=_cmd_states, + dtype=ChannelType.ENUM, + name='.STOP') + home_forward = pvproperty(value=_cmd_states[0], + enum_strings=_cmd_states, + dtype=ChannelType.ENUM, + name='.HOMF') + home_reverse = pvproperty(value=_cmd_states[0], + enum_strings=_cmd_states, + dtype=ChannelType.ENUM, + name='.HOMR') + + # Methods + + @user_setpoint.startup + async def user_setpoint(self, instance, async_lib): + instance.ev = async_lib.library.Event() + instance.async_lib = async_lib + + @user_setpoint.putter + async def user_setpoint(self, instance, value): + + if self.putter_lock.locked() is True: + return instance.value + else: + self.putter_lock.acquire() + + p0 = instance.value + dwell = self._step_size/self._velocity + N = max(1, int((value - p0) / self._step_size)) + + await self.motor_done_move.write(value='False') + + for j in range(N): + new_value = p0 + self._step_size*(j+1) + await instance.async_lib.library.sleep(dwell) + await self.user_readback.write(value=new_value) + + await self.motor_done_move.write(value='True') + + self.putter_lock.release() + + return value diff --git a/nslsii/iocs/motor_group_ioc_sim.py b/nslsii/iocs/motor_group_ioc_sim.py new file mode 100644 index 00000000..0503997f --- /dev/null +++ b/nslsii/iocs/motor_group_ioc_sim.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +from caproto.server import PVGroup, ioc_arg_parser, run + +from epics_motor_record import EpicsMotorRecord + + +class MotorGroupIOC(PVGroup): + """ + Simulates a group of EPICS motor records. + """ + + def __init__(self, prefix, *, groups, **kwargs): + super().__init__(prefix, **kwargs) + self.groups = groups + + +def create_ioc(prefix, axes, **ioc_options): + + groups = {} + + ioc = MotorGroupIOC(prefix=prefix, groups=groups, **ioc_options) + + for group_prefix in axes: + groups[group_prefix] = EpicsMotorRecord(f'{prefix}{group_prefix}', + ioc=ioc) + + for prefix, group in groups.items(): + ioc.pvdb.update(**group.pvdb) + + return ioc + + +if __name__ == '__main__': + ioc_options, run_options = ioc_arg_parser( + default_prefix='test-Ax:', + desc=MotorGroupIOC.__doc__, + ) + + axes = {"HGMtr", "HCMtr", "VGMtr", "VCMtr", + "IMtr", "OMtr", "TMtr", "BMtr",} + + ioc = create_ioc(axes=axes, **ioc_options) + + run(ioc.pvdb, **run_options) diff --git a/nslsii/tests/temperature_controllers_test.py b/nslsii/tests/temperature_controllers_test.py index 33b7978b..21aedfd6 100644 --- a/nslsii/tests/temperature_controllers_test.py +++ b/nslsii/tests/temperature_controllers_test.py @@ -6,6 +6,7 @@ import os import sys import pytest +import time @pytest.fixture @@ -27,13 +28,14 @@ def test_Eurotherm(RE): # Start up an IOC based on the thermo_sim device in caproto.ioc_examples ioc_process = subprocess.Popen([sys.executable, '-m', - 'caproto.tests.example_runner', 'caproto.ioc_examples.thermo_sim'], stdout=stdout, stdin=stdin, env=os.environ) print(f'caproto.ioc_examples.thermo_sim is now running') + time.sleep(5) + # Wrap the rest in a try-except to ensure the ioc is killed before exiting try: euro = Eurotherm('thermo:', name='euro') diff --git a/nslsii/iocs/tests/test_epstwostate_ioc.py b/nslsii/tests/test_epstwostate_ioc.py similarity index 98% rename from nslsii/iocs/tests/test_epstwostate_ioc.py rename to nslsii/tests/test_epstwostate_ioc.py index a30dabfb..80346b96 100644 --- a/nslsii/iocs/tests/test_epstwostate_ioc.py +++ b/nslsii/tests/test_epstwostate_ioc.py @@ -44,7 +44,6 @@ def test_epstwostate_ioc(): stdin = None ioc_process = subprocess.Popen([sys.executable, '-m', - 'caproto.tests.example_runner', 'nslsii.iocs.eps_two_state_ioc_sim'], stdout=stdout, stdin=stdin, env=os.environ) diff --git a/nslsii/tests/test_motorgroup_ioc.py b/nslsii/tests/test_motorgroup_ioc.py new file mode 100644 index 00000000..1a15298a --- /dev/null +++ b/nslsii/tests/test_motorgroup_ioc.py @@ -0,0 +1,82 @@ +# import os +import pytest +# import subprocess +# import sys +import time + +from ophyd import Device, EpicsMotor +from ophyd import Component as Cpt +from ophyd.device import create_device_from_components + +from caproto.sync.client import read + + +def slit(name, axes=None, *, docstring=None, default_read_attrs=None, + default_configuration_attrs=None): + + components = {} + for name, PV_suffix in axes.items(): + components[name] = Cpt(EpicsMotor, PV_suffix, name=name) + + new_class = create_device_from_components( + name, docstring=docstring, default_read_attrs=default_read_attrs, + default_configuration_attrs=default_configuration_attrs, + base_class=Device, **components) + + return new_class + + +@pytest.fixture(scope='class') +def ioc_sim(request): + + ''' + # setup code + + stdout = subprocess.PIPE + stdin = None + + ioc_process = subprocess.Popen([sys.executable, '-m', + 'nslsii.iocs.motor_group_ioc_sim'], + stdout=stdout, stdin=stdin, + env=os.environ) + + print(f'nslsii.iocs.motor_group_ioc_sim is now running') + + time.sleep(5) + ''' + + FourBladeSlits = slit(name='FourBladeSlits', + axes={'hg': '-Ax:HGMtr', 'hc': '-Ax:HCMtr', + 'vg': '-Ax:VGMtr', 'vc': '-Ax:VCMtr', + 'inb': '-Ax:IMtr', 'out': '-Ax:OMtr', + 'top': '-Ax:TMtr', 'bot': '-Ax:BMtr'}, + docstring='Four Blades Slits') + + slits = FourBladeSlits('test', name='slits') + + time.sleep(5) + + request.cls.slits = slits + + yield + + # teardown code + +# ioc_process.terminate() + + +@pytest.mark.usefixtures('ioc_sim') +class TestIOC: + + def test_caproto_level(self): + + res = read('test-Ax:HCMtr.VELO') + velocity_val = res.data[0] + assert velocity_val == 1.0 + + def test_device_level(self): + + assert(hasattr(self.slits, 'hc')) + + velocity_val = self.slits.hc.velocity.get() + assert velocity_val == 1 From c4014eccc60789cb7c5c646e5bd521da947effd2 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Thu, 16 May 2019 11:12:39 -0400 Subject: [PATCH 2/4] updated travis --- .travis.yml | 1 - nslsii/iocs/motor_group_ioc_sim.py | 4 ++-- nslsii/tests/test_motorgroup_ioc.py | 14 ++++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index f8c98fc9..402ff29a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ env: script: - export OPHYD_CONTROL_LAYER=caproto - - python -m nslsii.iocs.motor_group_ioc_sim - coverage run -m pytest # Run the tests and check for test coverage. - coverage report -m # Generate test coverage report. - codecov # Upload the report to codecov. diff --git a/nslsii/iocs/motor_group_ioc_sim.py b/nslsii/iocs/motor_group_ioc_sim.py index 0503997f..9fd2dea9 100644 --- a/nslsii/iocs/motor_group_ioc_sim.py +++ b/nslsii/iocs/motor_group_ioc_sim.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from caproto.server import PVGroup, ioc_arg_parser, run -from epics_motor_record import EpicsMotorRecord +from nslsii.iocs.epics_motor_record import EpicsMotorRecord class MotorGroupIOC(PVGroup): @@ -37,7 +37,7 @@ def create_ioc(prefix, axes, **ioc_options): ) axes = {"HGMtr", "HCMtr", "VGMtr", "VCMtr", - "IMtr", "OMtr", "TMtr", "BMtr",} + "IMtr", "OMtr", "TMtr", "BMtr", } ioc = create_ioc(axes=axes, **ioc_options) diff --git a/nslsii/tests/test_motorgroup_ioc.py b/nslsii/tests/test_motorgroup_ioc.py index 1a15298a..283430cf 100644 --- a/nslsii/tests/test_motorgroup_ioc.py +++ b/nslsii/tests/test_motorgroup_ioc.py @@ -1,7 +1,7 @@ -# import os +import os import pytest -# import subprocess -# import sys +import subprocess +import sys import time from ophyd import Device, EpicsMotor @@ -29,7 +29,6 @@ def slit(name, axes=None, *, docstring=None, default_read_attrs=None, @pytest.fixture(scope='class') def ioc_sim(request): - ''' # setup code stdout = subprocess.PIPE @@ -37,13 +36,12 @@ def ioc_sim(request): ioc_process = subprocess.Popen([sys.executable, '-m', 'nslsii.iocs.motor_group_ioc_sim'], - stdout=stdout, stdin=stdin, - env=os.environ) + stdout=stdout, stdin=stdin, + env=os.environ) print(f'nslsii.iocs.motor_group_ioc_sim is now running') time.sleep(5) - ''' FourBladeSlits = slit(name='FourBladeSlits', axes={'hg': '-Ax:HGMtr', 'hc': '-Ax:HCMtr', @@ -62,7 +60,7 @@ def ioc_sim(request): # teardown code -# ioc_process.terminate() + ioc_process.terminate() @pytest.mark.usefixtures('ioc_sim') From c5fabd6f87c79e5abd3d07db2753ebdbdf0e47c6 Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Thu, 16 May 2019 11:27:26 -0400 Subject: [PATCH 3/4] added create_device_from_components --- nslsii/tests/test_motorgroup_ioc.py | 39 ++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/nslsii/tests/test_motorgroup_ioc.py b/nslsii/tests/test_motorgroup_ioc.py index 283430cf..f2094ca6 100644 --- a/nslsii/tests/test_motorgroup_ioc.py +++ b/nslsii/tests/test_motorgroup_ioc.py @@ -5,18 +5,51 @@ import time from ophyd import Device, EpicsMotor -from ophyd import Component as Cpt -from ophyd.device import create_device_from_components +from ophyd import Component +# from ophyd.device import create_device_from_components from caproto.sync.client import read +from collections import OrderedDict + + +def create_device_from_components(name, *, docstring=None, + default_read_attrs=None, + default_configuration_attrs=None, + base_class=Device, class_kwargs=None, + **components): + + if docstring is None: + docstring = f'{name} Device' + + if not isinstance(base_class, tuple): + base_class = (base_class, ) + + if class_kwargs is None: + class_kwargs = {} + + clsdict = OrderedDict( + __doc__=docstring, + _default_read_attrs=default_read_attrs, + _default_configuration_attrs=default_configuration_attrs + ) + + for attr, component in components.items(): + if not isinstance(component, Component): + raise ValueError(f'Attribute {attr} is not a Component. ' + f'It is of type {type(component).__name__}') + + clsdict[attr] = component + + return type(name, base_class, clsdict, **class_kwargs) + def slit(name, axes=None, *, docstring=None, default_read_attrs=None, default_configuration_attrs=None): components = {} for name, PV_suffix in axes.items(): - components[name] = Cpt(EpicsMotor, PV_suffix, name=name) + components[name] = Component(EpicsMotor, PV_suffix, name=name) new_class = create_device_from_components( name, docstring=docstring, default_read_attrs=default_read_attrs, From 0e1ccc2128ab0e4ebb4e5e946355d59c2ce4f68b Mon Sep 17 00:00:00 2001 From: nmalitsky Date: Fri, 17 May 2019 13:25:14 -0400 Subject: [PATCH 4/4] added command-line args --- nslsii/iocs/motor_group_ioc_sim.py | 30 ++++++++++++++++++-------- nslsii/tests/test_motorgroup_ioc.py | 33 ++++++++++++++++++----------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/nslsii/iocs/motor_group_ioc_sim.py b/nslsii/iocs/motor_group_ioc_sim.py index 9fd2dea9..b69c19e7 100644 --- a/nslsii/iocs/motor_group_ioc_sim.py +++ b/nslsii/iocs/motor_group_ioc_sim.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from caproto.server import PVGroup, ioc_arg_parser, run +from caproto.server import PVGroup, template_arg_parser, run from nslsii.iocs.epics_motor_record import EpicsMotorRecord @@ -18,11 +18,16 @@ def create_ioc(prefix, axes, **ioc_options): groups = {} - ioc = MotorGroupIOC(prefix=prefix, groups=groups, **ioc_options) + mg_prefix = prefix.replace('{', '{'*2, 1) + ioc = MotorGroupIOC(prefix=mg_prefix, groups=groups, **ioc_options) + + rec_mg_prefix = prefix.replace('{', '{'*4, 1) for group_prefix in axes: - groups[group_prefix] = EpicsMotorRecord(f'{prefix}{group_prefix}', - ioc=ioc) + rec_group_prefix = group_prefix.replace('}', '}'*4, 1) + record_prefix = rec_mg_prefix + rec_group_prefix + groups[rec_group_prefix] = EpicsMotorRecord(record_prefix, + ioc=ioc) for prefix, group in groups.items(): ioc.pvdb.update(**group.pvdb) @@ -31,14 +36,21 @@ def create_ioc(prefix, axes, **ioc_options): if __name__ == '__main__': - ioc_options, run_options = ioc_arg_parser( - default_prefix='test-Ax:', + + parser, split_args = template_arg_parser( + default_prefix='test{tst-Ax:', desc=MotorGroupIOC.__doc__, ) - axes = {"HGMtr", "HCMtr", "VGMtr", "VCMtr", - "IMtr", "OMtr", "TMtr", "BMtr", } + axes_help = 'Comma-separated list of axes' - ioc = create_ioc(axes=axes, **ioc_options) + parser.add_argument('--axes', help=axes_help, + required=True, type=str) + args = parser.parse_args() + ioc_options, run_options = split_args(args) + + axes = [x.strip() for x in args.axes.split(',')] + + ioc = create_ioc(axes=axes, **ioc_options) run(ioc.pvdb, **run_options) diff --git a/nslsii/tests/test_motorgroup_ioc.py b/nslsii/tests/test_motorgroup_ioc.py index f2094ca6..5c45258c 100644 --- a/nslsii/tests/test_motorgroup_ioc.py +++ b/nslsii/tests/test_motorgroup_ioc.py @@ -59,6 +59,11 @@ def slit(name, axes=None, *, docstring=None, default_read_attrs=None, return new_class +prefix = 'test{tst-Ax:' +axes = {'hg': 'HG}Mtr', 'hc': 'HC}Mtr', 'vg': 'VG}Mtr', 'vc': 'VC}Mtr', + 'inb': 'I}Mtr', 'out': 'O}Mtr', 'top': 'T}Mtr', 'bot': 'B}Mtr'} + + @pytest.fixture(scope='class') def ioc_sim(request): @@ -67,8 +72,13 @@ def ioc_sim(request): stdout = subprocess.PIPE stdin = None - ioc_process = subprocess.Popen([sys.executable, '-m', - 'nslsii.iocs.motor_group_ioc_sim'], + axes_str = '' + for name, PV_suffix in axes.items(): + axes_str += PV_suffix + ',' + + ioc_process = subprocess.Popen([sys.executable, + '-m', 'nslsii.iocs.motor_group_ioc_sim', + '--axes', axes_str], stdout=stdout, stdin=stdin, env=os.environ) @@ -77,13 +87,10 @@ def ioc_sim(request): time.sleep(5) FourBladeSlits = slit(name='FourBladeSlits', - axes={'hg': '-Ax:HGMtr', 'hc': '-Ax:HCMtr', - 'vg': '-Ax:VGMtr', 'vc': '-Ax:VCMtr', - 'inb': '-Ax:IMtr', 'out': '-Ax:OMtr', - 'top': '-Ax:TMtr', 'bot': '-Ax:BMtr'}, + axes=axes, docstring='Four Blades Slits') - slits = FourBladeSlits('test', name='slits') + slits = FourBladeSlits(prefix, name='slits') time.sleep(5) @@ -101,13 +108,15 @@ class TestIOC: def test_caproto_level(self): - res = read('test-Ax:HCMtr.VELO') - velocity_val = res.data[0] - assert velocity_val == 1.0 + for name, PV_suffix in axes.items(): + pvname = prefix + PV_suffix + '.VELO' + res = read(pvname) + velocity_val = res.data[0] + assert velocity_val == 1.0 def test_device_level(self): - assert(hasattr(self.slits, 'hc')) + assert(hasattr(self.slits, 'hg')) - velocity_val = self.slits.hc.velocity.get() + velocity_val = self.slits.hg.velocity.get() assert velocity_val == 1