Python, pyserial program for communicating with Zaber TLSR300B

648 views Asked by At

I'm still new here so I apologize if I make any mistakes or if my question is not specific enough, please correct me! I'm working on a program for controlling two Zaber TLSR300B linear motion tracks in a laser lab via a serial connection. I have no problems communicating with them using pyserial, I've been able to write and read data no problem. My questions is more about how to structure my program to achieve the desired functionality (I have very little formal programming training).

What I would like the program to do is provide a number of methods which allow the user to send commands to the tracks which then return what the track responds. However, I do not want the program to hang while checking for responses, so I can't just write methods with write commands followed by a read command. For some commands, the tracks respond right away (return ID, return current position, etc.) but for others the tracks respond once the requested action has been performed (move to location, move home, etc.). For example, if a move_absolute command is sent the track will move to the desired position and then send a reply with its new position. Also, the tracks can be moved manually with a physical knob, which causes them to continuously send their current position as they move (this is why I need to continuously read the serial data).

I've attached code below where I implement a thread to read data from the serial port when there is data to read and put it in a queue. Then another thread takes items from the queue and handles them, modifying the appropriate values of the ZaberTLSR300B objects which store the current attributes of each track. The methods at the bottom are some of the methods I'd like the user to be able to call. They simply write commands to the serial port, and the responses are then picked up and handled by the continuously running read thread.

The main problem I run into is that those methods will have no knowledge of what the track responds so they can't return the reply, and I can't think of a way to fix this. So I can't write something like:

newPosition = TrackManager.move_absolute(track 1, 5000)

or

currentPosition = TrackManager.return_current_position(track 2)

which is in the end what I want the user to be able to do (also since I'd like to implement some sort of GUI on top of this in the future).

Anyways, am I going about this in the right way? Or is there a cleaner way to implement this sort of behavior? I am willing to completely rewrite everything if need be!

Thanks, and let me know if anything is unclear.

Zaber TLSR300B Manual (if needed): http://www.zaber.com/wiki/Manuals/T-LSR

CODE:

TRACK MANAGER CLASS

import serial
import threading
import struct
import time
from collections import deque
from zaberdevices import ZaberTLSR300B

class TrackManager:

def __init__(self):

    self.serial = serial.Serial("COM5",9600,8,'N',timeout=None)

    self.track1 = ZaberTLSR300B(1, self.serial)
    self.track2 = ZaberTLSR300B(2, self.serial)

    self.trackList = [self.track1, self.track2]

    self.serialQueue = deque()

    self.runThread1 = True
    self.thread1 = threading.Thread(target=self.workerThread1)
    self.thread1.start()

    self.runThread2 = True
    self.thread2 = threading.Thread(target=self.workerThread2)
    self.thread2.start()

def workerThread1(self):
    while self.runThread1 == True:
        while self.serial.inWaiting() != 0:
            bytes = self.serial.read(6)
            self.serialQueue.append(struct.unpack('<BBl', bytes))

def workerThread2(self):
    while self.runThread2 == True:
        try:
            reply = self.serialQueue.popleft()
            for track in self.trackList:
                if track.trackNumber == reply[0]:
                    self.handleReply(track, reply)
        except:
            continue

def handleReply(self, track, reply):
    if reply[1] == 10:
        track.update_position(reply[2])
    elif reply[1] == 16:
        track.storedPositions[address] = track.position
    elif reply[1] == 20:
        track.update_position(reply[2])
    elif reply[1] == 21:
        track.update_position(reply[2])
    elif reply[1] == 60:
        track.update_position(reply[2])

def move_absolute(self, trackNumber, position):
    packet = struct.pack("<BBl", trackNumber, 20, position)
    self.serial.write(packet)

def move_relative(self, trackNumber, distance):
    packet = struct.pack("<BBl", trackNumber, 21, distance)
    self.serial.write(packet)

def return_current_position(self, trackNumber):
    packet = struct.pack("<BBl", trackNumber, 60, 0)
    self.serial.write(packet)

def return_stored_position(self, trackNumber, address):
    packet = struct.pack("<BBl", trackNumber, 17, address)
    self.serial.write(packet)

def store_current_position(self, trackNumber, address):
    packet = struct.pack("<BBl", trackNumber, 16, address)
    self.serial.write(packet)

zaberdevices.py ZaberTLSR300B class

class ZaberTLSR300B:

def __init__(self, trackNumber, serial):
    self.trackNumber = trackNumber
    self.serial = serial
    self.position = None
    self.storedPositions = []

def update_position(self, position):
    self.position = position
2

There are 2 answers

0
ZaberCS On BEST ANSWER

There is an option on the controllers to disable all of the replies that don't immediately follow a command. You can do this by enabling bits 0 and 5 of the Device Mode setting. These bits correspond to 'disable auto-replies' and 'disable manual move tracking'. Bit 11 is enabled by default, so the combined value to enable these bits is 2081.

My recommendation would be to disable these extra responses so that you can then rely on the predictable and reliable command->response model. For example to move a device you would send the move command, but never look for a response from that command. To check whether the movement has completed, you could either use the Return Current Position command and read the position response, or use the Return Status (Cmd_54) command to check whether the device is busy (i.e. moving) or idle.

1
Rouse02 On

So, the way I went about doing this when a multithreaded application needed to share the same data was implementing my own "locker". Basically your threads would call this ChekWrite method, it would return true or false, if true, it would set the lock to true, then use the shared resource by calling another method inside of it, after complete, it would set the lock to false. If not, it would wait some time, then try again. The check write would be its own class. You can look online for some Multithreading lock examples and you should be golden. Also, this is all based on my interpretation of your code and description above... I like pictures.

Sample Multi App Custom Lock

Custom Locks Threading python /n/r

EDIT My fault. When your ListenerThread obtains data and needs to write it to the response stack, it will need to ask permission. So, it will call a method, CheckWrite, which is static, This method will take in the data as a parameter and return a bit or boolean. Inside this function, it will check to see if it is in use, we will call it Locked. If it is locked, it will return false, and your listener will wait some time and then try again. If it is unlocked. It will set it to locked, then will proceed in writing to the response stack. Once it is finished, it will unlock the method. On your response stack, you will have to implement the same functionality. It slipped my mind when i created the graphic. Your main program, or whatever wants to read the data, will need to ask permission to read from, lock the Locker value and proceed to read the data and clear it. Also, you can get rid of the stack altogether and have a method inside that write method that goes ahead and conducts logic on the data. I would separate it out, but to each their own.