From 14c759ff89a882e9108b5aa80211e71174d906a1 Mon Sep 17 00:00:00 2001 From: Yann Sionneau Date: Wed, 4 Mar 2015 14:30:33 +0000 Subject: [PATCH] add Thorlabs T-Cube NDSP --- artiq/devices/thorlabs_tcube/__init__.py | 0 artiq/devices/thorlabs_tcube/driver.py | 1548 +++++++++++++++++++ artiq/frontend/thorlabs_tcube_controller.py | 47 + doc/manual/ndsp_reference.rst | 25 + setup.py | 1 + 5 files changed, 1621 insertions(+) create mode 100644 artiq/devices/thorlabs_tcube/__init__.py create mode 100644 artiq/devices/thorlabs_tcube/driver.py create mode 100755 artiq/frontend/thorlabs_tcube_controller.py diff --git a/artiq/devices/thorlabs_tcube/__init__.py b/artiq/devices/thorlabs_tcube/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/artiq/devices/thorlabs_tcube/driver.py b/artiq/devices/thorlabs_tcube/driver.py new file mode 100644 index 000000000..889dc9c9f --- /dev/null +++ b/artiq/devices/thorlabs_tcube/driver.py @@ -0,0 +1,1548 @@ +from enum import Enum +import logging +import struct as st + +import serial + +from artiq.language.units import V, strip_unit + + +logger = logging.getLogger(__name__) + + +class MGMSG(Enum): + HW_DISCONNECT = 0x0002 + HW_REQ_INFO = 0x0005 + HW_GET_INFO = 0x0006 + HW_START_UPDATEMSGS = 0x0011 + HW_STOP_UPDATEMSGS = 0x0012 + HUB_REQ_BAYUSED = 0x0065 + HUB_GET_BAYUSED = 0x0066 + HW_RESPONSE = 0x0080 + HW_RICHRESPONSE = 0x0081 + MOD_SET_CHANENABLESTATE = 0x0210 + MOD_REQ_CHANENABLESTATE = 0x0211 + MOD_GET_CHANENABLESTATE = 0x0212 + MOD_IDENTIFY = 0x0223 + MOT_SET_ENCCOUNTER = 0x0409 + MOT_REQ_ENCCOUNTER = 0x040A + MOT_GET_ENCCOUNTER = 0x040B + MOT_SET_POSCOUNTER = 0x0410 + MOT_REQ_POSCOUNTER = 0x0411 + MOT_GET_POSCOUNTER = 0x0412 + MOT_SET_VELPARAMS = 0x0413 + MOT_REQ_VELPARAMS = 0x0414 + MOT_GET_VELPARAMS = 0x0415 + MOT_SET_JOGPARAMS = 0x0416 + MOT_REQ_JOGPARAMS = 0x0417 + MOT_GET_JOGPARAMS = 0x0418 + MOT_SET_LIMSWITCHPARAMS = 0x0423 + MOT_REQ_LIMSWITCHPARAMS = 0x0424 + MOT_GET_LIMSWITCHPARAMS = 0x0425 + MOT_REQ_STATUSBITS = 0x0429 + MOT_GET_STATUSBITS = 0x042A + MOT_SET_GENMOVEPARAMS = 0x043A + MOT_REQ_GENMOVEPARAMS = 0x043B + MOT_GET_GENMOVEPARAMS = 0x043C + MOT_SET_HOMEPARAMS = 0x0440 + MOT_REQ_HOMEPARAMS = 0x0441 + MOT_GET_HOMEPARAMS = 0x0442 + MOT_MOVE_HOME = 0x0443 + MOT_MOVE_HOMED = 0x0444 + MOT_SET_MOVERELPARAMS = 0x0445 + MOT_REQ_MOVERELPARAMS = 0x0446 + MOT_GET_MOVERELPARAMS = 0x0447 + MOT_MOVE_RELATIVE = 0x0448 + MOT_SET_MOVEABSPARAMS = 0x0450 + MOT_REQ_MOVEABSPARAMS = 0x0451 + MOT_GET_MOVEABSPARAMS = 0x0452 + MOT_MOVE_ABSOLUTE = 0x0453 + MOT_MOVE_VELOCITY = 0x0457 + MOT_MOVE_COMPLETED = 0x0464 + MOT_MOVE_STOP = 0x0465 + MOT_MOVE_STOPPED = 0x0466 + MOT_MOVE_JOG = 0x046A + MOT_SUSPEND_ENDOFMOVEMSGS = 0x046B + MOT_RESUME_ENDOFMOVEMSGS = 0x046C + MOT_REQ_DCSTATUSUPDATE = 0x0490 + MOT_GET_DCSTATUSUPDATE = 0x0491 + MOT_ACK_DCSTATUSUPDATE = 0x0492 + MOT_SET_DCPIDPARAMS = 0x04A0 + MOT_REQ_DCPIDPARAMS = 0x04A1 + MOT_GET_DCPIDPARAMS = 0x04A2 + MOT_SET_POTPARAMS = 0x04B0 + MOT_REQ_POTPARAMS = 0x04B1 + MOT_GET_POTPARAMS = 0x04B2 + MOT_SET_AVMODES = 0x04B3 + MOT_REQ_AVMODES = 0x04B4 + MOT_GET_AVMODES = 0x04B5 + MOT_SET_BUTTONPARAMS = 0x04B6 + MOT_REQ_BUTTONPARAMS = 0x04B7 + MOT_GET_BUTTONPARAMS = 0x04B8 + MOT_SET_EEPROMPARAMS = 0x04B9 + PZ_SET_POSCONTROLMODE = 0x0640 + PZ_REQ_POSCONTROLMODE = 0x0641 + PZ_GET_POSCONTROLMODE = 0x0642 + PZ_SET_OUTPUTVOLTS = 0x0643 + PZ_REQ_OUTPUTVOLTS = 0x0644 + PZ_GET_OUTPUTVOLTS = 0x0645 + PZ_SET_OUTPUTPOS = 0x0646 + PZ_REQ_OUTPUTPOS = 0x0647 + PZ_GET_OUTPUTPOS = 0x0648 + PZ_SET_INPUTVOLTSSRC = 0x0652 + PZ_REQ_INPUTVOLTSSRC = 0x0653 + PZ_GET_INPUTVOLTSSRC = 0x0654 + PZ_SET_PICONSTS = 0x0655 + PZ_REQ_PICONSTS = 0x0656 + PZ_GET_PICONSTS = 0x0657 + PZ_GET_PZSTATUSUPDATE = 0x0661 + PZ_SET_OUTPUTLUT = 0x0700 + PZ_REQ_OUTPUTLUT = 0x0701 + PZ_GET_OUTPUTLUT = 0x0702 + PZ_SET_OUTPUTLUTPARAMS = 0x0703 + PZ_REQ_OUTPUTLUTPARAMS = 0x0704 + PZ_GET_OUTPUTLUTPARAMS = 0x0705 + PZ_START_LUTOUTPUT = 0x0706 + PZ_STOP_LUTOUTPUT = 0x0707 + PZ_SET_EEPROMPARAMS = 0x07D0 + PZ_SET_TPZ_DISPSETTINGS = 0x07D1 + PZ_REQ_TPZ_DISPSETTINGS = 0x07D2 + PZ_GET_TPZ_DISPSETTINGS = 0x07D3 + PZ_SET_TPZ_IOSETTINGS = 0x07D4 + PZ_REQ_TPZ_IOSETTINGS = 0x07D5 + PZ_GET_TPZ_IOSETTINGS = 0x07D6 + + +def _write_exactly(f, data): + remaining = len(data) + pos = 0 + while remaining: + written = f.write(data[pos:]) + remaining -= written + pos += written + + +def _read_exactly(f, n): + r = bytes() + while len(r) < n: + r += f.read(n - len(r)) + return r + + +class Direction: + def __init__(self, direction): + if direction not in (1, 2): + raise ValueError("Direction must be either 1 or 2") + self.direction = direction + + def __str__(self): + if self.direction == 1: + return "forward" + else: + return "backward" + + +class MsgError(Exception): + pass + + +class Message: + def __init__(self, id, param1=0, param2=0, dest=0x50, src=0x01, + data=None): + if data is not None: + dest |= 0x80 + self.id = id + self.param1 = param1 + self.param2 = param2 + self.dest = dest + self.src = src + self.data = data + + @staticmethod + def unpack(data): + id, param1, param2, dest, src = st.unpack("` method + between the three values 75 V, 100 V and 150 V. + """ + + voltage = strip_unit(voltage, "V") + + if voltage < 0 or voltage > self.voltage_limit: + raise ValueError("Voltage must be in range [0;{}]" + .format(self.voltage_limit)) + volt = int(voltage*32767/self.voltage_limit) + payload = st.pack("` method. + + 0x01 External Signal: Unit sums the differential signal on the rear + panel EXT IN(+) and EXT IN(-) connectors with the voltage set + using the set_outputvolts method. + + 0x02 Potentiometer: The HV amp output is controlled by a + potentiometer input (either on the control panel, or connected + to the rear panel User I/O D-type connector) summed with the + voltage set using the set_outputvolts method. + + The values can be bitwise or'ed to sum the software source with + either or both of the other source options. + """ + + payload = st.pack("` method + docstring for meaning of bits. + :rtype: int + """ + + get_msg = self.send_request(MGMSG.PZ_REQ_INPUTVOLTSSRC, + MGMSG.PZ_GET_INPUTVOLTSSRC, 1) + return st.unpack("` + function), then only the first cyclelength values need to be set. In + this manner, any arbitrary voltage waveform can be programmed into the + LUT. Note. The LUT values are output by the system at a maximum + bandwidth of 7 KHz, e.g. 500 LUT values will take approximately 71 ms + to be clocked out. + + :param lut_index: The position in the array of the value to be set (0 + to 512 for TPZ). + :param output: The voltage value to be set. Values are in the range + [0; voltage_limit]. Voltage_limit being set with the + :py:meth:`set_tpz_io_settings + ` method. + """ + + output = strip_unit(output, "V") + volt = round(output*32767/self.voltage_limit) + payload = st.pack("` for the + meaning of those parameters. + :rtype: a 2 elements tuple (Quantity, int) + """ + + get_msg = self.send_request(MGMSG.PZ_REQ_TPZ_IOSETTINGS, + MGMSG.PZ_GET_TPZ_IOSETTINGS, 1) + voltage_limit, hub_analog_input = st.unpack("` for a + description of each tuple element meaning. + :rtype: An 8 int tuple + """ + + get_msg = self.send_request(MGMSG.MOT_REQ_POTPARAMS, + MGMSG.MOT_GET_POTPARAMS, 1) + return st.unpack("` + method. + :rtype: A 2 int tuple. + """ + + get_msg = self.send_request(MGMSG.MOT_REQ_LIMSWITCHPARAMS, + MGMSG.MOT_GET_LIMSWITCHPARAMS, 1) + return st.unpack("` + command. + """ + + self.send(Message(MGMSG.MOT_MOVE_RELATIVE, param1=1)) + + def move_relative(self, relative_distance): + """Start a relative move + + :param relative_distance: The distance to move in position encoder + counts. + """ + + payload = st.pack("` + command. + """ + + self.send(Message(MGMSG.MOT_MOVE_ABSOLUTE, param1=1)) + + def move_absolute(self, absolute_distance): + """Start an absolute move. + + :param absolute_distance: The distance to move. This is a signed + integer that specifies the absolute distance in position encoder + counts. + """ + + payload = st.pack("` + command until a :py:meth:`move_stop() + ` command (either + StopImmediate or StopProfiled) is called, or a limit switch is reached. + + :param direction: The direction to jog: 1 to move forward, 2 to move + backward. + """ + + self.send(Message(MGMSG.MOT_MOVE_VELOCITY, param1=1, param2=direction)) + + def move_stop(self, stop_mode, async=False): + """Stop any type of motor move. + + Stops any of those motor move: relative, absolute, homing or move at + velocity. + + :param stop_mode: The stop mode defines either an immediate (abrupt) + or profiled stop. Set this byte to 1 to stop immediately, or to 2 + to stop in a controlled (profiled) manner. + :param async: If set to False, this method will block until motor + is really stopped. Returns immediately if set to True. + """ + + if async: + self.send(Message(MGMSG.MOT_MOVE_STOP, param1=1, param2=stop_mode)) + else: + self.send_request(MGMSG.MOT_MOVE_STOP, + MGMSG.MOT_MOVE_STOPPED, 1, stop_mode) + + def set_dc_pid_parameters(self, proportional, integral, differential, + integral_limit, filter_control=0x0F): + """Set the position control loop parameters. + + :param proportional: The proportional gain, values in range [0; 32767]. + :param integral: The integral gain, values in range [0; 32767]. + :param differential: The differential gain, values in range [0; 32767]. + :param integral_limit: The integral limit parameter is used to cap the + value of the integrator to prevent runaway of the integral sum at + the output. Values are in range [0; 32767]. If set to 0, then + integration term in the PID loop is ignored. + :param filter_control: Identifies which of the above are applied by + setting the corresponding bit to 1. By default, all parameters are + applied, and this parameter is set to 0x0F (1111). + """ + + payload = st.pack("` for + precise description. + :rtype: A 5 int tuple. + """ + + get_msg = self.send_request(MGMSG.MOT_REQ_DCPIDPARAMS, + MGMSG.MOT_GET_DCPIDPARAMS, 1) + return st.unpack("` method. + If set to 2, each button can be programmed with a differente + position value such that the controller will move the motor to that + position when the specific button is pressed. + :param position1: The position (in encoder counts) to which the motor + will move when the top button is pressed. + :param position2: The position (in encoder counts) to which the motor + will move when the bottom button is pressed. + """ + + payload = st.pack("` for + description. + :rtype: A 3 int tuple + """ + + get_msg = self.send_request(MGMSG.MOT_REQ_BUTTONPARAMS, + MGMSG.MOT_GET_BUTTONPARAMS, 1) + return st.unpack(" 512: + raise ValueError("LUT index should be in range [0;512] and not {}" + .format(lut_index)) + self.lut[lut_index] = output + + def get_output_lut(self): + return 0, 0 # FIXME: the API description here doesn't make any sense + + def set_output_lut_parameters(self, mode, cycle_length, num_cycles, + delay_time, precycle_rest, postcycle_rest): + self.mode = mode + self.cycle_length = cycle_length + self.num_cycles = num_cycles + self.delay_time = delay_time + self.precycle_rest = precycle_rest + self.postcycle_rest = postcycle_rest + + def get_output_lut_parameters(self): + return (self.mode, self.cycle_length, self.num_cycles, + self.delay_time, self.precycle_rest, self.postcycle_rest) + + def start_lut_output(self): + pass + + def stop_lut_output(self): + pass + + def set_eeprom_parameters(self, msg_id): + pass + + def set_tpz_display_settings(self, intensity): + self.intensity = intensity + + def get_tpz_display_settings(self): + return self.intensity + + def set_tpz_io_settings(self, voltage_limit, hub_analog_input): + self.voltage_limit = strip_unit(voltage_limit, "V") + + if self.voltage_limit not in [75, 100, 150]: + raise ValueError("voltage_limit must be 75 V, 100 V or 150 V") + self.hub_analog_input = hub_analog_input + + def get_tpz_io_settings(self): + return self.voltage_limit*V, self.hub_analog_input + + +class TdcSim: + def module_identify(self): + pass + + def set_pot_parameters(self, zero_wnd, vel1, wnd1, vel2, wnd2, vel3, + wnd3, vel4): + self.zero_wnd = zero_wnd + self.vel1 = vel1 + self.wnd1 = wnd1 + self.vel2 = vel2 + self.wnd2 = wnd2 + self.vel3 = vel3 + self.wnd3 = wnd3 + self.vel4 = vel4 + + def get_pot_parameters(self): + return (self.zero_wnd, self.vel1, self.wnd1, self.vel2, self.wnd2, + self.vel3, self.wnd3, self.vel4) + + def hub_get_bay_used(self): + return False + + def set_position_counter(self, position): + self.position = position + + def get_position_counter(self): + return self.position + + def set_encoder_counter(self, encoder_count): + self.encoder_count = encoder_count + + def get_encoder_counter(self): + return self.encoder_count + + def set_velocity_parameters(self, acceleration, max_velocity): + self.acceleration = acceleration + self.max_velocity = max_velocity + + def get_velocity_parameters(self): + return self.acceleration, self.max_velocity + + def set_jog_parameters(self, mode, step_size, acceleration, + max_velocity, stop_mode): + self.jog_mode = mode + self.step_size = step_size + self.acceleration = acceleration + self.max_velocity = max_velocity + self.stop_mode = stop_mode + + def get_jog_parameters(self): + return (self.jog_mode, self.step_size, self.acceleration, + self.max_velocity, self.stop_mode) + + def set_gen_move_parameters(self, backlash_distance): + self.backlash_distance = backlash_distance + + def get_gen_move_parameters(self): + return self.backlash_distance + + def set_move_relative_parameters(self, relative_distance): + self.relative_distance = relative_distance + + def get_move_relative_parameters(self): + return self.relative_distance + + def set_move_absolute_parameters(self, absolute_position): + self.absolute_position = absolute_position + + def get_move_absolute_parameters(self): + return self.absolute_position + + def set_home_parameters(self, home_velocity): + self.home_velocity = home_velocity + + def get_home_parameters(self): + return self.home_velocity + + def move_home(self): + pass + + def set_limit_switch_parameters(self, cw_hw_limit, ccw_hw_limit): + self.cw_hw_limit = cw_hw_limit + self.ccw_hw_limit = ccw_hw_limit + + def get_limit_switch_parameters(self): + return self.cw_hw_limit, self.ccw_hw_limit + + def move_relative_memory(self): + pass + + def move_relative(self, relative_distance): + pass + + def move_absolute_memory(self): + pass + + def move_absolute(self, absolute_distance): + pass + + def move_jog(self, direction, async=False): + pass + + def move_velocity(self, direction): + pass + + def move_stop(self, stop_mode, async=False): + pass + + def set_dc_pid_parameters(self, proportional, integral, differential, + integral_limit, filter_control=0x0F): + self.proportional = proportional + self.integral = integral + self.differential = differential + self.integral_limit = integral_limit + self.filter_control = filter_control + + def get_dc_pid_parameters(self): + return (self.proportional, self.integral, self.differential, + self.integral_limit, self.filter_control) + + def set_av_modes(self, mode_bits): + self.mode_bits = mode_bits + + def get_av_modes(self): + return self.mode_bits + + def set_button_parameters(self, mode, position1, position2): + self.mode = mode + self.position1 = position1 + self.position2 = position2 + + def get_button_parameters(self): + return self.mode, self.position1, self.position2 + + def set_eeprom_parameters(self, msg_id): + pass + + def get_dc_status_update(self): + return None # FIXME: not implemented yet for simulation + + def get_status_bits(self): + return None # FIXME: not implemented yet for simulation + + def suspend_end_of_move_messages(self): + pass + + def resume_end_of_move_messages(self): + pass diff --git a/artiq/frontend/thorlabs_tcube_controller.py b/artiq/frontend/thorlabs_tcube_controller.py new file mode 100755 index 000000000..d7046d06e --- /dev/null +++ b/artiq/frontend/thorlabs_tcube_controller.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +import argparse + +from artiq.devices.thorlabs_tcube.driver import Tdc, Tpz, TdcSim, TpzSim +from artiq.protocols.pc_rpc import simple_server_loop +from artiq.tools import verbosity_args, init_logger + + +def get_argparser(): + parser = argparse.ArgumentParser() + parser.add_argument("-d", "--device", required=True, + help="type of the Thorlabs T-Cube device to control", + choices=["TDC001", "TPZ001", "TDCSim", "TPZSim"]) + parser.add_argument("--bind", default="::1", + help="hostname or IP address to bind to") + parser.add_argument("-p", "--port", default=3255, type=int, + help="TCP port to listen to") + parser.add_argument("-s", "--serial", default="/dev/ttyUSB0", + help="serial port: on Windows \"COMx\", on Linux a " + "device path (e.g. \"/dev/ttyUSB0\").") + verbosity_args(parser) + return parser + + +def main(): + args = get_argparser().parse_args() + init_logger(args) + + devname = args.device.lower() + + if devname == "tdc001": + dev = Tdc(args.serial) + elif devname == "tpz001": + dev = Tpz(args.serial) + elif devname == "tdcsim": + dev = TdcSim() + elif devname == "tpzsim": + dev = TpzSim() + else: + raise ValueError("Device can be either TDC001, TPZ001, TDCSim" + " or TPZSim") + + simple_server_loop({devname: dev}, args.bind, args.port) + +if __name__ == "__main__": + main() diff --git a/doc/manual/ndsp_reference.rst b/doc/manual/ndsp_reference.rst index 2eb98f48c..2fab9148c 100644 --- a/doc/manual/ndsp_reference.rst +++ b/doc/manual/ndsp_reference.rst @@ -63,6 +63,29 @@ Controller :ref: artiq.frontend.novatech409b_controller.get_argparser :prog: novatech409b_controller +Thorlabs T-Cube +--------------- + +TDC001 Driver ++++++++++++++ + +.. autoclass:: artiq.devices.thorlabs_tcube.driver.Tdc + :members: + +TPZ001 Driver ++++++++++++++ + +.. autoclass:: artiq.devices.thorlabs_tcube.driver.Tpz + :members: + +Controller +++++++++++ + +.. argparse:: + :ref: artiq.frontend.thorlabs_tcube_controller.get_argparser + :prog: thorlabs_controller + + Default TCP port list --------------------- @@ -81,3 +104,5 @@ When writing a new NDSP, choose a free TCP port and add it to this list. +--------------------------+--------------+ | Novatech 409B | 3254 | +--------------------------+--------------+ +| Thorlabs T-Cube | 3255 | ++--------------------------+--------------+ diff --git a/setup.py b/setup.py index bdda38e8f..ebb0a6861 100755 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ scripts = [ "novatech409b_controller=artiq.frontend.novatech409b_controller:main", "pdq2_client=artiq.frontend.pdq2_client:main", "pdq2_controller=artiq.frontend.pdq2_controller:main", + "thorlabs_tcube_controller=artiq.frontend.thorlabs_tcube_controller:main", ] if os.getenv("ARTIQ_GUI") == "1":