diff --git a/docs/api/instruments/oxfordinstruments/adapters.rst b/docs/api/instruments/oxfordinstruments/adapters.rst deleted file mode 100644 index 1ad8031e29..0000000000 --- a/docs/api/instruments/oxfordinstruments/adapters.rst +++ /dev/null @@ -1,11 +0,0 @@ -############################### -Oxford Instruments VISA Adapter -############################### - -.. autoclass:: pymeasure.instruments.oxfordinstruments.OxfordInstrumentsAdapter - :members: - :show-inheritance: - -.. autoclass:: pymeasure.instruments.oxfordinstruments.adapters.OxfordVISAError - :members: - :show-inheritance: diff --git a/docs/api/instruments/oxfordinstruments/base.rst b/docs/api/instruments/oxfordinstruments/base.rst new file mode 100644 index 0000000000..59da2dcc51 --- /dev/null +++ b/docs/api/instruments/oxfordinstruments/base.rst @@ -0,0 +1,11 @@ +################################## +Oxford Instruments Base Instrument +################################## + +.. autoclass:: pymeasure.instruments.oxfordinstruments.base.OxfordInstrumentsBase + :members: + :show-inheritance: + +.. autoclass:: pymeasure.instruments.oxfordinstruments.base.OxfordVISAError + :members: + :show-inheritance: diff --git a/docs/api/instruments/oxfordinstruments/index.rst b/docs/api/instruments/oxfordinstruments/index.rst index 6f86e1be88..a215bb4b62 100644 --- a/docs/api/instruments/oxfordinstruments/index.rst +++ b/docs/api/instruments/oxfordinstruments/index.rst @@ -9,7 +9,7 @@ This section contains specific documentation on the Oxford Instruments instrumen .. toctree:: :maxdepth: 2 - adapters + base ITC503 IPS120_10 PS120_10 diff --git a/pymeasure/instruments/oxfordinstruments/__init__.py b/pymeasure/instruments/oxfordinstruments/__init__.py index db4d8e9c18..7a9fc988c1 100644 --- a/pymeasure/instruments/oxfordinstruments/__init__.py +++ b/pymeasure/instruments/oxfordinstruments/__init__.py @@ -23,7 +23,6 @@ # -from .adapters import OxfordInstrumentsAdapter from .itc503 import ITC503 from .ips120_10 import IPS120_10 from .ps120_10 import PS120_10 diff --git a/pymeasure/instruments/oxfordinstruments/adapters.py b/pymeasure/instruments/oxfordinstruments/base.py similarity index 77% rename from pymeasure/instruments/oxfordinstruments/adapters.py rename to pymeasure/instruments/oxfordinstruments/base.py index bbfe94314e..15946225c5 100644 --- a/pymeasure/instruments/oxfordinstruments/adapters.py +++ b/pymeasure/instruments/oxfordinstruments/base.py @@ -23,9 +23,9 @@ # -from pymeasure.adapters import VISAAdapter +from pymeasure.instruments import Instrument from pyvisa.errors import VisaIOError - +from pyvisa import constants as vconst import re import logging @@ -38,23 +38,37 @@ class OxfordVISAError(Exception): pass -class OxfordInstrumentsAdapter(VISAAdapter): - """Adapter class for the VISA library using PyVISA to communicate - with instruments. +class OxfordInstrumentsBase(Instrument): + """Base instrument for devices from Oxford Instruments. + Checks the replies from instruments for validity. - :param resource_name: VISA resource name that identifies the address + :param adapter: A string, integer, or :py:class:`~pymeasure.adapters.Adapter` subclass object + :param string name: The name of the instrument. Often the model designation by default. :param max_attempts: Integer that sets how many attempts at getting a valid response to a query can be made - :param kwargs: key-word arguments for constructing a PyVISA Adapter + :param \\**kwargs: In case ``adapter`` is a string or integer, additional arguments passed on + to :py:class:`~pymeasure.adapters.VISAAdapter` (check there for details). + Discarded otherwise. """ timeoutError = VisaIOError(-1073807339) regex_pattern = r"^([a-zA-Z])[\d.+-]*$" - def __init__(self, resource_name, max_attempts=5, **kwargs): - super().__init__(resource_name, **kwargs) + def __init__(self, adapter, name="OxfordInstruments Base", max_attempts=5, **kwargs): + kwargs.setdefault('read_termination', '\r') + + super().__init__(adapter, + name=name, + includeSCPI=False, + asrl={ + 'baud_rate': 9600, + 'data_bits': 8, + 'parity': vconst.Parity.none, + 'stop_bits': vconst.StopBits.two, + }, + **kwargs) self.max_attempts = max_attempts def ask(self, command): @@ -72,9 +86,15 @@ def ask(self, command): """ for attempt in range(self.max_attempts): - response = super().ask(command) + # Skip the checks in "write", because we explicitly want to get an answer here + super().write(command) + self.wait_for() + response = self.read() if self.is_valid_response(response, command): + if command.startswith("R"): + # Remove the leading R of the response + return response.strip("R") return response log.debug("Received invalid response to '%s': %s", command, response) @@ -92,7 +112,7 @@ def ask(self, command): raise OxfordVISAError(f"Retried {self.max_attempts} times without getting a valid " "response, maybe there is something worse at hand.") - def _write(self, command): + def write(self, command): """Write command to instrument and check whether the reply indicates that the given command was not understood. The devices from Oxford Instruments reply with '?xxx' to a command 'xxx' if this command is @@ -103,7 +123,7 @@ def _write(self, command): :raises: :class:`~.OxfordVISAError` if the instrument does not recognise the supplied command or if the response of the instrument is not understood """ - super()._write(command) + super().write(command) if not command[0] == "$": response = self.read() @@ -158,4 +178,4 @@ def is_valid_response(self, response, command): return bool(match) def __repr__(self): - return "" % self.connection.resource_name + return "" % self.adapter.connection.resource_name diff --git a/pymeasure/instruments/oxfordinstruments/ips120_10.py b/pymeasure/instruments/oxfordinstruments/ips120_10.py index 8914800da3..a123ce5218 100644 --- a/pymeasure/instruments/oxfordinstruments/ips120_10.py +++ b/pymeasure/instruments/oxfordinstruments/ips120_10.py @@ -30,7 +30,7 @@ from pymeasure.instruments.validators import strict_discrete_set from pymeasure.instruments.validators import truncated_range -from .adapters import OxfordInstrumentsAdapter +from .base import OxfordInstrumentsBase # Setup logging log = logging.getLogger(__name__) @@ -47,7 +47,7 @@ class SwitchHeaterError(ValueError): pass -class IPS120_10(Instrument): +class IPS120_10(OxfordInstrumentsBase): """Represents the Oxford Superconducting Magnet Power Supply IPS 120-10. .. code-block:: python @@ -116,25 +116,10 @@ def __init__(self, field_range=None, **kwargs): - if isinstance(adapter, (int, str)): - kwargs.setdefault('read_termination', '\r') - kwargs.setdefault('send_end', True) - adapter = OxfordInstrumentsAdapter( - adapter, - asrl={ - 'baud_rate': 9600, - 'data_bits': 8, - 'parity': 0, - 'stop_bits': 20, - }, - preprocess_reply=lambda v: v[1:], - **kwargs, - ) - super().__init__( adapter=adapter, name=name, - includeSCPI=False, + **kwargs ) if switch_heater_heating_delay is not None: @@ -155,7 +140,6 @@ def __init__(self, version = Instrument.measurement( "V", """ A string property that returns the version of the IPS. """, - preprocess_reply=lambda v: v, ) control_mode = Instrument.control( diff --git a/pymeasure/instruments/oxfordinstruments/itc503.py b/pymeasure/instruments/oxfordinstruments/itc503.py index 978c84b9e9..0c0560af1c 100644 --- a/pymeasure/instruments/oxfordinstruments/itc503.py +++ b/pymeasure/instruments/oxfordinstruments/itc503.py @@ -32,7 +32,7 @@ from pymeasure.instruments.validators import strict_discrete_set, \ truncated_range, strict_range -from .adapters import OxfordInstrumentsAdapter +from .base import OxfordInstrumentsBase # Setup logging @@ -58,7 +58,7 @@ def pointer_validator(value, values): return tuple(strict_range(v, values) for v in value) -class ITC503(Instrument): +class ITC503(OxfordInstrumentsBase): """Represents the Oxford Intelligent Temperature Controller 503. .. code-block:: python @@ -84,25 +84,10 @@ def __init__(self, max_temperature=1677.7, **kwargs): - if isinstance(adapter, (int, str)): - kwargs.setdefault('read_termination', '\r') - kwargs.setdefault('send_end', True) - adapter = OxfordInstrumentsAdapter( - adapter, - asrl={ - 'baud_rate': 9600, - 'data_bits': 8, - 'parity': 0, - 'stop_bits': 20, - }, - preprocess_reply=lambda v: v[1:], - **kwargs, - ) - super().__init__( adapter=adapter, name=name, - includeSCPI=False, + **kwargs, ) # Clear the buffer in order to prevent communication problems diff --git a/pymeasure/instruments/oxfordinstruments/ps120_10.py b/pymeasure/instruments/oxfordinstruments/ps120_10.py index d11d4c65a0..03eb058685 100644 --- a/pymeasure/instruments/oxfordinstruments/ps120_10.py +++ b/pymeasure/instruments/oxfordinstruments/ps120_10.py @@ -27,12 +27,12 @@ def PS_custom_get_process(v): - """convert string to proper float value, for working with the PS 120-10 """ - return float(v[1:]) * 1e-2 + """Adjust the received value, for working with the PS 120-10 """ + return v * 1e-2 def PS_custom_set_process(v): - """convert float to proper int value, for working with the PS 120-10 """ + """Convert float to proper int value, for working with the PS 120-10 """ return int(v * 1e2) @@ -100,12 +100,12 @@ def __init__(self, current_setpoint_get_process = PS_custom_get_process current_setpoint_set_process = PS_custom_set_process - current_setpoint_set_command = "J%d" + current_setpoint_set_command = "I%d" field_setpoint_get_process = PS_custom_get_process field_setpoint_set_process = PS_custom_set_process - field_setpoint_set_command = "T%d" + field_setpoint_set_command = "J%d" sweep_rate_get_process = PS_custom_get_process sweep_rate_set_process = PS_custom_set_process - sweep_rate_set_command = "A%d" + sweep_rate_set_command = "T%d" diff --git a/tests/instruments/oxfordinstruments/test_base_instrument.py b/tests/instruments/oxfordinstruments/test_base_instrument.py new file mode 100644 index 0000000000..2fb8438a73 --- /dev/null +++ b/tests/instruments/oxfordinstruments/test_base_instrument.py @@ -0,0 +1,47 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import pytest + +from pymeasure.test import expected_protocol + + +from pymeasure.instruments.oxfordinstruments.base import OxfordInstrumentsBase, OxfordVISAError + + +def test_wrong_response(): + with expected_protocol(OxfordInstrumentsBase, + [("A", "B"), (None, "")], + max_attempts=1, + ) as inst: + with pytest.raises(OxfordVISAError): + inst.ask("A") + + +def test_write_not_understood_command(): + with expected_protocol(OxfordInstrumentsBase, + [("A", "?B")], + ) as inst: + with pytest.raises(OxfordVISAError): + inst.write("A") diff --git a/tests/instruments/oxfordinstruments/test_ips120_10.py b/tests/instruments/oxfordinstruments/test_ips120_10.py new file mode 100644 index 0000000000..985e032eb0 --- /dev/null +++ b/tests/instruments/oxfordinstruments/test_ips120_10.py @@ -0,0 +1,77 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.test import expected_protocol + + +from pymeasure.instruments.oxfordinstruments.ips120_10 import IPS120_10 + + +def test_version(): + with expected_protocol(IPS120_10, + [("V", "IPS120-10 Version 3.04 @ Oxford Instruments 1996")] + ) as inst: + assert inst.version == "IPS120-10 Version 3.04 @ Oxford Instruments 1996" + + +def test_activity_getter(): + with expected_protocol(IPS120_10, + [("X", "X00A0C0M00P00")] + ) as inst: + assert inst.activity == "hold" + + +def test_activity_setter(): + with expected_protocol(IPS120_10, + [("A0", "A")] + ) as inst: + inst.activity = "hold" + + +def test_current_setpoint_getter(): + with expected_protocol(IPS120_10, + [("R0", "R+1.3")] + ) as inst: + assert inst.current_setpoint == 1.3 + + +def test_current_setpoint_setter(): + with expected_protocol(IPS120_10, + [("I1.300000", "I")] + ) as inst: + inst.current_setpoint = 1.3 + + +def test_control_mode_getter(): + with expected_protocol(IPS120_10, + [("X", "X00A0C1M00P00")] + ) as inst: + assert inst.control_mode == "RL" + + +def test_control_mode_setter(): + with expected_protocol(IPS120_10, + [("C1", "C")] + ) as inst: + inst.control_mode = "RL" diff --git a/tests/instruments/oxfordinstruments/test_ps120_10.py b/tests/instruments/oxfordinstruments/test_ps120_10.py new file mode 100644 index 0000000000..64d4bf024f --- /dev/null +++ b/tests/instruments/oxfordinstruments/test_ps120_10.py @@ -0,0 +1,84 @@ +# +# This file is part of the PyMeasure package. +# +# Copyright (c) 2013-2023 PyMeasure Developers +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +from pymeasure.test import expected_protocol + + +from pymeasure.instruments.oxfordinstruments.ps120_10 import PS120_10 + + +def test_version(): + with expected_protocol(PS120_10, + [("V", "IPS120-10 Version 3.04 @ Oxford Instruments 1996")] + ) as inst: + assert inst.version == "IPS120-10 Version 3.04 @ Oxford Instruments 1996" + + +def test_activity_getter(): + with expected_protocol(PS120_10, + [("X", "X00A0C0M00P00")] + ) as inst: + assert inst.activity == "hold" + + +def test_activity_setter(): + with expected_protocol(PS120_10, + [("A0", "A")] + ) as inst: + inst.activity = "hold" + + +def test_current_setpoint_getter(): + with expected_protocol(PS120_10, + [("R0", "R+130")] + ) as inst: + assert inst.current_setpoint == 1.3 + + +def test_current_setpoint_setter(): + with expected_protocol(PS120_10, + [("I130", "I")] + ) as inst: + inst.current_setpoint = 1.3 + + +def test_control_mode_getter(): + with expected_protocol(PS120_10, + [("X", "X00A0C1M00P00")] + ) as inst: + assert inst.control_mode == "RL" + + +def test_control_mode_setter(): + with expected_protocol(PS120_10, + [("C1", "C")] + ) as inst: + inst.control_mode = "RL" + + +def test_field_setpoint(): + with expected_protocol(PS120_10, + [("R8", "R+00100")], + ) as inst: + assert inst.field_setpoint == 1.0