diff --git a/doc/examples/qubitekk/ex_qubitekk_mc1.py b/doc/examples/qubitekk/ex_qubitekk_mc1.py new file mode 100644 index 000000000..cd124e354 --- /dev/null +++ b/doc/examples/qubitekk/ex_qubitekk_mc1.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# Qubitekk Motor controller example +from time import sleep + +from instruments.qubitekk import MC1 +import quantities as pq + + +if __name__ == "__main__": + + mc1 = MC1.open_serial(vid=1027, pid=24577, baud=9600, timeout=1) + mc1.step_size = 25*pq.ms + mc1.inertia = 10*pq.ms + print("step size:", mc1.step_size) + print("inertial force: ", mc1.inertia) + + print("Firmware", mc1.firmware) + print("Motor controller type: ", mc1.controller) + print("centering") + + mc1.center() + while mc1.is_centering(): + print(str(mc1.metric_position)+" "+str(mc1.direction)) + pass + + print("Stage Centered") + # for the motor in the mechanical delay line, the travel is limited from + # the full range of travel. Here's how to set the limits. + mc1.lower_limit = -260*pq.ms + mc1.upper_limit = 300*pq.ms + mc1.increment = 5*pq.ms + x_pos = mc1.lower_limit + while x_pos <= mc1.upper_limit: + print(str(mc1.metric_position)+" "+str(mc1.direction)) + mc1.move(x_pos) + while mc1.move_timeout > 0: + sleep(0.5) + sleep(1) + x_pos += mc1.increment diff --git a/doc/source/apiref/qubitekk.rst b/doc/source/apiref/qubitekk.rst index 6c97a4d34..b86dc6253 100644 --- a/doc/source/apiref/qubitekk.rst +++ b/doc/source/apiref/qubitekk.rst @@ -14,4 +14,9 @@ Qubitekk :members: :undoc-members: +:class:`MC1` Motor Controller +============================= +.. autoclass:: MC1 + :members: + :undoc-members: diff --git a/instruments/qubitekk/__init__.py b/instruments/qubitekk/__init__.py index bea20273e..e71003a05 100644 --- a/instruments/qubitekk/__init__.py +++ b/instruments/qubitekk/__init__.py @@ -7,3 +7,4 @@ from __future__ import absolute_import from .cc1 import CC1 +from .mc1 import MC1 diff --git a/instruments/qubitekk/mc1.py b/instruments/qubitekk/mc1.py new file mode 100644 index 000000000..98b353962 --- /dev/null +++ b/instruments/qubitekk/mc1.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides support for the Qubitekk MC1 Motor Controller. + +MC1 Class originally contributed by Catherine Holloway. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import, division + +from builtins import range, map +from enum import Enum + +import quantities as pq + +from instruments.abstract_instruments import Instrument +from instruments.util_fns import ( + int_property, enum_property, unitful_property, assume_units +) + +# CLASSES ##################################################################### + + +class MC1(Instrument): + """ + The MC1 is a controller for the qubitekk motor controller. Used with a + linear actuator to perform a HOM dip. + """ + def __init__(self, filelike): + super(MC1, self).__init__(filelike) + self.terminator = "\r" + self._increment = 1*pq.ms + self._lower_limit = -300*pq.ms + self._upper_limit = 300*pq.ms + self._firmware = None + self._controller = None + + # ENUMS # + + class MotorType(Enum): + """ + Enum for the motor types for the MC1 + """ + radio = "Radio" + relay = "Relay" + + # PROPERTIES # + + @property + def increment(self): + """ + Gets/sets the stepping increment value of the motor controller + + :units: As specified, or assumed to be of units milliseconds + :type: `~quantities.Quantity` + """ + return self._increment + + @increment.setter + def increment(self, newval): + self._increment = assume_units(newval, pq.ms).rescale(pq.ms) + + @property + def lower_limit(self): + """ + Gets/sets the stepping lower limit value of the motor controller + + :units: As specified, or assumed to be of units milliseconds + :type: `~quantities.Quantity` + """ + return self._lower_limit + + @lower_limit.setter + def lower_limit(self, newval): + self._lower_limit = assume_units(newval, pq.ms).rescale(pq.ms) + + @property + def upper_limit(self): + """ + Gets/sets the stepping upper limit value of the motor controller + + :units: As specified, or assumed to be of units milliseconds + :type: `~quantities.Quantity` + """ + return self._upper_limit + + @upper_limit.setter + def upper_limit(self, newval): + self._upper_limit = assume_units(newval, pq.ms).rescale(pq.ms) + + direction = unitful_property( + name="DIRE", + doc=""" + Get the internal direction variable, which is a function of how far + the motor needs to go. + + :type: `~quantities.Quantity` + :units: milliseconds + """, + units=pq.ms, + readonly=True + ) + + inertia = unitful_property( + name="INER", + doc=""" + Gets/Sets the amount of force required to overcome static inertia. Must + be between 0 and 100 milliseconds. + + :type: `~quantities.Quantity` + :units: milliseconds + """, + format_code='{:.0f}', + units=pq.ms, + valid_range=(0*pq.ms, 100*pq.ms), + set_fmt=":{} {}" + ) + + @property + def internal_position(self): + """ + Get the internal motor state position, which is equivalent to the total + number of milliseconds that voltage has been applied to the motor in + the positive direction minus the number of milliseconds that voltage + has been applied to the motor in the negative direction. + + :type: `~quantities.Quantity` + :units: milliseconds + """ + response = int(self.query("POSI?"))*self.step_size + return response + + metric_position = unitful_property( + name="METR", + doc=""" + Get the estimated motor position, in millimeters. + + :type: `~quantities.Quantity` + :units: millimeters + """, + units=pq.mm, + readonly=True + ) + + setting = int_property( + name="OUTP", + doc=""" + Gets/sets the output port of the optical switch. 0 means input 1 is + directed to output 1, and input 2 is directed to output 2. 1 means that + input 1 is directed to output 2 and input 2 is directed to output 1. + + :type: `int` + """, + valid_set=range(2), + set_fmt=":{} {}" + ) + + step_size = unitful_property( + name="STEP", + doc=""" + Gets/Sets the number of milliseconds per step. Must be between 1 + and 100 milliseconds. + + :type: `~quantities.Quantity` + :units: milliseconds + """, + format_code='{:.0f}', + units=pq.ms, + valid_range=(1*pq.ms, 100*pq.ms), + set_fmt=":{} {}" + ) + + @property + def firmware(self): + """ + Gets the firmware version + + :rtype: `tuple`(Major:`int`, Minor:`int`, Patch`int`) + """ + # the firmware is assumed not to change while the device is active + # firmware is stored locally as it will be gotten often + # pylint: disable=no-member + if self._firmware is None: + while self._firmware is None: + self._firmware = self.query("FIRM?") + value = self._firmware.split(".") + if len(value) < 3: + for _ in range(3-len(value)): + value.append(0) + value = tuple(map(int, value)) + self._firmware = value + return self._firmware + + controller = enum_property( + 'MOTO', + MotorType, + doc=""" + Get the motor controller type. + """, + readonly=True + ) + + @property + def move_timeout(self): + """ + Get the motor's timeout value, which indicates the number of + milliseconds before the motor can start moving again. + + :type: `~quantities.Quantity` + :units: milliseconds + """ + response = int(self.query("TIME?")) + return response*self.step_size + + # METHODS # + + def is_centering(self): + """ + Query whether the motor is in its centering phase + + :return: False if not centering, True if centering + :rtype: `bool` + """ + response = self.query("CENT?") + return True if int(response) == 1 else False + + def center(self): + """ + Commands the motor to go to the center of its travel range + """ + self.sendcmd(":CENT") + + def reset(self): + """ + Sends the stage to the limit of one of its travel ranges + """ + self.sendcmd(":RESE") + + def move(self, new_position): + """ + Move to a specified location. Position is unitless and is defined as + the number of motor steps. It varies between motors. + + :param new_position: the location + :type new_position: `~quantities.Quantity` + """ + if self.lower_limit <= new_position <= self.upper_limit: + new_position = assume_units(new_position, pq.ms).rescale(pq.ms) + clock_cycles = new_position/self.step_size + cmd = ":MOVE "+str(int(clock_cycles)) + self.sendcmd(cmd) + else: + raise ValueError("Location out of range") diff --git a/instruments/tests/test_qubitekk/test_qubitekk_mc1.py b/instruments/tests/test_qubitekk/test_qubitekk_mc1.py new file mode 100644 index 000000000..eff278cf6 --- /dev/null +++ b/instruments/tests/test_qubitekk/test_qubitekk_mc1.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the Qubitekk MC1 +""" + +# IMPORTS #################################################################### + +from __future__ import absolute_import + +from nose.tools import raises + +import quantities as pq +import instruments as ik +from instruments.tests import expected_protocol + + +# TESTS ###################################################################### + + +def test_mc1_setting(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "OUTP?", + ":OUTP 0" + ], + [ + "1" + ], + sep="\r" + ) as mc: + assert mc.setting == 1 + mc.setting = 0 + + +def test_mc1_internal_position(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "POSI?", + "STEP?" + + ], + [ + "-100", + "1" + ], + sep="\r" + ) as mc: + assert mc.internal_position == -100*pq.ms + + +def test_mc1_metric_position(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "METR?" + ], + [ + "-3.14159" + ], + sep="\r" + ) as mc: + assert mc.metric_position == -3.14159*pq.mm + + +def test_mc1_direction(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "DIRE?" + ], + [ + "-100" + ], + sep="\r" + ) as mc: + assert mc.direction == -100 + + +def test_mc1_firmware(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "FIRM?" + ], + [ + "1.0.1" + ], + sep="\r" + ) as mc: + assert mc.firmware == (1, 0, 1) + + +def test_mc1_inertia(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "INER?" + ], + [ + "20" + ], + sep="\r" + ) as mc: + assert mc.inertia == 20 + + +def test_mc1_step(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "STEP?" + ], + [ + "20" + ], + sep="\r" + ) as mc: + assert mc.step_size == 20*pq.ms + + +def test_mc1_motor(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "MOTO?" + ], + [ + "Radio" + ], + sep="\r" + ) as mc: + assert mc.controller == mc.MotorType.radio + + +def test_mc1_move_timeout(): + with expected_protocol( + ik.qubitekk.MC1, + [ + "TIME?", + "STEP?" + ], + [ + "200", + "1" + ], + sep="\r" + ) as mc: + assert mc.move_timeout == 200*pq.ms + + +def test_mc1_is_centering(): + with expected_protocol( + ik.qubitekk.MC1, + ["CENT?"], + ["1"], + sep="\r" + ) as mc: + assert mc.is_centering() is True + + +def test_mc1_is_centering_false(): + with expected_protocol( + ik.qubitekk.MC1, + ["CENT?"], + ["0"], + sep="\r" + ) as mc: + assert mc.is_centering() is False + + +def test_mc1_center(): + with expected_protocol( + ik.qubitekk.MC1, + [":CENT"], + [""], + sep="\r" + ) as mc: + mc.center() + + +def test_mc1_reset(): + with expected_protocol( + ik.qubitekk.MC1, + [":RESE"], + [""], + sep="\r" + ) as mc: + mc.reset() + + +def test_mc1_move(): + with expected_protocol( + ik.qubitekk.MC1, + ["STEP?", ":MOVE 0"], + ["1"], + sep="\r" + ) as mc: + mc.move(0) + + +@raises(ValueError) +def test_mc1_move_value_error(): + with expected_protocol( + ik.qubitekk.MC1, + [":MOVE -1000"], + [""], + sep="\r" + ) as mc: + mc.move(-1000)