Getting Started

Let’s suppose that one has a Gemini GV-6 linear servo motor drive supporting the ASCII protocol connected to the RS232 port '/dev/ttyS1' (would look more like 'COM1' on Windows).

Accessing The Drive Directly

The drive is accessed directly by using drivers in the GeminiMotorDrive.drivers module. Right now, there is only one driver there, which is GeminiMotorDrive.drivers.ASCII_RS232 for the ASCII protocol over an RS232 connection (sometimes known as a serial connection).

Connecting to The Drive

The first step is to connect to the drive using GeminiMotorDrive.drivers.ASCII_RS232. There are two ways to access it. One is to access it directly as

>>> import GeminiMotorDrive
>>> from GeminiMotorDrive.drivers import ASCII_RS232
>>> dr = ASCII_RS232('/dev/ttyS1')

and another is to use the GeminiMotorDrive.get_driver() function in the main module.

>>> import GeminiMotorDrive
>>> dr = GeminiMotorDrive.get_driver(driver='ASCII_RS232')

Sending Commands to The Drive

Using GeminiMotorDrive.drivers.ASCII_RS232.send_command() and GeminiMotorDrive.drivers.ASCII_RS232.send_commands(), one can send commands to the drive using the drive’s ASCII language. For example, the drive would be energized (motor turned on) by doing

>>> dr.send_command('DRIVE1', immediate=False, timeout=1.0)
['DRIVE1', 'DRIVE1\r\r\n', 'DRIVE1', None, []]

The command was sent to the drive’s command buffer to be executed when the drive is finished moving (immediate=False) with a timeout of one second to get a response from the drive. The returned list gives the command sent to the drive (after sanitizing it a bit), the full response from the drive, the echoed command (dr was initialized so that the drive would echo back commands to make it easier to send commands correctly), whether there was an error (None means there was not), and the drive’s repsonse separated into its individual lines. From the return values, there was no error meaning that the drive was successfully energized.

If the same command had been sent but with a typo,

>>> dr.send_command('DRIV1', immediate=False, timeout=1.0)
['DRIV1', 'DRIV1\r*UNDEFINED_LABEL\r\r\n', 'DRIV1',
 'UNDEFINED_LABEL', []]

The command produced an error since it was recognized. Specifically, the drive reported and 'UNDEFINED_LABEL' error.

An easier way to check whether there was an error or not is to use the GeminiMotorDrive.drivers.ASCII_RS232.command_error() function, which returns 'True' if a command response indicates an error. Doing this on the previous incorrect command,

>>> resp = dr.send_command('DRIV1', immediate=False, timeout=1.0)
>>> dr.command_error(resp)
True

A list of multiple commands can be sent using GeminiMotorDrive.drivers.ASCII_RS232.send_commands(). Let’s energize the drive, move the motor 10000 motor units, make it oscillate 10000 motor units 4 times, and then de-energize the drive.

>>> dr.send_commands(['DRIVE1', 'D-10000', 'GO']
...                  + ['D-10000','GO','D10000','GO']*4
...                  + [ 'DRIVE0'])
[['DRIVE1', 'DRIVE1\r', 'DRIVE1', None, []],
 ['D-10000', 'D-10000\r', 'D-10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D-10000', 'D-10000\r', 'D-10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D10000', 'D10000\r', 'D10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D-10000', 'D-10000\r', 'D-10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D10000', 'D10000\r', 'D10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D-10000', 'D-10000\r', 'D-10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D10000', 'D10000\r', 'D10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D-10000', 'D-10000\r', 'D-10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['D10000', 'D10000\r', 'D10000', None, []],
 ['GO', 'GO\r', 'GO', None, []],
 ['DRIVE0', 'DRIVE0\r', 'DRIVE0', None, []]]

The returned list is just a list of the outputs for the individual commands.

Using The Convenience Class GeminiG6

A major downside to using GeminiMotorDrive.drivers.ASCII_RS232 like this is that one must give the drive the exact commands for each action in the drive’s language and then parse the output. This can be inconvenient at times and is also error prone.

The GeminiMotorDrive.GeminiG6 class wraps around the driver to provide a more convenient interface for many commands.

Wrapping The Driver

An instance is made by passing a suitable driver (dr in this case)

>>> gem = GeminiMotorDrive.GeminiG6(dr)

The driver can still be accessed directly with dr or as gem.driver (GeminiMotorDrive.GeminiG6.driver).

Using Properties to Control And Query

Now, energizing the drive is as simple as setting the GeminiMotorDrive.GeminiG6.energized property.

>>> gem.energized = True

And reading the property will return whether the drive is energized or not.

>>> gem.energized
True

Using Programs And Profiles

Many times, it is undesirable to control the motor’s motion one command at a time. For repeated patterns of motion, doing it each time is error prone and can suffer from transmission errors. In addition, the transmission time for each command can slow down operation. A useful feature of the Gemini G6 series is that they can store sequences of commands as programs and sequences of movement as motion profiles in their internal memory. The commands can then be run locally one after another without having to wait for a new command to be sent.

Programs and profiles are a bit different though. Programs allow nearly all commands and operate as if the commands had been sent, interpretting each command in the program one after another. Profiles allow a much more limited set commands that essentially only describe movement patterns. But profiles are not interpreted each time they are run. Instead, the drive compiles them to an internal motion description when they are received. Profiles run faster since there is no interpretting at each step. Moreover, the drive can receive and run commands while a profile is running.

Programs and profiles are numbered starting from one.

Warning

Gemini G6 drives have a limited amount of memory. This means that they can not necessarily store the full numerical number of programs and profiles indicated in the manual. Larger programs and profiles consume more memory.

Let’s take the earlier example motion of moving the motor 10000 motor units and then making it oscillate 10000 motor units 4 times. But this time, we will make the motor pause for one second before it starts to oscillate. For the program version, we will make it energize the drive at the beginning and de-energize the drive at the end (this cannot be done in a profile). The program version of this looks like

>>> pgm = ['DRIVE1', 'D-10000', 'GO', 'WAIT(AS.1=b0)', 'T1']
...       + ['D-10000','GO','D10000','GO']*4
...       + [ 'DRIVE0']

and the profile version looks like

>>> pfl = ['D-10000', 'GOBUF', 'GOWHEN(T=1000)']
...       + ['D-10000','GOBUF','D10000','GOBUF']*4

Programs and profiles are set using GeminiMotorDrive.GeminiG6.set_program_profile(). To set program 1,

>>> gem.set_program_profile(1, pgm, program_or_profile='program')
True

We can then read the program back with GeminiMotorDrive.GeminiG6.get_program()

>>> gem.get_program(1)
['Drive1',
 'D-10000',
 'GO1',
 'WAIT(AS.1=b0)',
 'T1',
 'D-10000',
 'GO1',
 'D10000',
 'GO1',
 'D-10000',
 'GO1',
 'D10000',
 'GO1',
 'D-10000',
 'GO1',
 'D10000',
 'GO1',
 'D-10000',
 'GO1',
 'D10000',
 'GO1',
 'DRIVE0']

This looks a bit different than the program that was sent. A lot of commands have default values for arguments or can interpret many strings to be the same. When a program is read from the drive, all arguments are given, even if they have the default value. And they can be rendered into strings differently than a human would.

Setting profile 1 is similar

>>> gem.set_program_profile(1, pfl, program_or_profile='profile')
True

except that we cannot read a profile from the drive. This is something that Gemini drives does not support.

Programs and profiles are run the same way using GeminiMotorDrive.GeminiG6.run_program_profile(). Running program 1 is done by

>>> gem.run_program_profile(1, program_or_profile='program',
...                         timeout=20.0)

and running profile 1 is done by

>>> gem.run_program_profile(1, program_or_profile='profile',
...                         timeout=2.0)

Which are nearly identical. It is just a matter of specifying whether to run a 'program' or a 'profile'. The timeout argument does deserve special attention. It is used to specify how many seconds to wait to collect the output from the drive. When running a program, each command that is run is returned as it is run, meaning that the timeout needs to be longer than the program takes to run if one wants to collect all of its output. When running a profile, just the command to run a profile is returned. Thus, for a profile, the timeout can be relatively short.

GeminiMotorDrive.GeminiG6.run_program_profile() returns the output that it captures within the window of the timeout.

Generating Programs and Profiles

Programs and profiles give a large level of convenience, but they still have to be written without introducing errors. For simple motion composed of sequences of parabolic motion (parobolic position vs. time graphs) coming to a stop at the end of each step with optional pauses in beteen, a sort of higher level language description of the motion is provided. It can be compiled to the program and profile language of the drive using the module GeminiMotorDrive.compilers.move_sequence.

Defining Movement Sequences (Syntax)

A move sequence is formatted as an iterable of dict. Each element of the iterable defines a block of movement steps. The blocks are done one after another.

Each block defines the movements to perform, the time to wait after each movement, and the number of to do the motion of the block. These are defined using the 'moves', 'wait_times', and 'iterations' keys respectively.

  • 'moves' is an iterable of the movements to do, which are themselves dict.
  • 'wait_times' is an iterable of the number of seconds to wait after each movement. Values of 0 mean no wait.
  • 'iterations' is a positive integer specifying how many times to do the block. Values > 1 imply doing a loop.

A block consisting of movements M1 and M2, with waits of 1.0 and 0.3 seconds after the first and second movements, and is done 20 times would look like

{'iterations': 20, 'wait_times': [1.0, 0.3], 'moves': [M1, M2]}

Movements need to specify a distance to move, a maximum velocity that will the motor will attempt to reach, the acceleration magnitude to use when speeding up, and the accleration magnitude (deceleration) to use when slowing down to a stop. To match the program language of the drive, these are given as the keys 'D', 'V', 'A', and 'AD' respecively. They each take single numerical values, which can be floating point.

  • 'D' The distance to move including sign.
  • 'V' Maximum velocity for the motor to attempt to reach.
  • 'A' Acceleration magnitude to use when speeding up.
  • 'AD' Acceleration magnitude to use when slowing down to a stop.

Notice that the units have not been defined yet. At this stage, they do not have to be. One could use the motor’s unit of distance, meters, centimeters, etc. And one could be using the motor’s units of velocity, m/s, cm/s, etc.

A movement going a distance of 120 backwards (negative) with a maximum velocity of 3.3, an acceleration of 0.1, and a deceleration of 30 would be

{'D': -120, 'V': 3.3, 'A': 0.1, 'AD': 30}

Now, let’s define a movement sequence consisting of one block where there are two motions inside it with a pause of 1 second in between movements.

>>> cycles = [{'iterations':1, 'wait_times':[1, 0],
...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100},
...           {'A':90, 'AD':0, 'D':-1000, 'V':100}]}]

And another longer movement sequence consisting of a single movement with a 1 second paus, then oscillating 100 times, and then moving back to the starting position.

>>> long_cycles = [{'iterations':1, 'wait_times':[1],
...                'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100}]},
...                {'iterations':100, 'wait_times':[0, 0],
...                'moves':[{'A':50, 'AD':40, 'D':-1000, 'V':30},
...                {'A':50, 'AD':40, 'D':1000, 'V':30}]},
...                {'iterations':1, 'wait_times':[0],
...                'moves':[{'A':100, 'AD':0, 'D':1000, 'V':100}]}]

Units

The units in the motion sequence are thus far undefined. The Gemini drive has its own particular set of units it uses, much like many stepper and servo motor drives.

If the sequence was written using the motor’s units, then the sequence can be compiled and the motion will be performed truthfully. But if say we are using units of meters, m/s, and m/s**2; this will not be the case and we will have to do a unit conversion first.

The one thing that is fixed in the units is the unit of time, which is fixed to seconds.

Now, the motor’s units are defined by two variables, called DMEPIT and ERES. They are the electrical pitch and encoder resolution of the motor respectively. For Gemini drives, the conversions between motor units and MKS units (distance in meters, velocity in m/s, and acceleration in m/s**2) is

  • D_motor = 1e3 * D * ERES / DMEPIT
  • V_motor = 1e3 * V / DMEPIT
  • A_motor = 1e3 * A / DMEPIT

The module GeminiMotorDrive.utilities provides a class for converting units called GeminiMotorDrive.utilities.UnitConverter. If dmepit and eres are the DMEPIT and ERES values for the motor, a unit converter for MKS can be created by

>>> from GeminiMotorDrive.utilities import UnitConverter
>>> uc = UnitConverter(dmepit=dmepit, eres=eres,
...                    unit_in_meters=1.0)

The unit_in_meters parameter is used to specify the unit of distance (remember, the unit of time is fixed as seconds) in meters. So, if one wanted to do centimeters, one would use unit_in_meters=0.01. If one wanted to do inches, one would use unit_in_meters=0.0254.

Now, uc (GeminiMotorDrive.utilities.UnitConverter) is not really meant to be used by the user directly. It is instead an object passed as an argument when compiling the move sequence or calculating how long it will take to complete. However, the move squence’s units can be converted directly using GeminiMotorDrive.compilers.move_sequence.convert_sequence_to_motor_units() in the module GeminiMotorDrive.compilers.move_sequence.

>>> from GeminiMotorDrive.compilers.move_sequence import convert_sequence_to_motor_units
>>> convert_sequence_to_motor_units(cycles, unit_converter=uc)

Compiling the Program or Profile

Move sequences are compiled into programs and profiles using the module GeminiMotorDrive.compilers.move_sequence. Specifically, the function GeminiMotorDrive.compilers.move_sequence.compile_sequence() is used. Import it

>>> from GeminiMotorDrive.compilers.move_sequence import compile_sequence

If we want to compile the move sequence ms to a program using uc, one would do

>>> compile_sequence(ms, program_or_profile='program',
...                  unit_converter=uc)

or to compile it to a profile

>>> compile_sequence(ms, program_or_profile='profile',
...                  unit_converter=uc)

By default, the function assumes that the move sequence is given in motor units.

Now, lets compile cycles into a program and then a profile assuming that it is in motor units. First, a program

>>> compile_sequence(cycles, program_or_profile='program')
['A100',
 'AD0',
 'V100',
 'D-1000',
 'GO1',
 'WAIT(AS.1=b0)',
 'T1',
 'A90',
 'GO1',
 'WAIT(AS.1=b0)']

and now a profile

>>> compile_sequence(cycles, program_or_profile='profile')
['A100',
 'AD100',
 'V100',
 'D-1000',
 'VF0',
 'GOBUF1',
 'GOWHEN(T=1000)',
 'A90',
 'AD90',
 'VF0',
 'GOBUF1']

Compiling the longer program, which had oscillation, into a program results in

>>> compile_sequence(long_cycles, program_or_profile='program')
['A100',
 'AD0',
 'V100',
 'D-1000',
 'GO1',
 'WAIT(AS.1=b0)',
 'T1',
 'L100',
 'A50',
 'AD40',
 'V30',
 'D-1000',
 'GO1',
 'WAIT(AS.1=b0)',
 'D~',
 'GO1',
 'WAIT(AS.1=b0)',
 'LN',
 'A100',
 'AD0',
 'V100',
 'GO1',
 'WAIT(AS.1=b0)']

The programs and profiles can be sent to the drive and run.

Movement Completion Time

Sometimes it is useful to know how long a movement sequence is going to take. This is done using the function GeminiMotorDrive.compilers.move_sequence.get_sequence_time() in the module GeminiMotorDrive.compilers.move_sequence. It can either be given a unit convert uc directory or the encoder resolution (ERES stored in eres). It then returns the time the move sequence takes in seconds to perform.

>>> from GeminiMotorDrive.compilers.move_sequence import get_sequence_time
>>> get_sequence_time(cycles, unit_converter=uc)

or using ERES

>>> from GeminiMotorDrive.compilers.move_sequence import get_sequence_time
>>> get_sequence_time(cycles, eres=eres)

Due to drive latency and motor latencies, it usually takes a bit longer for the motion sequence to complete when it is run by the drive. This is especially true for programs where each command in the program has to be interpreted.