2

I have a GUI that will do stuff, then reboot the panel, then wait for the panel to finish rebooting. The idea is for the user to press "PROGRAM HMI" which will execute the actions(). My big problem is during the reboot, the GUI needs to wait for 10 seconds for the HMI to reboot. During these 10 seconds, the GUI freezes, and I get a "Window not responding" message up top. The code is executing as I can see it in the terminal and it GUI eventually unfreezes. I've read that I'm not supposed to use the time.sleep() here. But how should I write this so it doesn't freeze? I'm using python 3.8.

import requests
import json
import time
import warnings
import subprocess
import xml.etree.ElementTree as ET
from ftplib import FTP
import hashlib
from datetime import datetime
import sys
import tkinter as tk
import time
import threading
from tkinter import *

class main:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("1750+ HMI Setup App")
        self.window.geometry("550x675")
        self.window.resizable(width=False, height=False)
        self.window.configure(bg='#34aeeb')

        self.action_updateIP = False

        self.ipString = "192.168.0.20"    # Should be found by DHCP server.
        self.newIPString = "192.168.0.20" # Default.
        self.url = "https://" + self.ipString + "/"   # TODO: Get IP address based on DHCP of panel, or fixed 192.168.0.1 if it exists?
        self.urlConfig = self.url + "machine_config/"
        self.urlApi = self.url + "rest/api/v1/"
        self.authData = ('admin', 'admin')
        
        #OUTPUT TEXT BOX
        self.outText = Text(self.window, width=50, height=20, wrap=WORD)
        self.outText.grid(row=10, columnspan=3, padx=10)

        self.make_widgets()
        self.window.mainloop()

    def make_widgets(self):
        Button(self.window, text='Select All', font=("Arial", 12), width=10, command=self.select_all).grid(row=7, sticky=W, padx=10, pady=20)

        ButtonShow = tk.Button(text="Program HMI", width=20, font=("Arial", 14), command=self.actions)
        ButtonShow.grid(column=0, row=8, sticky=W, padx=10, pady=10)

    def verboseSleep(self,seconds):
        chars = len(str(seconds))
        s = "{:" + str(chars) + "d}s"
        cntdn = seconds + 1
        for i in range(seconds, 0, -1):
            sys.stdout.write(str("\b" * (chars+1)) + s.format(i))
            cntdn = cntdn - 1    
            msg_seconds = str(cntdn)
            self.outText.insert(tk.END, str(cntdn))
            pos = self.outText.index('end')
            float_pos = float(pos) - 1.0
            self.window.update_idletasks()
            sys.stdout.flush()
            self.window.after(1000)
            self.outText.delete(str(float_pos), "end")
            self.outText.insert(tk.END, "\n")     

        sys.stdout.write(str("\b" * (chars + 1)))   # Remove all evidence of our countdown timer.
        sys.stdout.flush()

    def select_all(self):
        self.action_updateIP = True

    def actions(self):
        ##########################################################    
        ipString = "192.168.0.20"    # Should be found by DHCP server.
        newIPString = "192.168.0.20" # Default.
        self.outText.delete(1.0, END)
        #print (self.action_updateIP)
        if self.action_updateIP == True:
            print("Updating IP address...")
            self.outText.insert(tk.END, "Updating IP address...")  
            self.window.update_idletasks()
            postData = {"bridge":{"enabled":False,"interfaces":["eth0"],"list":[]},"wifi":{"interfaces":[]},"version":0,"dns":{"servers":[],"search":[]},"hostname":"HMI-2133","interfaces":[{"name":"eth0","label":"WAN","mac_address":"00:30:d8:06:21:33","dhcp":False,"configured":True,"readonly":False,"virtual":False,"hidden":False,"actual_netmask":"255.255.255.0","actual_ip_address":ipString,"ip_address":newIPString,"netmask":"255.255.255.0","gateway_ip":"192.168.0.100"},{"name":"lo","mac_address":"00:00:00:00:00:00","dhcp":False,"configured":True,"readonly":True,"virtual":True,"hidden":True,"actual_netmask":"255.0.0.0","actual_ip_address":"127.0.0.1"}]}
            try:
            # We expect this to fail due to the connection being abruptly ended by the panel...
                r = requests.post(url = urlApi + 'network', data = json.dumps(postData), timeout = 10, headers={"content-type": "application/json"}, auth=authData, verify=False)
            except:
                print("Waiting 10s for panel to update.")
                self.outText.insert(tk.END, "Waiting 10s for panel to update" + "\n")  
                self.window.update_idletasks()
                self.verboseSleep(10)

            print("IP Updated.  New IP: ", newIPString)
            msg_newIP = "IP Updated. New IP: " + newIPString + "\n"
            self.outText.insert(tk.END, msg_newIP)  
            self.window.update_idletasks()       
            self.ipString = self.newIPString
        else:
            print (self.action_updateIP)
            print ('Done!' + "\n")
            self.outText.insert(tk.END, "Done! DONE! I'm all DONE!!" +"\n")

main()
exit(0)
nearbyatom
  • 37
  • 4
  • Could you please send the rest of your code? What's ```window```, for instance? – OctaveL Dec 08 '20 at 14:46
  • I updated with the window definition. The entire code is quite long. I hope I've captured enough information. I can update as needed. Basically, it needs to do something, wait for 10 seconds, the do more things. But during the 10 seconds the GUI freezes. – nearbyatom Dec 08 '20 at 15:13

2 Answers2

0

Look into the after method in tkinter. You can basically set a delay for when a task will be executed with this, without freezing the UI

So something like

window = tk.Tk()

...

def do_actions():
   some_function_calls

...

# delay is given in milliseconds, so you need to multiply with 1000 to get in seconds
window.after(10000, do_actions)  

If you wish to create it as a loop instead, you could do the following:

def do_actions():
    some_function_calls
    return window.after(10000, do_actions)

after_loop = do_actions()

Then if you want to cancel the after loop, you can call window.after_cancel(after_loop)

oskros
  • 3,101
  • 2
  • 9
  • 28
  • Noob question here: if I'm reading correctly, your example reads, execute do_actions every 10 seconds. But how do I you get out of that loop then? After 10seconds, it needs to proceed to the next step "...do more stuff" – nearbyatom Dec 08 '20 at 15:08
  • My first example would only execute the code once. But I edited my answer to show you the difference between making a one-off delayed call, and an "after loop" that keeps calling itself every 10 seconds (and how to eventually cancel it) – oskros Dec 08 '20 at 15:12
  • I'm think about this, but after my time.sleep I'm also doing something too, deleting and adding a new line. How would that fit into your example? And what is after_loop? Is this how/where you initially start your do_actions() function? – nearbyatom Dec 08 '20 at 15:32
  • @nearbyatom If you update your code with an example we can run and check, we could fix most of your problems – Delrius Euphoria Dec 08 '20 at 15:49
  • @nearbyatom You should not use time.sleep at all. You should split individual components of your code into separate functions, and delay them appropriately with the `.after()` method. The variable `after_loop` is a reference to the `after` function call that you need to pass in case you wish to cancel your loop again at a later point in time – oskros Dec 08 '20 at 16:16
  • @nearbyatom made an example in another answer, which you might be able to use to understand how `after` works – oskros Dec 10 '20 at 13:26
  • OK, I updated the code to reflect closer to what I have going. Hopefully it's reproduceable. I'm using the .after where I was using the time.sleep. The problem seems to be somewhere in the def actions() function. Tkinter window will stop responding, when the code is updating the IP address. – nearbyatom Dec 11 '20 at 17:25
  • Forgot the link, see example here. Your implementation wont work. https://stackoverflow.com/questions/65231089/python-how-do-i-make-a-fast-reaction-test-with-tkinter/65231501#65231501 – oskros Dec 11 '20 at 20:34
  • @oskros - Thanks for the link. I did as you suggested as per the link. I've updated the code in this thread, but I am having the same issue. I'm pretty sure I'm not using the class correctly. How should I use class here? – nearbyatom Dec 14 '20 at 19:05
  • @nearbyatom the after method needs to take a second argument which is the function you wish to run after the delay. You haven't added that in your code. Try and run the example I posted in the other thread and see if you can debug / understand how the after method could be implemented in your code :) – oskros Dec 15 '20 at 10:16
0

'Not responding' problem can be avoided using Multithreading in python using the thread module.

If you've defined any function, say combine() to be added as command in a button:

btn = Button(root, text="Click Me",command=combine)

Due to which the window is freezing, then edit the above type code as shown below:

import threading
btn = Button(root,text="Click Me", command=threading.Thread(target=combine).start()) 

Have a look for reference: stackoverflow - avoid freezing of tkinter window using one line multithreading code

Prashantzz
  • 301
  • 2
  • 7