├── .gitignore ├── 99-libftdi.rules ├── LICENSE ├── README.md ├── bench.py ├── configfile.yml ├── get_info.py ├── get_position.py ├── get_status.py ├── get_velocity_params.py ├── goto.py ├── home.py ├── identify.py ├── linearstage.py ├── move.py ├── pyAPT ├── __init__.py ├── controller.py ├── lts300.py ├── message.py ├── mts50.py └── prm1.py ├── raster.py ├── reset.py ├── runner.py ├── set_velocity_params.py └── spiral_scan.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.swp 4 | .Python 5 | bin/ 6 | include/ 7 | lib/ 8 | man/ 9 | -------------------------------------------------------------------------------- /99-libftdi.rules: -------------------------------------------------------------------------------- 1 | UBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="dialout", MODE="0666" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Shuning Bian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | The Software shall be used for Good, not Evil. 16 | 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyAPT 2 | ===== 3 | 4 | Python interface to Thorlab's APT motion controllers. Depends on `libftdi1` and 5 | `pylibftdi`. Under Linux the easiest way to get these is to install `libftdi1` 6 | using the package management system, and then install `pylibftdi` into a virtual 7 | environment or equivalent. 8 | 9 | Development is ongoing, and I will be adding functionality as I need them in 10 | the course of my DPhil. 11 | 12 | Note on stage limits 13 | ==================== 14 | 15 | The stage limits (maximum acceleration, velocity, etc) as quoted on the 16 | Thorlabs website, or in their user manuals, often don't agree with reality. The 17 | best way to get these limits is to install the APT User software, which seems 18 | to have built-in limits for the various stages. These correspond much better to 19 | the actual performance of stages. 20 | -------------------------------------------------------------------------------- /bench.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Usage: python bench.py 5 | 6 | Performs simply benchmarks on how long it takes to list devices, open a device 7 | and performing a status query 8 | """ 9 | from __future__ import absolute_import 10 | from __future__ import print_function 11 | 12 | import pylibftdi 13 | import pyAPT 14 | import time 15 | 16 | def main(args): 17 | print('Looking for APT controllers') 18 | drv = pylibftdi.Driver() 19 | 20 | st = time.time() 21 | controllers = drv.list_devices() 22 | print('\tlist_devices:',time.time()-st) 23 | 24 | if controllers: 25 | for con in controllers: 26 | print('Found %s %s S/N: %s'%con) 27 | st = time.time() 28 | with pyAPT.MTS50(serial_number=con[2]) as con: 29 | print('\topen:',time.time()-st) 30 | st = time.time() 31 | status = con.status() 32 | print('\tstatus:',time.time()-st) 33 | 34 | print('\tController status:') 35 | print('\t\tPosition: %.2fmm'%(status.position)) 36 | print('\t\tVelocity: %.2fmm'%(status.velocity)) 37 | print('\t\tStatus:',status.flag_strings()) 38 | 39 | return 0 40 | else: 41 | print('\tNo APT controllers found. Maybe you need to specify a PID') 42 | return 1 43 | 44 | 45 | if __name__ == '__main__': 46 | import sys 47 | sys.exit(main(sys.argv)) 48 | -------------------------------------------------------------------------------- /configfile.yml: -------------------------------------------------------------------------------- 1 | # Serial Number of each stage 2 | X_AXIS_SN: '83853044' 3 | Y_AXIS_SN: '83854474' 4 | Z_AXIS_SN: '83853018' 5 | 6 | # Scanning Distance Parameters 7 | MAX_DIST: 50 # mm 8 | ENCODER_SCALE: 24576 9 | -------------------------------------------------------------------------------- /get_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python get_info.py [] 4 | 5 | Gets the controller information of all APT controllers, or the one specified 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | 10 | import pyAPT 11 | 12 | from runner import runner_serial 13 | 14 | @runner_serial 15 | def info(serial): 16 | with pyAPT.Controller(serial_number=serial) as con: 17 | info = con.info() 18 | print('\tController info:') 19 | labels=['S/N','Model','Type','Firmware Ver', 'Notes', 'H/W Ver', 20 | 'Mod State', 'Channels'] 21 | 22 | for idx,ainfo in enumerate(info): 23 | print('\t%12s: %s'%(labels[idx], bytes(ainfo))) 24 | 25 | if __name__ == '__main__': 26 | import sys 27 | sys.exit(info()) 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /get_position.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python get_position.py [] 4 | 5 | This program reads the position of all APT controllers found, or the one 6 | specified 7 | """ 8 | from __future__ import absolute_import 9 | from __future__ import print_function 10 | 11 | import time 12 | import pylibftdi 13 | import pyAPT 14 | 15 | def main(args): 16 | print('Looking for APT controllers') 17 | drv = pylibftdi.Driver() 18 | controllers = drv.list_devices() 19 | 20 | if len(args)>1: 21 | serial = args[1] 22 | else: 23 | serial = None 24 | 25 | if serial: 26 | controllers = [x for x in controllers if x[2] == serial] 27 | 28 | if controllers: 29 | for con in controllers: 30 | print('Found %s %s S/N: %s'%con) 31 | with pyAPT.MTS50(serial_number=con[2]) as con: 32 | print('\tPosition (mm) = %.2f [enc:%d]'%(con.position(), con.position(raw=True))) 33 | 34 | return 0 35 | else: 36 | print('\tNo APT controllers found. Maybe you need to specify a PID') 37 | return 1 38 | 39 | if __name__ == '__main__': 40 | import sys 41 | sys.exit(main(sys.argv)) 42 | 43 | -------------------------------------------------------------------------------- /get_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python get_status.py [] 4 | 5 | Gets the status of all APT controllers, or of the one specified 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | import pyAPT 10 | 11 | from runner import runner_serial 12 | 13 | @runner_serial 14 | def status(serial): 15 | with pyAPT.MTS50(serial_number=serial) as con: 16 | status = con.status() 17 | print('\tController status:') 18 | print('\t\tPosition: %.3fmm (%d cnt)'%(status.position, status.position_apt)) 19 | print('\t\tVelocity: %.3fmm'%(status.velocity)) 20 | print('\t\tStatus:',status.flag_strings()) 21 | 22 | 23 | if __name__ == '__main__': 24 | import sys 25 | sys.exit(status()) 26 | 27 | -------------------------------------------------------------------------------- /get_velocity_params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python get_status.py [] 4 | 5 | Gets the status of all APT controllers, or of the one specified 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | import pyAPT 10 | 11 | from runner import runner_serial 12 | 13 | @runner_serial 14 | def get_vel_params(serial): 15 | with pyAPT.MTS50(serial_number=serial) as con: 16 | min_vel, acc, max_vel = con.velocity_parameters() 17 | raw_min_vel, raw_acc, raw_max_vel = con.velocity_parameters(raw=True) 18 | print('\tController velocity parameters:') 19 | print('\t\tMin. Velocity: %.2fmm/s (%d)'%(min_vel, raw_min_vel)) 20 | print('\t\tAcceleration: %.2fmm/s/s (%d)'%(acc, raw_acc)) 21 | print('\t\tMax. Velocity: %.2fmm/s (%d)'%(max_vel, raw_max_vel)) 22 | 23 | if __name__ == '__main__': 24 | import sys 25 | sys.exit(get_vel_params()) 26 | 27 | -------------------------------------------------------------------------------- /goto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python goto.py 4 | 5 | This program tells the specified controller to move the stage to the specified 6 | position. 7 | """ 8 | from __future__ import absolute_import 9 | from __future__ import print_function 10 | 11 | import time 12 | import pylibftdi 13 | import pyAPT 14 | import sys 15 | 16 | def main(args): 17 | if len(args)<3: 18 | print(__doc__) 19 | return 1 20 | else: 21 | serial = args[1] 22 | position = float(args[2]) 23 | 24 | try: 25 | with pyAPT.MTS50(serial_number=serial) as con: 26 | print('Found APT controller S/N',serial) 27 | print('\tMoving stage to %.2fmm...'%(position)) 28 | st=time.time() 29 | con.goto(position, wait=False) 30 | stat = con.status() 31 | while stat.moving: 32 | out = ' pos %3.2fmm vel %3.2fmm/s'%(stat.position, stat.velocity) 33 | sys.stdout.write(out) 34 | time.sleep(0.01) 35 | stat=con.status() 36 | l = len(out) 37 | sys.stdout.write('\b'*l) 38 | sys.stdout.write(' '*l) 39 | sys.stdout.write('\b'*l) 40 | 41 | print('\tMove completed in %.2fs'%(time.time()-st)) 42 | print('\tNew position: %.2fmm'%(con.position())) 43 | print('\tStatus:',con.status()) 44 | return 0 45 | except pylibftdi.FtdiError as ex: 46 | print('\tCould not find APT controller S/N of',serial) 47 | return 1 48 | 49 | if __name__ == '__main__': 50 | import sys 51 | sys.exit(main(sys.argv)) 52 | 53 | -------------------------------------------------------------------------------- /home.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python home.py [] 4 | 5 | This program homes all APT controllers found, or of the one specified 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | 10 | import time 11 | import pyAPT 12 | 13 | from runner import runner_serial 14 | 15 | @runner_serial 16 | def home(serial): 17 | with pyAPT.MTS50(serial_number=serial) as con: 18 | print('\tIdentifying controller') 19 | con.identify() 20 | print('\tHoming parameters:', con.request_home_params()) 21 | print('\tHoming stage...', end=' ') 22 | con.home(velocity = 10) 23 | print('homed') 24 | 25 | if __name__ == '__main__': 26 | import sys 27 | sys.exit(home()) 28 | 29 | -------------------------------------------------------------------------------- /identify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python identify.py [] 4 | 5 | Finds all APT controllers and flashes their activity lights 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | import time 10 | import pyAPT 11 | import sys 12 | from runner import runner_serial 13 | 14 | @runner_serial 15 | def identify(serial): 16 | with pyAPT.Controller(serial_number=serial) as con: 17 | print('\tIdentifying controller') 18 | con.identify() 19 | print('\n>>>>Press enter to continue') 20 | sys.stdin.readline() 21 | 22 | if __name__ == '__main__': 23 | sys.exit(identify()) 24 | 25 | -------------------------------------------------------------------------------- /linearstage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @brief Class that represents a Thorlabs 3D linear stage. 4 | It allows the user to perform different types of scans throught 5 | the workspace. 6 | @author Luis Carlos Garcia-Peraza Herrera 7 | @author Efthymios Maneas 8 | 9 | The coordinate system of the 3D linear stage is as follows: 10 | 11 | Top view: Right view: Front view: 12 | --------- ----------- ----------- 13 | 14 | Motor controllers 15 | __ __ __ 16 | |__||__||__| 17 | 18 | Y Z Z 19 | ^ ^ ^ 20 | | | | __  __ __ 21 | | | __ | | || || | 22 | o-----> X o-----> Y |__| o-----> X 23 | 24 | Pre-requisites: 25 | 26 | 1) pip install --upgrade matplotlib 27 | 2) pip install --upgrade mpl_toolkits 28 | 3) Run this command before executing this script: 29 | export PYTHONPATH=/Library/Python/2.7/site-packages 30 | 31 | ''' 32 | from __future__ import absolute_import 33 | from __future__ import print_function 34 | from __future__ import division 35 | 36 | import pyAPT 37 | import threading 38 | import time 39 | import yaml 40 | import sys 41 | from math import * 42 | from runner import runner_serial 43 | from matplotlib import pyplot as plt 44 | from mpl_toolkits.mplot3d import Axes3D 45 | 46 | class LinearStage(object): 47 | 48 | ''' 49 | @brief Loading configuration from config file. 50 | ''' 51 | def __init__(self): 52 | config = yaml.load(open("configfile.yml")) # $ pip install pyyaml 53 | 54 | # Reading linear stage serial number from config file 55 | self.X_AXIS_SN = config["X_AXIS_SN"] 56 | self.Y_AXIS_SN = config["Y_AXIS_SN"] 57 | self.Z_AXIS_SN = config["Z_AXIS_SN"] 58 | 59 | # Reading distance range and scaling from config file 60 | self.MAX_DIST = config["MAX_DIST"] 61 | self.ENCODER_SCALE = config["ENCODER_SCALE"] 62 | self.MAX_DIST_ENCODER = self.MAX_DIST * self.ENCODER_SCALE 63 | 64 | # Moving 3D Stage Flags 65 | self.RIGHT = 0 66 | self.LEFT = 1 67 | self.DOWN = 0 68 | self.UP = 1 69 | 70 | # Plotting stuff 71 | self.fig = plt.figure() 72 | plt.ion() 73 | self.ax = self.fig.gca(projection = '3d') 74 | self.ax.set_xlim3d(0, self.MAX_DIST) 75 | self.ax.set_ylim3d(0, self.MAX_DIST) 76 | self.ax.set_zlim3d(0, self.MAX_DIST) 77 | # self.ax.view_init(elev = 45, azim = 90) 78 | 79 | def getInfoAxis(self, axis): 80 | con = pyAPT.MTS50(serial_number = axis) 81 | ret = con.info() 82 | con.close() 83 | return ret 84 | 85 | ''' 86 | @brief Prints the serial number, model, type, firmware version and servo of all the connected stages. 87 | ''' 88 | def getInfo(self): 89 | labels = ['S/N','Model','Type','Firmware Ver', 'Notes', 'H/W Ver', 'Mod State', 'Channels'] 90 | xInfo = self.getInfoAxis(self.X_AXIS_SN) 91 | yInfo = self.getInfoAxis(self.Y_AXIS_SN) 92 | zInfo = self.getInfoAxis(self.Z_AXIS_SN) 93 | print("\nInformation of the X axis:") 94 | print('--------------------------') 95 | for idx, ainfo in enumerate(xInfo): 96 | print(("\t%12s: %s" % (labels[idx], bytes(ainfo)))) 97 | print("\nInformation of the Y axis:") 98 | print('--------------------------') 99 | for idx, ainfo in enumerate(yInfo): 100 | print(("\t%12s: %s" % (labels[idx], bytes(ainfo)))) 101 | print("\nInformation of the Z axis:") 102 | print('--------------------------') 103 | for idx, ainfo in enumerate(zInfo): 104 | print(("\t%12s: %s" % (labels[idx], bytes(ainfo)))) 105 | print("\n") 106 | 107 | ''' 108 | @brief Obtains the current position, velocity and status of a linear stage connected through USB. 109 | @param[in] axis Serial number of the target linear stage. 110 | @returns Status for the stage with the serial number provided. 111 | ''' 112 | def getStatusAxis(self, axis): 113 | con = pyAPT.MTS50(serial_number = axis) 114 | ret = con.status() 115 | con.close() 116 | return ret 117 | 118 | ''' 119 | @brief Prints the axis, position and velocity of the connected stages. 120 | ''' 121 | def getStatus(self): 122 | xStatus = self.getStatusAxis(self.X_AXIS_SN) 123 | yStatus = self.getStatusAxis(self.Y_AXIS_SN) 124 | zStatus = self.getStatusAxis(self.Z_AXIS_SN) 125 | print("\nAxis: Position [mm]: Velocity [mm/s]:") 126 | print('----- -------------- ----------------') 127 | print(("X %6.3f %6.3f" % (float(self.MAX_DIST) - xStatus.position, xStatus.velocity))) 128 | print(("Y %6.3f %6.3f" % (yStatus.position, yStatus.velocity))) 129 | print(("Z %6.3f %6.3f\n" % (float(self.MAX_DIST) - zStatus.position, zStatus.velocity))) 130 | 131 | ''' 132 | @brief Provides the position of one or all axes. 133 | @param[in] axis String with the name of the axis we want to retrieve. 134 | @returns position[s] [X, Y, Z] of the stage. 135 | ''' 136 | def getPos(self, axis = None): 137 | if (axis == 'X' or axis == 'x' or axis == None): 138 | with pyAPT.MTS50(serial_number = self.X_AXIS_SN) as con: 139 | status = con.status() 140 | posX = float(self.MAX_DIST_ENCODER - status.position_apt) / self.ENCODER_SCALE 141 | if (axis != None): 142 | return posX 143 | if (axis == 'Y' or axis == 'y' or axis == None): 144 | with pyAPT.MTS50(serial_number = self.Y_AXIS_SN) as con: 145 | status = con.status() 146 | posY = float(status.position_apt) / self.ENCODER_SCALE 147 | if (axis != None): 148 | return posY 149 | if (axis == 'Z' or axis == 'z' or axis == None): 150 | with pyAPT.MTS50(serial_number = self.Z_AXIS_SN) as con: 151 | status = con.status() 152 | posZ = float(self.MAX_DIST_ENCODER - status.position_apt) / self.ENCODER_SCALE 153 | if (axis != None): 154 | return posZ 155 | return [posX, posY, posZ] 156 | 157 | ''' 158 | @brief Sends the 3D linear stage to the position (0, 0, 0). 159 | ''' 160 | def goHome(self): 161 | # Move to home position of the stage 162 | self.moveAbsolute(self.MAX_DIST, 0, self.MAX_DIST) 163 | 164 | # Verify X axis home position 165 | con = pyAPT.MTS50(serial_number = self.X_AXIS_SN) 166 | con.home() 167 | con.close() 168 | 169 | # Verify Y axis home position 170 | con = pyAPT.MTS50(serial_number = self.Y_AXIS_SN) 171 | con.home() 172 | con.close() 173 | 174 | # Verify Z axis home position 175 | con = pyAPT.MTS50(serial_number = self.Z_AXIS_SN) 176 | con.home() 177 | con.close() 178 | 179 | # Move to our reference frame home position 180 | self.moveAbsolute(0, 0, 0) 181 | 182 | ''' 183 | @brief This method performs a 3D raster scan. 184 | @param[in] step Increment in microns from point to point. 185 | @param[in] delay Seconds of delay after each position has been reached. 186 | FIXME: show points in graph at the same time that the stage is moving. 187 | FIXME: rotate the 3D view so that it is equivalent to the real coordinate frame. 188 | ''' 189 | def rasterScan(self, step, delay): 190 | # FIXME: reset plot if it was already opened 191 | # plt.close() 192 | 193 | # Going home to reset the encoders 194 | sys.stdout.write('Homing... ') 195 | sys.stdout.flush() 196 | self.moveAbsolute(0, 0, 0) 197 | print('OK') 198 | 199 | # Setting the initial direction of the X (k) and Y(j) axes 200 | kDir = self.RIGHT 201 | jDir = self.DOWN 202 | 203 | # Initialising iterators 204 | i = 0.0 205 | j = 0.0 206 | k = 0.0 207 | 208 | # Showing the window with the plot of the points 209 | # plt.show() 210 | 211 | # Looping through the workspace in a raster fashion 212 | while (i <= self.MAX_DIST): 213 | if j > self.MAX_DIST: 214 | j = self.MAX_DIST 215 | jDir = self.UP 216 | if j < 0: 217 | j = 0 218 | jDir = self.DOWN 219 | while (j >= 0 and j <= self.MAX_DIST): 220 | if k > self.MAX_DIST: 221 | k = self.MAX_DIST 222 | kDir = self.LEFT 223 | if k < 0: 224 | k = 0 225 | kDir = self.RIGHT 226 | while (k >= 0 and k <= self.MAX_DIST): 227 | self.moveAbsolute(k, j, i) 228 | print(('Current position: %6.3f %6.3f %6.3f' % (k, j, i))) 229 | # self.ax.scatter(k, j, i, zdir = 'z', c = 'red') 230 | # plt.draw() 231 | time.sleep(delay) 232 | print('Moving to next position ...') 233 | if kDir == self.RIGHT: 234 | k += step 235 | else: 236 | k -= step 237 | if jDir == self.DOWN: 238 | j += step 239 | else: 240 | j -= step 241 | i += step 242 | 243 | ''' 244 | @brief Cylindrical scan starting from the floor and going up. For each height level it 245 | performs (self.MAX_DIST / step) circles. The scanning is clockwise. The spacing 246 | between the points of each circle is maintained the same regardless the distance 247 | to the centre of the cylinder. That is, the external circles have more points 248 | than the internal ones to maintain the same spacing. 249 | @param[in] stepAngle Initial angle of separation between points. It is a ratio of pi. 250 | @param[in] step Increment of the radius of the circle for each step of scanning. 251 | It is also used for the increment in the z axis. It is a ratio of 252 | the maximum distance. 253 | @param[in] delay Delay in seconds after a position has been reached. 254 | FIXME: rotate the 3D view so that it is equivalent to the real coordinate frame. 255 | ''' 256 | def cylindricalScan(self, stepAngle, step, delay): 257 | # Checking that the parameters are ratios 258 | if (stepAngle > 1 or step > 1): 259 | print('The step angle and the step must be lower than one because they are ratios.') 260 | 261 | IN = 0 262 | OUT = 1 263 | stepAngle *= pi 264 | initialStepAngle = stepAngle 265 | phi = pi 266 | step *= self.MAX_DIST 267 | r = step 268 | z = 0 269 | rDir = OUT 270 | epsilon = 0.001 271 | 272 | # FIXME: reset plot if it was already opened 273 | # plt.close() 274 | 275 | # Going home to reset the encoders 276 | sys.stdout.write('Homing... ') 277 | sys.stdout.flush() 278 | # self.moveAbsolute(0, 0, 0) 279 | print('OK') 280 | 281 | # Showing the window with the plot of the points 282 | plt.show() 283 | 284 | # Looping around all the points of the cylinder 285 | while (z <= self.MAX_DIST): 286 | if r > self.MAX_DIST / 2: 287 | stepAngle = (stepAngle * r) / (r - step) 288 | r -= step 289 | rDir = IN 290 | else: 291 | stepAngle = initialStepAngle 292 | r = step 293 | rDir = OUT 294 | x = self.MAX_DIST / 2 295 | y = self.MAX_DIST / 2 296 | self.moveAbsolute(x, y, z) 297 | print(('Current position: %6.3f %6.3f %6.3f' % (x, y, z))) 298 | self.ax.scatter(x, y, z, zdir = 'z', c = 'red') 299 | plt.draw() 300 | time.sleep(delay) 301 | while ((r > step or abs(r - step) < epsilon) and (r < self.MAX_DIST / 2 or abs(r - self.MAX_DIST / 2) < epsilon)): 302 | while (phi > -pi): 303 | x = r * cos(phi) + self.MAX_DIST / 2 304 | y = r * sin(phi) + self.MAX_DIST / 2 305 | self.moveAbsolute(x, y, z) 306 | print(('Current position: %6.3f %6.3f %6.3f' % (x, y, z))) 307 | self.ax.scatter(x, y, z, zdir = 'z', c = 'red') 308 | plt.draw() 309 | time.sleep(delay) 310 | phi -= stepAngle 311 | if (rDir == IN): 312 | if (abs(r - step) > epsilon): 313 | stepAngle = (stepAngle * r) / (r - step) 314 | r -= step 315 | else: 316 | stepAngle = (stepAngle * r) / (r + step) 317 | r += step 318 | phi = pi 319 | if (rDir == IN): 320 | x = self.MAX_DIST / 2 321 | y = self.MAX_DIST / 2 322 | self.moveAbsolute(x, y, z) 323 | print(('Current position: %6.3f %6.3f %6.3f' % (x, y, z))) 324 | self.ax.scatter(x, y, z, zdir = 'z', c = 'red') 325 | plt.draw() 326 | time.sleep(delay) 327 | z += step 328 | 329 | ''' 330 | @brief Moving X axis of the stage to the position x (mm) 331 | @param[in] x Goal position in mm. 332 | ''' 333 | def moveAbsoluteX(self, x): 334 | x = float(self.MAX_DIST) - x 335 | con = pyAPT.MTS50(serial_number = self.X_AXIS_SN) 336 | con.goto(x, wait = False) 337 | stat = con.status() 338 | while stat.moving: 339 | time.sleep(0.01) 340 | stat = con.status() 341 | con.close() 342 | 343 | ''' 344 | @brief Moving Y axis of the stage to the position y (mm) 345 | @param[in] y Goal position in mm. 346 | ''' 347 | def moveAbsoluteY(self, y): 348 | con = pyAPT.MTS50(serial_number = self.Y_AXIS_SN) 349 | con.goto(y, wait = False) 350 | stat = con.status() 351 | while stat.moving: 352 | time.sleep(0.01) 353 | stat = con.status() 354 | con.close() 355 | 356 | ''' 357 | @brief Moving Z axis of the stage to the position z (mm) 358 | @param[in] z Goal position in mm. 359 | ''' 360 | def moveAbsoluteZ(self, z): 361 | z = float(self.MAX_DIST) - z 362 | con = pyAPT.MTS50(serial_number = self.Z_AXIS_SN) 363 | con.goto(z, wait = False) 364 | stat = con.status() 365 | while stat.moving: 366 | time.sleep(0.01) 367 | stat = con.status() 368 | con.close() 369 | 370 | ''' 371 | @brief Move the stage to the position x, y, z. 372 | @param[in] x Position of the x axis in mm. 373 | @param[in] y Position of the y axis in mm. 374 | @param[in] z Position of the z axis in mm. 375 | @param[in] delay Delay (in seconds) after each position has been reached. 376 | ''' 377 | def moveAbsolute(self, x, y, z): 378 | #tx = threading.Thread(target = self.moveAbsoluteX(x)) 379 | #ty = threading.Thread(target = self.moveAbsoluteY(y)) 380 | #tz = threading.Thread(target = self.moveAbsoluteZ(z)) 381 | #tx.daemon = True 382 | #ty.daemon = True 383 | #tz.daemon = True 384 | #tx.start() 385 | #ty.start() 386 | #tz.start() 387 | self.moveAbsoluteX(x) 388 | self.moveAbsoluteY(y) 389 | self.moveAbsoluteZ(z) 390 | 391 | ''' 392 | @brief TODO 393 | @param[in] x TODO 394 | @param[in] y TODO 395 | @param[in] z TODO 396 | @param[in] delayMs TODO 397 | ''' 398 | def moveRelative(self, x, y, z): 399 | return 0 400 | -------------------------------------------------------------------------------- /move.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python move.py 4 | 5 | This program tells the specified controller to move the stage by the specified 6 | distance in mm 7 | """ 8 | from __future__ import absolute_import 9 | from __future__ import print_function 10 | 11 | import time 12 | import pylibftdi 13 | import pyAPT 14 | 15 | def main(args): 16 | if len(args)<3: 17 | print(__doc__) 18 | return 1 19 | else: 20 | serial = args[1] 21 | dist = float(args[2]) 22 | 23 | try: 24 | with pyAPT.MTS50(serial_number=serial) as con: 25 | print('Found APT controller S/N',serial) 26 | print('\tMoving stage by %.2fmm...'%(dist), end=' ') 27 | con.move(dist) 28 | print('moved') 29 | print('\tNew position: %.2fmm'%(con.position())) 30 | return 0 31 | except pylibftdi.FtdiError as ex: 32 | print('\tCould not find APT controller S/N of',serial) 33 | return 1 34 | 35 | if __name__ == '__main__': 36 | import sys 37 | sys.exit(main(sys.argv)) 38 | 39 | -------------------------------------------------------------------------------- /pyAPT/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import pylibftdi 3 | 4 | from pyAPT import message, controller, mts50, prm1 5 | 6 | __version__ = "0.01" 7 | __author__ = "Shuning Bian" 8 | 9 | __all__ = ['Message', 'Controller', 'MTS50', 'OutOfRangeError', 'PRM1', 10 | 'add_PID'] 11 | 12 | Message = message.Message 13 | Controller = controller.Controller 14 | MTS50 = mts50.MTS50 15 | PRM1 = prm1.PRM1 16 | OutOfRangeError = controller.OutOfRangeError 17 | 18 | _PRODUCT_IDS = pylibftdi.USB_PID_LIST 19 | _PRODUCT_IDS[:] = [0xFAF0] 20 | 21 | 22 | def add_PID(pid): 23 | """ 24 | Adds a USB PID to the list of PIDs to look for when searching for APT 25 | controllers 26 | """ 27 | _PRODUCT_IDS.append(pid) 28 | -------------------------------------------------------------------------------- /pyAPT/controller.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple class which encapsulate an APT controller 3 | """ 4 | from __future__ import absolute_import, division 5 | import pylibftdi 6 | import time 7 | import struct as st 8 | 9 | from .message import Message 10 | from . import message 11 | 12 | class OutOfRangeError(Exception): 13 | def __init__(self, requested, allowed): 14 | val = '%f requested, but allowed range is %.2f..%.2f'%(requested, allowed[0], allowed[1]) 15 | super(OutOfRangeError, self).__init__(val) 16 | 17 | class Controller(object): 18 | def __init__(self, serial_number=None, label=None): 19 | super(Controller, self).__init__() 20 | 21 | if type(serial_number) == bytes: 22 | serial_number = serial_number.decode() 23 | else: 24 | serial_number = str(serial_number) 25 | 26 | # this takes up to 2-3s: 27 | dev = pylibftdi.Device(mode='b', device_id=serial_number) 28 | dev.baudrate = 115200 29 | 30 | def _checked_c(ret): 31 | if not ret == 0: 32 | raise Exception(dev.ftdi_fn.ftdi_get_error_string()) 33 | 34 | _checked_c(dev.ftdi_fn.ftdi_set_line_property( 8, # number of bits 35 | 1, # number of stop bits 36 | 0 # no parity 37 | )) 38 | time.sleep(50.0/1000) 39 | 40 | dev.flush(pylibftdi.FLUSH_BOTH) 41 | 42 | time.sleep(50.0/1000) 43 | 44 | # skipping reset part since it looks like pylibftdi does it already 45 | 46 | # this is pulled from ftdi.h 47 | SIO_RTS_CTS_HS = (0x1 << 8) 48 | _checked_c(dev.ftdi_fn.ftdi_setflowctrl(SIO_RTS_CTS_HS)) 49 | 50 | _checked_c(dev.ftdi_fn.ftdi_setrts(1)) 51 | 52 | self.serial_number = serial_number 53 | self.label = label 54 | self._device = dev 55 | 56 | # some conservative limits 57 | # velocity is in mm/s 58 | # acceleration is in mm^2/s 59 | self.max_velocity = 0.3 60 | self.max_acceleration = 0.3 61 | 62 | # these define how encode count translates into position, velocity 63 | # and acceleration. e.g. 1 mm is equal to 1 * self.position_scale 64 | 65 | # these are set to None on purpose - you should never use this class 66 | # as is. 67 | self.position_scale = None 68 | self.velocity_scale = None 69 | self.acceleration_scale = None 70 | 71 | # defines the linear, i.e. distance, range of the controller 72 | # unit is in mm 73 | self.linear_range = (0,50) 74 | 75 | # whether or not sofware limit in position is applied 76 | self.soft_limits = True 77 | 78 | # the message queue are messages that are sent asynchronously. For example 79 | # if we performed a move, and are waiting for move completed message, 80 | # any other message received in the mean time are place in the queue. 81 | self.message_queue = [] 82 | 83 | def __enter__(self): 84 | return self 85 | 86 | def __exit__(self, type_, value, traceback): 87 | self.close() 88 | 89 | def __del__(self): 90 | self.close() 91 | 92 | def close(self): 93 | if not self._device.closed: 94 | # print 'Closing connnection to controller',self.serial_number 95 | self.stop(wait=False) 96 | # XXX we might want a timeout here, or this will block forever 97 | self._device.close() 98 | 99 | def _send_message(self, m): 100 | """ 101 | m should be an instance of Message, or has a pack() method which returns 102 | bytes to be sent to the controller 103 | """ 104 | self._device.write(m.pack()) 105 | 106 | def _read(self, length, block=True): 107 | """ 108 | If block is True, then we will return only when have have length number of 109 | bytes. Otherwise we will perform a read, then immediately return with 110 | however many bytes we managed to read. 111 | 112 | Note that if no data is available, then an empty byte string will be 113 | returned. 114 | """ 115 | data = bytes() 116 | while len(data) < length: 117 | diff = length - len(data) 118 | data += self._device.read(diff) 119 | if not block: 120 | break 121 | 122 | time.sleep(0.001) 123 | 124 | return data 125 | 126 | def _read_message(self): 127 | data = self._read(message.MGMSG_HEADER_SIZE) 128 | msg = Message.unpack(data, header_only=True) 129 | if msg.hasdata: 130 | data = self._read(msg.datalength) 131 | msglist = list(msg) 132 | msglist[-1] = data 133 | return Message._make(msglist) 134 | return msg 135 | 136 | def _wait_message(self, expected_messageID): 137 | found = False 138 | while not found: 139 | m = self._read_message() 140 | found = m.messageID == expected_messageID 141 | if found: 142 | return m 143 | else: 144 | self.message_queue.append(m) 145 | 146 | def _position_in_range(self, absolute_pos_mm): 147 | """ 148 | Returns True if requested absolute position is within range, False 149 | otherwise 150 | """ 151 | # get rid of floating point artifacts below our resolution 152 | enccnt = int(absolute_pos_mm * self.position_scale) 153 | absolute_pos_mm = enccnt / self.position_scale 154 | 155 | if absolute_pos_mm < self.linear_range[0]: 156 | return False 157 | 158 | if absolute_pos_mm > self.linear_range[1]: 159 | return False 160 | 161 | return True 162 | 163 | def status(self, channel=1): 164 | """ 165 | Returns the status of the controller, which is its position, velocity, and 166 | statusbits 167 | 168 | Position and velocity will be in mm and mm/s respectively. 169 | """ 170 | reqmsg = Message(message.MGMSG_MOT_REQ_DCSTATUSUPDATE, param1=channel) 171 | self._send_message(reqmsg) 172 | 173 | getmsg = self._wait_message(message.MGMSG_MOT_GET_DCSTATUSUPDATE) 174 | return ControllerStatus(self, getmsg.datastring) 175 | 176 | def identify(self): 177 | """ 178 | Flashes the controller's activity LED 179 | """ 180 | idmsg = Message(message.MGMSG_MOD_IDENTIFY) 181 | self._send_message(idmsg) 182 | 183 | def reset_parameters(self): 184 | """ 185 | Resets all parameters to their EEPROM default values. 186 | 187 | IMPORTANT: only one class of controller appear to support this at the 188 | moment, that being the BPC30x series. 189 | """ 190 | resetmsg = Message(message.MGMSG_MOT_SET_PZSTAGEPARAMDEFAULTS) 191 | self._send_message(resetmsg) 192 | 193 | def request_home_params(self): 194 | reqmsg = Message(message.MGMSG_MOT_REQ_HOMEPARAMS) 195 | self._send_message(reqmsg) 196 | 197 | getmsg = self._wait_message(message.MGMSG_MOT_GET_HOMEPARAMS) 198 | dstr = getmsg.datastring 199 | 200 | """ 201 | <: little endian 202 | H: 2 bytes for channel id 203 | H: 2 bytes for home direction 204 | H: 2 bytes for limit switch 205 | i: 4 bytes for homing velocity 206 | i: 4 bytes for offset distance 207 | """ 208 | return st.unpack(' (3, 0)): 143 | if type(self.data) == bytes: 144 | return self.data 145 | else: 146 | return self.data.encode() 147 | else: 148 | if type(self.data) == str: 149 | return self.data 150 | else: 151 | return ''.join(chr(x) for x in self.data) 152 | 153 | @property 154 | def datalength(self): 155 | if self.hasdata: 156 | if self.data: 157 | return len(self.data) 158 | else: 159 | return self.param1 | (self.param2<<8) 160 | else: 161 | return -1 162 | 163 | @property 164 | def hasdata(self): 165 | return self.dest & 0x80 166 | 167 | 168 | def pack_unpack_test(): 169 | """ 170 | If we pack a message, then unpack it, we should recover the message exactly. 171 | """ 172 | a = Message(0x0223,data=[1,2,3,4,5]) 173 | s = a.pack(True) 174 | b = Message.unpack(s) 175 | assert a == b 176 | 177 | MGMSG_HEADER_SIZE = 6 178 | 179 | # Generic Commands 180 | MGMSG_MOD_IDENTIFY = 0x0223 181 | MGMSG_HW_RESPONSE = 0x0080 182 | 183 | MGMSG_HW_REQ_INFO = 0x0005 184 | MGMSG_HW_GET_INFO = 0x0006 185 | 186 | MGMSG_MOT_ACK_DCSTATUSUPDATE = 0x0492 187 | 188 | # Motor Commands 189 | MGMSG_MOT_SET_PZSTAGEPARAMDEFAULTS = 0x0686 190 | 191 | MGMSG_MOT_MOVE_HOME = 0x0443 192 | MGMSG_MOT_MOVE_HOMED = 0x0444 193 | MGMSG_MOT_MOVE_ABSOLUTE = 0x0453 194 | MGMSG_MOT_MOVE_COMPLETED = 0x0464 195 | 196 | MGMSG_MOT_SET_HOMEPARAMS = 0x0440 197 | MGMSG_MOT_REQ_HOMEPARAMS = 0x0441 198 | MGMSG_MOT_GET_HOMEPARAMS = 0x0442 199 | 200 | MGMSG_MOT_REQ_POSCOUNTER = 0x0411 201 | MGMSG_MOT_GET_POSCOUNTER = 0x0412 202 | 203 | MGMSG_MOT_REQ_DCSTATUSUPDATE = 0x0490 204 | MGMSG_MOT_GET_DCSTATUSUPDATE = 0x0491 205 | 206 | MGMSG_MOT_SET_VELPARAMS = 0x413 207 | MGMSG_MOT_REQ_VELPARAMS = 0x414 208 | MGMSG_MOT_GET_VELPARAMS = 0x415 209 | 210 | MGMSG_MOT_SUSPEND_ENDOFMOVEMSGS = 0x046B 211 | MGMSG_MOT_RESUME_ENDOFMOVEMSGS = 0x046C 212 | 213 | MGMSG_MOT_MOVE_STOP = 0x0465 214 | MGMSG_MOT_MOVE_STOPPED = 0x0466 215 | -------------------------------------------------------------------------------- /pyAPT/mts50.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | from .controller import Controller 3 | 4 | class MTS50(Controller): 5 | """ 6 | A controller for a MTS50/M-Z8 stage. 7 | """ 8 | def __init__(self,*args, **kwargs): 9 | super(MTS50, self).__init__(*args, **kwargs) 10 | 11 | # http://www.thorlabs.co.uk/thorProduct.cfm?partNumber=MTS50/M-Z8 12 | # Note that these values are pulled from the APT User software, 13 | # as they agree with the real limits of the stage better than 14 | # what the website or the user manual states 15 | self.max_velocity = 0.45 16 | self.max_acceleration = 0.45 17 | 18 | # from private communication with thorlabs tech support: 19 | # steps per revolution: 48 20 | # gearbox ratio: 256 21 | # pitch: 0.5 mm 22 | # thus to advance 1 mm you need to turn 48*256*2 times 23 | enccnt = 48*256*2 24 | T = 2048/6e6 25 | 26 | # these equations are taken from the APT protocol manual 27 | self.position_scale = enccnt 28 | self.velocity_scale = enccnt * T * 65536 29 | self.acceleration_scale = enccnt * T * T * 65536 30 | 31 | self.linear_range = (0,50) 32 | 33 | -------------------------------------------------------------------------------- /pyAPT/prm1.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division 2 | from .controller import Controller 3 | 4 | class PRM1(Controller): 5 | """ 6 | A controller for a PRM1 rotation stage 7 | """ 8 | def __init__(self,*args, **kwargs): 9 | super(PRM1, self).__init__(*args, **kwargs) 10 | 11 | # http://www.thorlabs.de/newgrouppage9.cfm?objectgroup_id=2875 12 | # Note that these values should be pulled from the APT User software, 13 | # as they agree with the real limits of the stage better than 14 | # what the website or the user manual states 15 | self.max_velocity = 0.3 # units? 16 | self.max_acceleration = 0.3 # units? 17 | 18 | # from the manual 19 | # encoder counts per revoultion of the output shaft: 34304 20 | # no load speed: 16500 rpm = 275 1/s 21 | # max rotation velocity: 25deg/s 22 | # Gear ratio: 274 / 25 rounds/deg 23 | # to move 1 deg: 274/25 rounds = 274/25 * 34304 encoder steps 24 | # measured value: 1919.2689 25 | # There is an offset off 88.2deg -> enc(0) = 88.2deg 26 | enccnt = 1919.2698 27 | 28 | T = 2048/6e6 29 | 30 | # these equations are taken from the APT protocol manual 31 | self.position_scale = enccnt #the number of enccounts per deg 32 | self.velocity_scale = enccnt * T * 65536 33 | self.acceleration_scale = enccnt * T * T * 65536 34 | 35 | self.linear_range = (-180,180) 36 | 37 | -------------------------------------------------------------------------------- /raster.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from numpy import * 3 | from mpl_toolkits.mplot3d import * 4 | from matplotlib import pyplot as plt 5 | import time 6 | 7 | i = 0 8 | j = 0 9 | k = 0 10 | maxSize = 5 11 | step = 1 12 | xscan = [] 13 | yscan = [] 14 | zscan = [] 15 | 16 | ''' 17 | Constants 18 | ''' 19 | RIGHT = 0 20 | LEFT = 1 21 | DOWN = 0 22 | UP = 1 23 | 24 | 25 | kDir = RIGHT 26 | jDir = DOWN 27 | 28 | fig = plt.figure() 29 | ax = fig.gca(projection='3d') # to work in 3d 30 | #plt.axes([0, maxSize, 0, maxSize]) 31 | ax.set_xlim3d(0, maxSize) 32 | ax.set_ylim3d(0, maxSize) 33 | ax.set_zlim3d(0, maxSize) 34 | 35 | ax.set_xlabel('X axis') 36 | ax.set_ylabel('Y axis') 37 | ax.set_zlabel('Z axis') 38 | 39 | plt.ion() 40 | plt.show() 41 | 42 | while (i <= maxSize): 43 | if j > maxSize: 44 | j = maxSize 45 | jDir = UP 46 | if j < 0: 47 | j = 0 48 | jDir = DOWN 49 | while (j >= 0 and j <= maxSize): 50 | if k > maxSize: 51 | k = maxSize 52 | kDir = LEFT 53 | if k < 0: 54 | k = 0 55 | kDir = RIGHT 56 | while (k >= 0 and k <= maxSize): 57 | xscan.append(k) 58 | yscan.append(j) 59 | zscan.append(i) 60 | 61 | #ax = fig.gca(projection='3d') # to work in 3d 62 | ax.scatter(k, j, i, zdir='z', c= 'red') 63 | plt.draw() 64 | time.sleep(0.01) 65 | 66 | if kDir == RIGHT: 67 | k += step 68 | else: 69 | k -= step 70 | if jDir == DOWN: 71 | j += step 72 | else: 73 | j -= step 74 | i += step 75 | 76 | #fig = plt.figure() 77 | #ax = fig.gca(projection='3d') # to work in 3d 78 | #ax = fig.add_subplot(222, projection='3d') 79 | #ax.plot(xscan, yscan, zscan, zdir='z', c= 'red') 80 | plt.hold(True) 81 | #plt.show() 82 | -------------------------------------------------------------------------------- /reset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python reset.py [] 4 | 5 | Resets all controller parameters to their default EEPROM value. 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | import pyAPT 10 | 11 | from runner import runner_serial 12 | 13 | @runner_serial 14 | def reset(serial): 15 | with pyAPT.Controller(serial_number=serial) as con: 16 | print('\tResetting controller parameters to EEPROM defaults') 17 | con.reset_parameters() 18 | 19 | if __name__ == '__main__': 20 | import sys 21 | sys.exit(reset()) 22 | 23 | -------------------------------------------------------------------------------- /runner.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | #!/usr/bin/env python 4 | import pylibftdi 5 | 6 | def runner_serial(func): 7 | """ 8 | Decorator for functions that take a serial number as the first argument, 9 | possibly with other arguments to follow 10 | """ 11 | def inner(): 12 | import sys 13 | args = sys.argv 14 | 15 | if len(args)>1: 16 | serial = args[1] 17 | else: 18 | serial = None 19 | 20 | if serial: 21 | func(serial) 22 | return 0 23 | else: 24 | print('Looking for APT controllers') 25 | drv = pylibftdi.Driver() 26 | controllers = drv.list_devices() 27 | 28 | if controllers: 29 | for con in controllers: 30 | print('Found %s %s S/N: %s'%con) 31 | func(con[2]) 32 | print('') 33 | 34 | return 0 35 | else: 36 | print('\tNo APT controllers found. Maybe you need to specify a PID') 37 | return 1 38 | return inner 39 | 40 | -------------------------------------------------------------------------------- /set_velocity_params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Usage: python get_status.py ] 4 | 5 | Gets the status of all APT controllers, or of the one specified 6 | """ 7 | from __future__ import absolute_import 8 | from __future__ import print_function 9 | import pylibftdi 10 | import pyAPT 11 | 12 | def set_vel_params(serial, acc, max_vel): 13 | with pyAPT.MTS50(serial_number=serial) as con: 14 | print('\tSetting new velocity parameters',acc,max_vel) 15 | con.set_velocity_parameters(acc, max_vel) 16 | min_vel, acc, max_vel = con.velocity_parameters() 17 | print('\tNew velocity parameters:') 18 | print('\t\tMin. Velocity: %.2fmm'%(min_vel)) 19 | print('\t\tAcceleration: %.2fmm'%(acc)) 20 | print('\t\tMax. Velocity: %.2fmm'%(max_vel)) 21 | 22 | def main(args): 23 | if len(args)<3: 24 | print(__doc__) 25 | return 1 26 | 27 | acc = float(args[1]) 28 | max_vel = float(args[2]) 29 | 30 | if len(args)>3: 31 | serial = args[3] 32 | else: 33 | serial = None 34 | 35 | if serial: 36 | set_vel_params(serial, acc, max_vel) 37 | return 0 38 | else: 39 | print('Looking for APT controllers') 40 | drv = pylibftdi.Driver() 41 | controllers = drv.list_devices() 42 | 43 | if controllers: 44 | for con in controllers: 45 | print('Found %s %s S/N: %s'%con) 46 | set_vel_params(con[2], acc, max_vel) 47 | 48 | return 0 49 | else: 50 | print('\tNo APT controllers found. Maybe you need to specify a PID') 51 | return 1 52 | 53 | if __name__ == '__main__': 54 | import sys 55 | sys.exit(main(sys.argv)) 56 | 57 | -------------------------------------------------------------------------------- /spiral_scan.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function, division 3 | from math import * 4 | from matplotlib import pyplot as plt 5 | from mpl_toolkits.mplot3d import * 6 | import time 7 | 8 | stepAngle = 0.25 * pi 9 | phi = pi 10 | maxSize = 50 11 | step = 0.2 * maxSize 12 | r = step 13 | z = 0 14 | IN = 0 15 | OUT = 1 16 | rDir = OUT 17 | 18 | xvector = [] 19 | yvector = [] 20 | zvector = [] 21 | 22 | fig = plt.figure() 23 | ax = fig.gca(projection='3d') # to work in 3d 24 | #plt.axes([0, maxSize, 0, maxSize]) 25 | ax.set_xlim3d(0, maxSize) 26 | ax.set_ylim3d(0, maxSize) 27 | ax.set_zlim3d(0, maxSize) 28 | 29 | ax.set_xlabel('X axis') 30 | ax.set_ylabel('Y axis') 31 | ax.set_zlabel('Z axis') 32 | 33 | plt.ion() 34 | plt.show() 35 | 36 | ax.scatter(maxSize / 2, maxSize / 2, z, zdir='z', c= 'red') 37 | plt.draw() 38 | 39 | icolor = 0 40 | epsilon = 0.001 41 | 42 | while (z <= maxSize): 43 | if r > maxSize / 2: 44 | stepAngle = (stepAngle * r) / (r - step) 45 | 46 | # stepAngle = (stepAngle * (r + step)) / r 47 | r -= step 48 | rDir = IN 49 | else: 50 | #stepAngle = (stepAngle * r) / (r + step) 51 | stepAngle = 0.25 * pi 52 | 53 | r = step 54 | rDir = OUT 55 | 56 | x = y = maxSize / 2 57 | xvector.append(x) 58 | yvector.append(y) 59 | zvector.append(z) 60 | if (icolor % 2 == 0): 61 | ax.scatter(x, y, z, zdir='z', c= 'red') 62 | else: 63 | ax.scatter(x, y, z, zdir='z', c= 'blue') 64 | plt.draw() 65 | 66 | while ((r > step or abs(r - step) < epsilon) and (r < maxSize / 2 or abs(r - maxSize / 2) < epsilon)): 67 | while (phi > -pi): 68 | 69 | x = r * cos(phi) + maxSize / 2 70 | y = r * sin(phi) + maxSize / 2 71 | 72 | xvector.append(x) 73 | yvector.append(y) 74 | zvector.append(z) 75 | 76 | if (icolor % 2 == 0): 77 | ax.scatter(x, y, z, zdir='z', c= 'red') 78 | else: 79 | ax.scatter(x, y, z, zdir='z', c= 'blue') 80 | plt.draw() 81 | time.sleep(0.0000001) 82 | 83 | phi -= stepAngle 84 | 85 | print('B', icolor, stepAngle, r) 86 | 87 | 88 | if (rDir == IN): 89 | #stepAngle = (stepAngle * (2 * r + 2 * step)) / (2 * r) 90 | if (abs(r - step) > epsilon): 91 | stepAngle = (stepAngle * r) / (r - step) 92 | 93 | r -= step 94 | else: 95 | #stepAngle = (stepAngle * 2 * r) / (2 * r + 2 * step) 96 | stepAngle = (stepAngle * r) / (r + step) 97 | r += step 98 | 99 | print('A', icolor, stepAngle, r) 100 | 101 | phi = pi 102 | 103 | 104 | if (rDir == IN): 105 | x = y = maxSize / 2 106 | xvector.append(x) 107 | yvector.append(y) 108 | zvector.append(z) 109 | if (icolor % 2 == 0): 110 | ax.scatter(x, y, z, zdir='z', c= 'red') 111 | else: 112 | ax.scatter(x, y, z, zdir='z', c= 'blue') 113 | plt.draw() 114 | 115 | z += step 116 | 117 | icolor += 1 118 | 119 | 120 | plt.hold(True) 121 | 122 | 123 | #plt.plot(xvector, yvector) 124 | #plt.axis([- maxSize / 2, maxSize / 2, - maxSize / 2, maxSize / 2]) 125 | plt.show() 126 | --------------------------------------------------------------------------------