I have a question regarding the use of a stop button in Tkinter
.
For an experiment, I have to set up and X/Y stage that works by using two stepper motors. The arduino program works perfectly. The only problem is that when I activate the start function, which drives the stage to various coordinates, it freezes. Now the problem is that it has to run for weeks on end and it needs a stop button for emergencies and stopping the stepper motor in general. The stop button has to do two things: it has to stop the stepper driver motors, and it has to break the tkinter.after
loop. However, due to the freezing, it is impossible to click on the button.
Here is my code:
import tkinter as tk
import serial
ser = serial.Serial('COM5', 115200)
running = False
def quit():
"""Function that closes the serial port and destroys the root of the GUI"""
global root
ser.close()
root.destroy()
def route():
"""Writes coordinates to the arduino, which in return drives the stepper motors"""
if running == True:
# The g line stands for go to!
ser.write(b'g115000\r\n')
root.after(50)
ser.write(b'g225000\r\n')
root.after(30000)
ser.write(b'g1400\r\n')
root.after(50)
ser.write(b'g2500\r\n')
root.after(12000,route())
def zeroing():
"""Zeros the program, this is necessary for the stage to
calibrate it's boundary conditions"""
#zeros the stage so that it is ready to use!
varLabel.set("zeroing, please move away from the stage")
#the z command zeros the motors for boundary business
ser.write(b'z\r\n')
def run_program():
"""Runs the function Route and sets running to True (not a good start/stop system)"""
#starts the program, but only after you zero the stage
global running
running = True
varLabel.set("Program running")
route()
def stop_program():
"""Sets the running flag to False and sends a stop command to the arduino"""
#stops the program immediately
global running
running = False
varLabel.set("Program stopped,please zero before continuing")
#the s byte is a command that stops the stepper motors
ser.write(b's\r\n')
if __name__== "__main__":
root = tk.Tk()
canvas1 = tk.Canvas(root, width=800, height=400)
canvas1.pack()
root.title('XY-stage controller')
#instructions
instructions = tk.Label(root,text='Enter the amount of hours you want your measurements to last in the text box.'
'\n Click on run program to start a measurement session.'
'\n Click on stop incase of an emergency or if it is wanted to stop the program.',
font = "Raleway")
instructions.pack(side='bottom')
# initialize active labels
varLabel = tk.IntVar()
tkLabel = tk.Label(textvariable=varLabel,)
tkLabel.pack(side='top')
# Buttons for initializing a bunch of good functions
zerobutton = tk.IntVar()
tkrunprogram= tk.Button(
root,
text='Zero',
command = zeroing,
height = 4,
fg = "black",
width = 10,
bg = 'gray',
bd = 5,
activebackground = 'green'
)
tkrunprogram.pack(side='top')
runprogbutton = tk.IntVar()
tkrunprogram= tk.Button(
root,
text='Run Program',
command = run_program,
height = 4,
fg = "black",
width = 10,
bg = 'gray',
bd = 5,
activebackground = 'green'
)
tkrunprogram.pack(side='top')
stopbutton = tk.IntVar()
tkstopprog= tk.Button(
root,
text='Stop Program',
command = stop_program,
height = 4,
fg = "black",
width = 10,
bg = 'gray',
bd = 5,
activebackground = 'red'
)
tkstopprog.pack(side='top')
Buttonquit = tk.IntVar()
tkButtonQuit = tk.Button(
root,
text='Quit',
command = quit,
height = 4,
fg = "black",
width = 10,
bg = 'yellow',
bd = 5
)
# initialize an entry box
entry1 = tk.Entry(root)
durbox = canvas1.create_window(400, 200, window=entry1)
tkButtonQuit.pack(side='top')
root.mainloop()
The after commands in the end will introduce pauses of 60 minutes, which would make the program freeze for 60 minutes. Hopefully there is an easy solution to interrupting the function!
Thank you in advance!
CodePudding user response:
You can make use of multithreading. Make all the communication in a separate thread and also make sure you don't update the GUI components in the child thread.
Here is a minimal example:
import serial
import tkinter as tk
from threading import Thread
import time
def start():
global running
stop()
btn.config(text="Stop", command=stop)
running = True
info_label["text"] = "Starting..."
thread = Thread(target=run, daemon=True)
thread.start()
def run():
ser = serial.Serial("COM5", 115200, timeout=2)
while running:
ser.write(b'g115000\r\n')
time.sleep(50)
ser.write(b'g225000\r\n')
time.sleep(30000)
ser.write(b'g1400\r\n')
time.sleep(50)
ser.write(b'g2500\r\n')
ser.write(b's\r\n')
ser.close()
def stop():
global running
running = False
info_label["text"] = "Stopped"
btn.config(text="Start", command=start)
root = tk.Tk()
running = False
info_label = tk.Label(root, text="INFO:")
info_label.pack()
btn = tk.Button(root, text="Start", command=start)
btn.pack()
root.mainloop()
CodePudding user response:
after(x000)
is effectively the same as time.sleep(x)
- it puts the whole app to sleep. As a general rule of thumb, you should never do this in the same thread as the GUI. That doesn't mean you need to use threads, however.
tkinter's after
method lets you schedule commands to run in the future. If the commands you are running are fast such as sending a few bytes down a serial connection, this is really all you need. It is less complex and has less overhead than using threads.
For example, your route
function can probably be written something like this:
def route():
if running == True:
# immediately write this:
ser.write(b'g115000\r\n')
# after 50ms, write this:
root.after(50, ser.write, b'g225000')
# after 30 more seconds, write this
root.after(50 30000, ser.write, b'g1400\r\n')
# and then after 50ms more, write this
root.after(50 30000 50, ser.write, b'g2500\r\n')
# and finally, after 12 seconds, do it all again
root.after(50 30000 50 12000,route)
Once you call this once, you don't need to call it again, and you don't need to call it in a thread. It simply places some work on a queue that gets picked up some time in the future.
Since each call to root.after
returns an id, you can save these ids so that in the case of wanting to stop everything, you can call after_cancel
on each saved id.
Another way is to define a job as a series of delays and then bytes to write. For example:
job = (
(0, b'g115000\r\n'),
(50, b'g225000'),
(30000, b'g1400\r\n'),
(50, b'g2500\r\n'),
)
Then, your route
function can look something like this (untested, but this is pretty close)
def route(job):
global after_id
delay = 0
for (delta, data) in job:
delay = delta
root.after(delay, ser.write, data)
delay = 12000
root.after(delay, route, job)
There are many variations of that theme. For example, you could create a Job
class that implements this logic, or the job
could contain the commands rather than the data. The point being, you can define a data structure that defines work to be done, and then use after
to schedule that work.