Как реализовать кнопку остановки с помощью Tkinter для системы шагового двигателя?

#python #tkinter #pyserial #motordriver

Вопрос:

У меня есть вопрос относительно использования кнопки «Стоп» в Tkinter .

Для эксперимента я должен настроить X/Y-ступень, которая работает с использованием двух шаговых двигателей. Программа arduino работает идеально. Единственная проблема заключается в том, что, когда я активирую функцию запуска, которая приводит сцену в различные координаты, она зависает. Теперь проблема в том, что он должен работать неделями подряд, и ему нужна кнопка остановки для экстренных ситуаций и остановки шагового двигателя в целом. Кнопка «Стоп» должна делать две вещи: она должна останавливать двигатели шагового привода и прерывать tkinter.after цикл. Однако из-за замерзания нажать на кнопку невозможно.

Вот мой код:

 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'g115000rn')
        root.after(50)
        ser.write(b'g225000rn')
        root.after(30000)
        ser.write(b'g1400rn')
        root.after(50)
        ser.write(b'g2500rn')
        
    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'zrn')
    
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'srn')
    

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()
 

Команды «После» в конце концов введут 60-минутные паузы, из — за которых программа остановится на 60 минут. Надеюсь, есть простое решение для прерывания функции!

Заранее благодарю вас!

Ответ №1:

Вы можете использовать многопоточность. Сделайте все коммуникации в отдельном потоке, а также убедитесь, что вы не обновляете компоненты графического интерфейса в дочернем потоке.

Вот минимальный пример:

 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'g115000rn')
        time.sleep(50)
        ser.write(b'g225000rn')
        time.sleep(30000)
        ser.write(b'g1400rn')
        time.sleep(50)
        ser.write(b'g2500rn')
    
    ser.write(b'srn')
    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()
 

Комментарии:

1. Спасибо! Если подход Брайана не сработает, то я обязательно попробую потоковую обработку!

2. Это на самом деле сработало идеально, с некоторыми изменениями, конечно! Большое спасибо!

Ответ №2:

after(x000) по сути, это то же самое, что time.sleep(x) — это переводит все приложение в спящий режим. Как правило, вы никогда не должны делать это в том же потоке, что и графический интерфейс. Однако это не означает, что вам нужно использовать потоки.

after метод tkinter позволяет планировать выполнение команд в будущем. Если вы выполняете быстрые команды, такие как отправка нескольких байтов по последовательному соединению, это действительно все, что вам нужно. Это менее сложно и требует меньших затрат, чем использование потоков.

Например, вашу route функцию, вероятно, можно записать примерно так:

 def route():
    if running == True:
        # immediately write this:
        ser.write(b'g115000rn')

        # after 50ms, write this:
        root.after(50, ser.write, b'g225000')
 
        # after 30 more seconds, write this
        root.after(50 30000, ser.write, b'g1400rn')

        # and then after 50ms more, write this
        root.after(50 30000 50, ser.write, b'g2500rn')

        # and finally, after 12 seconds, do it all again
        root.after(50 30000 50 12000,route)
 

Как только вы позвоните один раз, вам не нужно будет звонить снова, и вам не нужно будет звонить в потоке. Это просто помещает некоторую работу в очередь, которую заберут когда-нибудь в будущем.

Поскольку каждый вызов root.after возвращает идентификатор, вы можете сохранить эти идентификаторы, чтобы в случае необходимости остановить все, вы могли позвонить after_cancel по каждому сохраненному идентификатору.

Другой способ-определить задание как последовательность задержек, а затем байты для записи. Например:

 job = (
    (0, b'g115000rn'),
    (50, b'g225000'),
    (30000, b'g1400rn'),
    (50, b'g2500rn'),
)
 

Тогда ваша route функция может выглядеть примерно так (непроверено, но это довольно близко)

 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)
 

Существует множество вариаций этой темы. Например, вы можете создать Job класс, реализующий эту логику, или job он может содержать команды, а не данные. Дело в том, что вы можете определить структуру данных, которая определяет выполняемую работу, а затем использовать after ее для планирования этой работы.

Комментарии:

1. Большое спасибо! Я попробую 🙂