Search code examples
pythonmultithreadingcontrollerqueuexbox

Python threaded class calls another threaded class (queue help)


I am trying to control a 3-axis printer using an x-box controller. To get inputs from the x-box I have borrowed code from martinohanlon https://github.com/martinohanlon/XboxController/blob/master/XboxController.py I have also created code that reads a text file line by line (G-code) to move the printer.

I would like to be able to use the X-Box controller to select a G-code file and run it, then as the printer is running continue to listen for a cancel button just in case the print goes wrong. The controller is a threaded class, and my readGcode is a threaded class.

The problem I'm having is that when I use the controller to start the readGcode class I cant communicate with the controller until that thread finished.

My temporary solution is to use the controller to select a file then pass that files path to the readGcode class. In the readGcode class it keeps trying to open a file using a try block and fails until the filepath is acceptable. Then it changes a bool which makes it skip further reading until its done.

Code:

import V2_Controller as Controller
import V2_ReadFile as Read
import time
import sys
# file daialogue
import tkinter as tk
from tkinter import filedialog

# when X is selected on the x-box controller 
def X(xValue):
    if not bool(xValue):
        try:
            f=selectfile()
            rf.setfilename(f)
        except:
            print("failed to select file")

def selectfile():
    try:
        root = tk.Tk()  # opens tkinter
        root.withdraw()  # closes the tkinter window
        return filedialog.askopenfilename()
    except Exception:
        print("no file")

# setup xbox controller
xboxCont = Controller.XboxController(controlCallBack, deadzone=30, 
scale=100, invertYAxis=True)
# init the readfile class
rf = Read.Readfile()

# set the custom function for pressing X
xboxCont.setupControlCallback(xboxCont.XboxControls.X, X)

try:
    # start the controller and readfile threads
    xboxCont.start()
    rf.start()

    xboxCont.join()
    rf.join()

    while True:
        time.sleep(1)

# Ctrl C
except KeyboardInterrupt:
    print("User cancelled")

# error
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

finally:
    # stop the controller
    xboxCont.stop()
    rf.stop()

V2_Readfile

# Main class for reading the script
class Readfile(threading.Thread):

    # supports all variables needed to read a script
    class readfile:
        fileselect = True
        linecount = 0
        currentline = 0
        commands = []

    # setup readfile class
    def __init__(self):
        # setup threading
        threading.Thread.__init__(self)
        # persist values
        self.running = False
        self.reading = False

    def setfilename(self,filename):
        self.filename = filename

    # called by the thread
    def run(self):
        self._start()

    # start reading
    def _start(self):
        self.running = True
        while self.running:
            time.sleep(1)
            if not self.reading:
                try:
                    self.startread()
                except:
                    pass

    def startread(self):
        try:
            with open(self.filename, "r") as f:  # read a local file
                f1 = f.readlines()
                # run through each line and extract the command from each line
                linecount = 0
                line = []
                for x in f1:
                    # read each line into an array
                    line.append(x.split(";")[0])
                    linecount += 1

                # Store the variables for later use
                self.readfile.linecount = linecount
                self.readfile.commands = line
                self.reading = True
        except Exception:
            pass

        i = 0
        while i < self.readfile.linecount and self.reading:
            self.readfile.currentline = i + 1
            self.readline(i)
            i += 1

        # the following stops the code from reading again
        self.reading = False
        self.filename = ""


    def readline(self,line):
        Sort.sortline(self.readfile.commands[line])

    # stops the controller
    def stop(self):
        self.running = False

Solution

  • You could use a syncronization primitive like threading.Event.

    To do so, you need to modify your Readfile class like this:

    from threading import Event
    
    
    class Readfile(threading.Thread):
    
        # setup readfile class
        def __init__(self):
            # setup threading
            threading.Thread.__init__(self)
            # persist values
            self.running = False
            self.reading = False
            self.reading_file = Event()  # Initialize your event here it here
    
        def setfilename(self,filename):
            self.filename = filename
            self.reading_file.set()  # This awakens the reader
    
        def _start(self):
            self.running = True
            self.reading_file.wait()  # Thread will be stopped until readfilename is called
            self.startread()
    

    Another syncronization primitive worth exploring is queue.Queue. It could be useful if you want to process more than one filename.

    The pattern you describe in your question is called Busy Waiting, and should be avoided when possible.