diff --git a/doc/examples/minghe/ex_minghe_mhs5200.py b/doc/examples/minghe/ex_minghe_mhs5200.py new file mode 100644 index 000000000..60b69bedb --- /dev/null +++ b/doc/examples/minghe/ex_minghe_mhs5200.py @@ -0,0 +1,28 @@ +#!/usr/bin/python +from instruments.minghe import MHS5200 +import quantities as pq + +mhs = MHS5200.open_serial(vid=6790, pid=29987, baud=57600) +print(mhs.serial_number) +mhs.channel[0].frequency = 3000000*pq.Hz +print(mhs.channel[0].frequency) +mhs.channel[0].function = MHS5200.Function.sawtooth_down +print(mhs.channel[0].function) +mhs.channel[0].amplitude = 9.0*pq.V +print(mhs.channel[0].amplitude) +mhs.channel[0].offset = -0.5 +print(mhs.channel[0].offset) +mhs.channel[0].phase = 90 +print(mhs.channel[0].phase) + +mhs.channel[1].frequency = 2000000*pq.Hz +print(mhs.channel[1].frequency) +mhs.channel[1].function = MHS5200.Function.square +print(mhs.channel[1].function) +mhs.channel[1].amplitude = 2.0*pq.V +print(mhs.channel[1].amplitude) +mhs.channel[1].offset = 0.0 +print(mhs.channel[1].offset) +mhs.channel[1].phase = 15 +print(mhs.channel[1].phase) + diff --git a/doc/source/apiref/minghe.rst b/doc/source/apiref/minghe.rst new file mode 100644 index 000000000..6a1763039 --- /dev/null +++ b/doc/source/apiref/minghe.rst @@ -0,0 +1,15 @@ +.. + TODO: put documentation license header here. + +.. currentmodule:: instruments.minghe + +====== +Minghe +====== + +:class:`MHS5200` Function Generator +=================================== + +.. autoclass:: MHS5200 + :members: + :undoc-members: diff --git a/instruments/__init__.py b/instruments/__init__.py index 29b03f2ca..c913473af 100644 --- a/instruments/__init__.py +++ b/instruments/__init__.py @@ -17,6 +17,7 @@ from . import hp from . import keithley from . import lakeshore +from . import minghe from . import newport from . import oxford from . import phasematrix diff --git a/instruments/minghe/__init__.py b/instruments/minghe/__init__.py new file mode 100644 index 000000000..997950f6c --- /dev/null +++ b/instruments/minghe/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing MingHe instruments +""" +from __future__ import absolute_import +from .mhs5200a import MHS5200 diff --git a/instruments/minghe/mhs5200a.py b/instruments/minghe/mhs5200a.py new file mode 100644 index 000000000..1f2c035e2 --- /dev/null +++ b/instruments/minghe/mhs5200a.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides the support for the MingHe low-cost function generator. + +Class originally contributed by Catherine Holloway. +""" + +# IMPORTS ##################################################################### + +from __future__ import absolute_import +from __future__ import division + +from builtins import range +from enum import Enum + +import quantities as pq +from instruments.abstract_instruments import Instrument, FunctionGenerator +from instruments.util_fns import ProxyList, assume_units + +# CLASSES ##################################################################### + + +class MHS5200(Instrument): + """ + The MHS5200 is a low-cost, 2 channel function generator. + + There is no user manual, but Al Williams has reverse-engineered the + communications protocol: + https://github.com/wd5gnr/mhs5200a/blob/master/MHS5200AProtocol.pdf + """ + def __init__(self, filelike): + super(MHS5200, self).__init__(filelike) + self._channel_count = 2 + self.terminator = "\r\n" + + def _ack_expected(self, msg=""): + if msg.find(":r") == 0: + return None + else: + # most commands res + return "ok" + + # INNER CLASSES # + + class Channel(FunctionGenerator): + """ + Class representing a channel on the MHS52000. + """ + # pylint: disable=protected-access + + __CHANNEL_NAMES = { + 1: '1', + 2: '2' + } + + def __init__(self, mhs, idx): + self._mhs = mhs + super(MHS5200.Channel, self).__init__(self._mhs._file) + # Use zero-based indexing for the external API, but one-based + # for talking to the instrument. + self._idx = idx + 1 + self._chan = self.__CHANNEL_NAMES[self._idx] + self._count = 0 + + def _get_amplitude_(self): + query = ":r{0}a".format(self._chan) + response = self._mhs.query(query) + return float(response.replace(query, ""))/100.0, self.VoltageMode.rms + + def _set_amplitude_(self, new_val, units): + if units == self.VoltageMode.peak_to_peak or \ + units == self.VoltageMode.rms: + new_val = assume_units(new_val, "V").rescale(pq.V).magnitude + elif units == self.VoltageMode.dBm: + raise NotImplementedError("Decibel units are not supported.") + new_val *= 100 + query = ":s{0}a{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def duty_cycle(self): + """ + Gets/Sets the duty cycle of this channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units seconds. + :type: `~quantities.Quantity` + """ + query = ":r{0}d".format(self._chan) + response = self._mhs.query(query) + duty = float(response.replace(query, ""))*pq.s + return duty + + @duty_cycle.setter + def duty_cycle(self, new_val): + new_val = assume_units(new_val, pq.s).rescale(pq.s).magnitude + query = ":s{0}d{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def enable(self): + """ + Gets/Sets the enable state of this channel. + + :param new_val: the enable state + :type: `bool` + """ + query = ":r{0}b".format(self._chan) + return int(self._mhs.query(query).replace(query, ""). + replace("\r", "")) + + @enable.setter + def enable(self, new_val): + query = ":s{0}b{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def frequency(self): + """ + Gets/Sets the frequency of this channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of units hertz. + :type: `~quantities.Quantity` + """ + query = ":r{0}f".format(self._chan) + response = self._mhs.query(query) + freq = float(response.replace(query, ""))*pq.Hz + return freq/100.0 + + @frequency.setter + def frequency(self, new_val): + new_val = assume_units(new_val, pq.Hz).rescale(pq.Hz).\ + magnitude*100.0 + query = ":s{0}f{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def offset(self): + """ + Gets/Sets the offset of this channel. + + :param new_val: The fraction of the duty cycle to offset the + function by. + :type: `float` + """ + # need to convert + query = ":r{0}o".format(self._chan) + response = self._mhs.query(query) + return int(response.replace(query, ""))/100.0-1.20 + + @offset.setter + def offset(self, new_val): + new_val = int(new_val*100)+120 + query = ":s{0}o{1}".format(self._chan, new_val) + self._mhs.sendcmd(query) + + @property + def phase(self): + """ + Gets/Sets the phase of this channel. + + :units: As specified (if a `~quantities.Quantity`) or assumed to be + of degrees. + :type: `~quantities.Quantity` + """ + # need to convert + query = ":r{0}p".format(self._chan) + response = self._mhs.query(query) + return int(response.replace(query, ""))*pq.deg + + @phase.setter + def phase(self, new_val): + new_val = assume_units(new_val, pq.deg).rescale("deg").magnitude + query = ":s{0}p{1}".format(self._chan, int(new_val)) + self._mhs.sendcmd(query) + + @property + def function(self): + """ + Gets/Sets the wave type of this channel. + + :type: `MHS5200.Function` + """ + query = ":r{0}w".format(self._chan) + response = self._mhs.query(query).replace(query, "") + return self._mhs.Function(int(response)) + + @function.setter + def function(self, new_val): + query = ":s{0}w{1}".format(self._chan, + self._mhs.Function(new_val).value) + self._mhs.sendcmd(query) + + class Function(Enum): + """ + Enum containing valid wave modes for + """ + sine = 0 + square = 1 + triangular = 2 + sawtooth_up = 3 + sawtooth_down = 4 + + @property + def channel(self): + """ + Gets a specific channel object. The desired channel is specified like + one would access a list. + + For instance, this would print the counts of the first channel:: + + >>> mhs = ik.minghe.MHS5200.open_serial(vid=1027, pid=24577, + baud=19200, timeout=1) + >>> print(mhs.channel[0].frequency) + + :rtype: `CC1.Channel` + """ + return ProxyList(self, MHS5200.Channel, range(self._channel_count)) + + @property + def serial_number(self): + """ + Get the serial number, as an int + + :rtype: int + """ + query = ":r0c" + response = self.query(query) + response = response.replace(query, "").replace("\r", "") + return response diff --git a/instruments/tests/test_minghe/test_minghe_mhs5200a.py b/instruments/tests/test_minghe/test_minghe_mhs5200a.py new file mode 100644 index 000000000..34dc2e3e8 --- /dev/null +++ b/instruments/tests/test_minghe/test_minghe_mhs5200a.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Module containing tests for the MingHe MHS52000a +""" + +# 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, unit_eq + + +# TESTS ###################################################################### + + +def test_mhs_amplitude(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1a", + ":r2a", + ":s1a660", + ":s2a800" + ], + [ + ":r1a330", + ":r2a500", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].amplitude[0] == 3.3*pq.V + assert mhs.channel[1].amplitude[0] == 5.0*pq.V + mhs.channel[0].amplitude = 6.6*pq.V + mhs.channel[1].amplitude = 8.0*pq.V + + +@raises(NotImplementedError) +def test_mhs_amplitude_dbm_notimplemented(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":s1a660" + ], + [ + "ok" + ], + sep="\r\n" + ) as mhs: + mhs.channel[0].amplitude = 6.6*ik.units.dBm + + +def test_mhs_duty_cycle(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1d", + ":r2d", + ":s1d6", + ":s2d8" + + ], + [ + ":r1d330", + ":r2d500", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].duty_cycle == 330.0*pq.s + assert mhs.channel[1].duty_cycle == 500.0*pq.s + mhs.channel[0].duty_cycle = 6*pq.s + mhs.channel[1].duty_cycle = 8*pq.s + + +def test_mhs_enable(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1b", + ":r2b", + ":s1b0", + ":s2b1" + ], + [ + ":r1b1", + ":r2b0", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].enable + assert not mhs.channel[1].enable + mhs.channel[0].enable = False + mhs.channel[1].enable = True + + +def test_mhs_frequency(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1f", + ":r2f", + ":s1f600000", + ":s2f800000" + + ], + [ + ":r1f3300000", + ":r2f50000000", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].frequency == 33.0*pq.kHz + assert mhs.channel[1].frequency == 500.0*pq.kHz + mhs.channel[0].frequency = 6*pq.kHz + mhs.channel[1].frequency = 8*pq.kHz + + +def test_mhs_offset(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1o", + ":r2o", + ":s1o60", + ":s2o180" + + ], + [ + ":r1o120", + ":r2o0", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].offset == 0 + assert mhs.channel[1].offset == -1.2 + mhs.channel[0].offset = -0.6 + mhs.channel[1].offset = 0.6 + + +def test_mhs_phase(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1p", + ":r2p", + ":s1p60", + ":s2p180" + + ], + [ + ":r1p120", + ":r2p0", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].phase == 120 + assert mhs.channel[1].phase == 0 + mhs.channel[0].phase = 60 + mhs.channel[1].phase = 180 + + +def test_mhs_wave_type(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r1w", + ":r2w", + ":s1w2", + ":s2w3" + + ], + [ + ":r1w0", + ":r2w1", + "ok", + "ok" + ], + sep="\r\n" + ) as mhs: + assert mhs.channel[0].function == mhs.Function.sine + assert mhs.channel[1].function == mhs.Function.square + mhs.channel[0].function = mhs.Function.triangular + mhs.channel[1].function = mhs.Function.sawtooth_up + + +def test_mhs_serial_number(): + with expected_protocol( + ik.minghe.MHS5200, + [ + ":r0c" + + ], + [ + ":r0c5225A1", + ], + sep="\r\n" + ) as mhs: + assert mhs.serial_number == "5225A1" \ No newline at end of file