Page 1 of 1

tkinter memory Leak.

Posted: Fri Dec 07, 2018 11:43 am
by grahamr123
Hi,

I've made a program which uses a PWM Pihat to control servo motors which act as shutters for a laser. The servos are powered by a seperate power supply (5v 2A) using the ADA2327 Hat. It also has two switch inputs that are connected to magnetic doors switches to close the shutter if a door is opened (safety feature) and has a physical go and stop button to start the timed opening of the 'shutter'.

Anyway it all works well except for the GUI that seems to crash after a short while, The actual shutter activation with the buttons still works, but the GUi is unresponsive.

I believe this to be because of a memory leak in tkinter.
I'm not sure how to fix it though as this is one of my first major Python projects. Is there anything obviously wrong here?

Code: Select all

from __future__ import division
import RPi.GPIO as GPIO
import time
from tkinter import *
from tkinter import font
import sys
from tkinter import messagebox

# Import the PCA9685 module.
import Adafruit_PCA9685

# Initialise the PCA9685 using the default address (0x40).
pwm = Adafruit_PCA9685.PCA9685()

# Configure min and max servo pulse lengths
servo_min = 350  # Min pulse length out of 4096
servo_max = 100  # Max pulse length out of 4096


goButton = 21
stopButton = 20
door1 = 16
door2 = 13
laser1 = 4
laser2 = 6
GPIO.setmode(GPIO.BCM)
GPIO.setup(goButton, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(stopButton, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(door1, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(door2, GPIO.IN, pull_up_down=GPIO.PUD_UP)

isrunning = True


# Helper function to make setting a servo pulse width simpler.
def set_servo_pulse(channel, pulse):
    pulse_length = 1000000    # 1,000,000 us per second
    pulse_length //= 60       # 60 Hz
    print('{0}us per period'.format(pulse_length))
    pulse_length //= 4096     # 12 bits of resolution
    print('{0}us per bit'.format(pulse_length))
    pulse *= 1000
    pulse //= pulse_length
    pwm.set_pwm(channel, 0, pulse)


# Set frequency to 60hz, good for servos.
pwm.set_pwm_freq(60)

pwm.set_pwm(laser1, 0, servo_min)
pwm.set_pwm(laser2, 0, servo_min)

root = Tk()
root.attributes("-fullscreen", True)
root.configure(background='gray15')
default_font = font.nametofont("TkDefaultFont") # TkDefaultFont, TkTextFont, TkFixedFont
default_font.configure(size=28)

default_textfont = font.nametofont("TkTextFont") # TkDefaultFont, TkTextFont, TkFixedFont
default_textfont.configure(size=24)
default_1font = font.nametofont("TkMenuFont") # TkDefaultFont, TkTextFont, TkFixedFont
default_1font.configure(size=26)


Ltime = StringVar(root, value='60')
e1 = Entry(root, textvariable=Ltime)
e1.grid(row=0, column=1, padx=20, pady=20)

tkvar = StringVar(root)
timeleft = StringVar(root, value='0')
  
    
    
def stop (*args):
    global trigger
    pwm.set_pwm(laser1, 0, servo_min)
    pwm.set_pwm(laser2, 0, servo_min)
    trigger = False



 
# Dictionary with options
choices = { 'Laser 1','Laser 2'}
tkvar.set('Laser 1') # set the default option
Button(root, text="STOP", bg="Red", command=stop).grid(row = 4, column = 1, pady=(50,20), padx=(250,10))





popupMenu = OptionMenu(root, tkvar, *choices)
Label(root, text="Choose a laser", bg ="Gray15", fg="white").grid(row = 2, column = 0)
Label(root, text="Laser time (Secs)", bg ="Gray15", fg="white").grid(row = 0, column = 0, padx=20, pady=20)
Label(root, textvariable=timeleft, fg="black", bg ="yellow").grid(row = 3, column = 1, pady=20)
Label(root, text="Time Left", width=8, fg="black", bg ="yellow").grid(row = 3, column = 0, pady=80, padx=(150, 1))
popupMenu.grid(row = 2, column =1, padx=(10,240))
 
# on change dropdown value
def change_dropdown(*args):
    print( tkvar.get() )
 
# link function to change dropdown
tkvar.trace('w', change_dropdown)


def on_closing():
    if messagebox.askokcancel("Quit", "Do you want to quit?"):
        pwm.set_pwm(laser1, 0, servo_min)
        pwm.set_pwm(laser2, 0, servo_min)
        root.destroy()

root.protocol("WM_DELETE_WINDOW", on_closing)









count_time = time.clock() #used for updating the UI variables
ui_time = time.clock() #used for updating the UI
Laser_time = time.clock() #used for updating the UI
trigger = False


 
while isrunning:

    if GPIO.input(door1) == GPIO.HIGH and trigger:
        pwm.set_pwm(laser1, 0, servo_min)
        pwm.set_pwm(laser2, 0, servo_min)
        #p.ChangeDutyCycle(6.7)
        trigger = False
        messagebox.showinfo("ERROR!", "Door is opened! Close the Door!")


    if GPIO.input(door2) == GPIO.HIGH and trigger:
        pwm.set_pwm(laser1, 0, servo_min)
        pwm.set_pwm(laser2, 0, servo_min)
        #p.ChangeDutyCycle(6.7)
        trigger = False
        messagebox.showinfo("ERROR!", "Door is opened! Close the Door!")

        

    
    if count_time < time.clock():
        count_time = time.clock() + 1	#once per second
        #cycles.set(cycle_count)	#updates the number of cycles

    if ui_time < time.clock():			
        ui_time = time.clock() + 0.1		#every 0.1 second (reduce this if the interface is too sluggish)
        if trigger:
            timeleft.set(int(Laser_time-time.clock()))
            
        root.update_idletasks()			#update GUI 
        root.update()

        

    input_value = GPIO.input(goButton)
  
    
    if not input_value: #button press checking
         if tkvar.get() == "Laser 1":
           #print("laser 1 selected")
            pwm.set_pwm(laser1, 0, servo_max)
           #p.ChangeDutyCycle(9)
         else:
           #print("laser 2 selected")
            pwm.set_pwm(laser2, 0, servo_max)    # change p. to different label for servo 2
         trigger = True
         Laser_time = time.clock()+float(Ltime.get()) #used for updating the UI

    input_value1 = GPIO.input(stopButton)

    if input_value1:  #button press checking
         if tkvar.get() == "Laser 1":
           #print("laser 1 selected")
           pwm.set_pwm(laser1, 0, servo_min)
           #p.ChangeDutyCycle(9)
         else:
           #print("laser 2 selected")
           pwm.set_pwm(laser2, 0, servo_min)   # change p. to different label for servo 2
         trigger = False
         Laser_time = time.clock()+float(Ltime.get()) #used for updating the UI




     
    if trigger and Laser_time < time.clock():
       pwm.set_pwm(laser1, 0, servo_min)
       pwm.set_pwm(laser2, 0, servo_min)
       #p.ChangeDutyCycle(6.7)
       trigger = False
      
      

       



Re: tkinter memory Leak.

Posted: Fri Dec 07, 2018 12:01 pm
by scotty101
There are two simple rules for a responsive tkinter GUI
1. Avoid infinite loops or functions that will take a long time to execute
2. Update widgets rather than redrawing them.

You've met rule 2 but have broken rule 1.

Your whole main body of code is inside one large while loop and the only time that the gui will be updated is when the root.update_idletask() and root.update functions are allowed to run. You've actively put these inside an if loop that will delay how often the gui can update. NO!

My suggestions for improving your code.
1. Remove the while "isrunning loop"
2. Setup all your GUI widgets and then have a root.mainloop() call.
3. Use the .after method to periodically schedule a function which will read the GPIO inputs and based on their state, update the GUI.
4. If any of your GPIO pins need a more instantaneous response in the GUI, use the GPIO.add_event_callback method to call a function.
5. Add more meaningful variable names to your code. For example what is e1? If it is the entry box for the laser time call it something like entryLaserTime. Same for "tkvar" and "popupMenu" What are the for?