I am writing a small application based on tkinter in order to read serial data from my arduino.
The arduino, when it receives a serial text (rf), it will begin sending data to the pc.
Below is the suspicious code:
def readSerial():
ser_bytes = ser.readline()
ser_bytes = ser_bytes.decode("utf-8")
text.insert("end", ser_bytes)
after_id=root.after(100,readSerial)
#root.after(100,readSerial)
def measure_all():
global stop_
stop_ = False
ser.write("rf".encode()) #Send string 'rf to arduino', which means Measure all Sensors
readSerial() #Start Reading data
Now this does not work. The program freezes, and no info is revealed on the terminal.
When i change the line after_id=root.after(100,readSerial) to root.after(100,readSerial) then the program works, but only when i receive serial input.
For example, if there is a 5 second delay to when arduino sends serial, then the program will freeze, until it receives the data. More specificallly, if the program is minimized, and i select to view it as normal, it will not respond unless it receives input from arduino (which will display normally).
So even now, it still does not work properly.
But also have in mind, that i need to have the after_id line, so that i can have a handle, so that i can terminate the readSerial() function (for example when the user presses the 'stop measurement' button).
Can someone understand what is going on, and how i can have the after_id behaviour (so i can stop the continuous function later), while having the program behaving normal, without crashing or stuck until it receives data?
EDIT: This is the modified code after user's acw1668 suggestions. This does not work. I see nothing on the text frame of tkinter.
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports #for a list of all the COM ports
from tkinter import scrolledtext
import threading
import time
from queue import SimpleQueue
#to be used on our canvas
HEIGHT = 800
WIDTH = 800
#hardcoded baud rate
baudRate = 9600
# this is the global variable that will hold the serial object value
ser = None #initial value. will change at 'on_select()'
after_id = None
#this is the global variable that will hold the value from the dropdown for the sensor select
dropdown_value = None
# create the queue for holding serial data
queue = SimpleQueue()
# flag use to start/stop thread tasks
stop_flag = None
# --- functions ---
#the following two functtions are for the seria port selection, on frame 1
#this function populates the combobox on frame1, with all the serial ports of the system
def serial_ports():
return serial.tools.list_ports.comports()
#when the user selects one serial port from the combobox, this function will execute
def on_select(event=None):
global ser
COMPort = cb.get()
string_separator = "-"
COMPort = COMPort.split(string_separator, 1)[0] #remove everything after '-' character
COMPort = COMPort[:-1] #remove last character of the string (which is a space)
ser = serial.Serial(port = COMPort, baudrate=9600, timeout=0.1)
#readSerial() #start reading shit. DELETE. later to be placed in a button
# get selection from event
#print("event.widget:", event.widget.get())
# or get selection directly from combobox
#print("comboboxes: ", cb.get())
#ser = Serial(serialPort , baudRate, timeout=0, writeTimeout=0) #ensure non-blocking
def readSerial(queue):
global stop_flag
if stop_flag:
print("Reading task is already running")
else:
print("started")
stop_flag = threading.Event()
while not stop_flag.is_set():
if ser.in_waiting:
try:
ser_bytes = ser.readline()
data = ser_bytes.decode("utf-8")
queue.put(data)
except UnicodeExceptionError:
print("Unicode Error")
else:
time.sleep(0.1)
print("stopped")
stop_flag = None
# function to monitor whether data is in the queue
# if there is data, get it and insert into the text box
def data_monitor(queue):
if not queue.empty():
text.insert("end", f"{queue.get()}\n")
if vsb.get()[1]==1.0: #if the scrollbar is down to the bottom, then autoscroll
text.see("end")
root.after(100, data_monitor, queue)
# this function is triggered, when a value is selected from the dropdown
def dropdown_selection(*args):
global dropdown_value
dropdown_value = clicked.get()
button_single['state'] = 'normal' #when a selection from the dropdown happens, change the state of the 'Measure This Sensor' button to normal
# this function is triggered, when button 'Measure all Sensors' is pressed, on frame 2
def measure_all():
button_stop['state']='normal' #make the 'Stop Measurement' button accessible
ser.write("rf".encode()) #Send string 'rf to arduino', which means Measure all Sensors
sleep(0.05) # 50 milliseconds
threading.Thread(target=readSerial, args=(queue,)).start()
# this function is triggered, when button 'Measure this Sensor' is pressed, on frame 2
def measure_single():
global stop_
stop_=False
button_stop['state']='normal'
ser.write(dropdown_value.encode()) #Send string 'rf to arduino', which means Measure all Sensors!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
readSerial()
# this function is triggered, when button 'STOP measurement(s)' is pressed, on frame 2
def stop_measurement():
button_stop['state']='disabled'
ser.write("c".encode())
if stop_flag:
stop_flag.set()
else:
print("Reading task is not running")
# --- functions ---
# --- main ---
root = tk.Tk() #here we create our tkinter window
root.title("Sensor Interface")
#we use canvas as a placeholder, to get our initial screen size (we have defined HEIGHT and WIDTH)
canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()
#we use frames to organize all the widgets in the screen
'''
relheight, relwidth − Height and width as a float between 0.0 and 1.0, as a fraction of the height and width of the parent widget.
relx, rely − Horizontal and vertical offset as a float between 0.0 and 1.0, as a fraction of the height and width of the parent widget.
'''
# --- frame 1 ---
frame1 = tk.Frame(root)
frame1.place(relx=0, rely=0.05, relheight=0.03, relwidth=1, anchor='nw') #we use relheight and relwidth to fill whatever the parent is - in this case- root
label0 = tk.Label(frame1, text="Select the COM port that the device is plugged in: ")
label0.config(font=("TkDefaultFont", 8))
label0.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
cb = ttk.Combobox(frame1, values=serial_ports())
cb.place(relx=0.5, rely=0.5, anchor='center')
# assign function to combobox, that will run when an item is selected from the dropdown
cb.bind('<<ComboboxSelected>>', on_select)
# --- frame 1 ---
# --- frame 2 ---
frame2 = tk.Frame(root, bd=5) #REMOVED THIS bg='#80c1ff' (i used it to see the borders of the frame)
frame2.place(relx=0, rely=0.1, relheight=0.07, relwidth=1, anchor='nw')
#Button for 'Measure All Sensors'
#it will be enabled initially
button_all = tk.Button(frame2, text="Measure all Sensors", bg='#80c1ff', fg='red', state='normal', command=measure_all) #bg='gray'
button_all.place(relx=0.2, rely=0.5, anchor='center')
#label
label1 = tk.Label(frame2, text="OR, select a single sensor to measure: ")
label1.config(font=("TkDefaultFont", 9))
label1.place(relx = 0.32, rely=0.3, relwidth=0.3, relheight=0.4)
#dropdown
#OPTIONS = [0,1,2,3,4,5,6,7]
OPTIONS = list(range(8)) #[0,1,2,3,4,5,6,7]
clicked = tk.StringVar(master=frame2) # Always pass the `master` keyword argument, in order to run the function when we select from the dropdown
clicked.set(OPTIONS[0]) # default value
clicked.trace("w", dropdown_selection) #When a value from the dropdown is selected, function dropdown_selection() is executed
drop = tk.OptionMenu(frame2, clicked, *OPTIONS)
drop.place(relx = 0.65, rely=0.25, relwidth=0.08, relheight=0.6)
#Button for 'Measure Single Sensor'
#this will be disabled initially, and will be enabled when an item from the dropdown is selected
button_single = tk.Button(frame2, text="Measure this Sensor", bg='#80c1ff', fg='red', state='disabled', command=measure_single) #bg='gray'
button_single.place(relx = 0.85, rely=0.5, anchor='center')
# --- frame 2 ---
# --- frame 3 ---
frame3 = tk.Frame(root, bd=5) #REMOVED THIS bg='#80c1ff' (i used it to see the borders of the frame)
frame3.place(relx=0, rely=0.2, relheight=0.07, relwidth=1, anchor='nw')
#Button for 'STOP Measurement(s)'
#this will be disabled initially, and will be enabled only when a measurement is ongoing
button_stop = tk.Button(frame3, text="STOP measurement(s)", bg='#80c1ff', fg='red', state='disabled', command=stop_measurement)
button_stop.place(relx=0.5, rely=0.5, anchor='center')
# --- frame 3 ---
# --- frame 4 ---
frame4 = tk.Frame(root, bd=5)
frame4.place(relx=0, rely=0.3, relheight=0.09, relwidth=1, anchor='nw')
label2 = tk.Label(frame4, text="Select a sensor to plot data: ")
label2.place(relx = 0.1, rely=0.3, relwidth=0.3, relheight=0.5)
clickedForPlotting = tk.StringVar()
clickedForPlotting.set(OPTIONS[0]) # default value
dropPlot = tk.OptionMenu(frame4, clickedForPlotting, *OPTIONS)
dropPlot.place(relx=0.5, rely=0.5, anchor='center')
#CHANGE LATER
#dropDownButton = tk.Button(frame4, text="Plot sensor data", bg='#80c1ff', fg='red', command=single_Sensor) #bg='gray'
#dropDownButton.place(relx = 0.85, rely=0.5, anchor='center')
# --- frame 4 ---
#frame 5 will be the save to txt file
#frame 6 will be the area with the text field
# --- frame 6 ---
frame6 = tk.Frame(root, bg='#80c1ff') #remove color later
frame6.place(relx=0.0, rely=0.4, relheight=1, relwidth=1, anchor='nw')
text_frame=tk.Frame(frame6)
text_frame.place(relx=0, rely=0, relheight=0.6, relwidth=1, anchor='nw')
text=tk.Text(text_frame)
text.place(relx=0, rely=0, relheight=1, relwidth=1, anchor='nw')
vsb=tk.Scrollbar(text_frame)
vsb.pack(side='right',fill='y')
text.config(yscrollcommand=vsb.set)
vsb.config(command=text.yview)
# --- frame 6 ---
# start data monitor task
data_monitor(queue)
root.mainloop() #here we run our app
# --- main ---
In order to not blocking the main tkinter application, it is recommended to use thread to run the serial reading. Also use queue.SimpleQueue to transfer the serial data to main task so that the serial data can be inserted into the Text widget.
Below is an example:
import threading
import time
from queue import SimpleQueue
import tkinter as tk
import serial
class SerialReader(threading.Thread):
def __init__(self, ser, queue, *args, **kw):
super().__init__(*args, **kw)
self.ser = ser
self.queue = queue
self._stop_flag = threading.Event()
def run(self):
print("started")
while not self._stop_flag.is_set():
if self.ser.in_waiting:
ser_bytes = self.ser.readline()
data = ser_bytes.decode("utf-8")
self.queue.put(data)
else:
time.sleep(0.1)
print("stopped")
def terminate(self):
self._stop_flag.set()
# create the serial instance
ser = serial.Serial(port="COM1") # provide other parameters as well
# create the queue for holding serial data
queue = SimpleQueue()
# the serial reader task
reader = None
def start_reader():
global reader
if reader is None:
# create the serial reader task
reader = SerialReader(ser, queue, daemon=True)
if not reader.is_alive():
# start the serial reader task
reader.start()
else:
print("Reader is already running")
def stop_reader():
global reader
if reader and reader.is_alive():
# stop the serial reader task
reader.terminate()
reader = None
else:
print("Reader is not running")
# function to monitor whether data is in the queue
# if there is data, get it and insert into the text box
def data_monitor(queue):
if not queue.empty():
text.insert("end", f"{queue.get()}\n")
root.after(100, data_monitor, queue)
root = tk.Tk()
text = tk.Text(root, width=80, height=20)
text.pack()
frame = tk.Frame(root)
tk.Button(frame, text="Start", command=start_reader).pack(side="left")
tk.Button(frame, text="Stop", command=stop_reader).pack(side="left")
frame.pack()
# start data monitor task
data_monitor(queue)
root.mainloop()
Update#2021-04-16: Example without using class
import threading
import time
from queue import SimpleQueue
import tkinter as tk
import serial
# create the serial instance
ser = serial.Serial(port="COM1") # provide other parameters as well
# create the queue for holding serial data
queue = SimpleQueue()
# flag use to start/stop thread tasks
stop_flag = None
def readSerial(queue):
global stop_flag
if stop_flag:
print("Reading task is already running")
else:
print("started")
stop_flag = threading.Event()
while not stop_flag.is_set():
if ser.in_waiting:
ser_bytes = ser.readline()
data = ser_bytes.decode("utf-8")
queue.put(data)
else:
time.sleep(0.1)
print("stopped")
stop_flag = None
def start_reader():
threading.Thread(target=readSerial, args=(queue,)).start()
def stop_reader():
if stop_flag:
stop_flag.set()
else:
print("Reading task is not running")
# function to monitor whether data is in the queue
# if there is data, get it and insert into the text box
def data_monitor(queue):
if not queue.empty():
text.insert("end", f"{queue.get()}\n")
root.after(100, data_monitor, queue)
root = tk.Tk()
text = tk.Text(root, width=80, height=20)
text.pack()
frame = tk.Frame(root)
tk.Button(frame, text="Start", command=start_reader).pack(side="left")
tk.Button(frame, text="Stop", command=stop_reader).pack(side="left")
frame.pack()
# start data monitor task
data_monitor(queue)
root.mainloop()
Newbie question: How to make a tkinter scale operate continuously so that whenever the value is changed, the new value is written (to I2C or to a file) and this repeats until the user terminates operation of the overall program (there will need to be multiple scale controls that are operational on the GUI until the program is exited by user). The purpose is to keep sending volume values as changed by the slider. I've had no problem with buttons, text entry, radiobuttons, but I just don't see what I need to add to accomplish this. Here is code:
from tkinter import * # Python 3
from math import log10
root = Tk()
root.title("Fidelity Science Audio Control GUI")
root.geometry("500x500")
Volume = IntVar
Vol = Scale(root, variable=Volume, from_=0, to=100, tickinterval=10, orient=VERTICAL,
length=400, width=20, borderwidth=10, background="light blue", foreground="black",
troughcolor="light blue", label="Volume")
Vol.set(50)
Vol.grid(row=10, column=1)
if Vol.get() > 0:
Volume = Vol.get()
LogVolume = log10(Volume)
print("Volume = ", Volume)
print("Value sent To I2C =", LogVolume)
root.mainloop()
Do I need to use a loop function like while or if? Thanks!
You can use the callback associated to the Scale via command option to send the volume to I2C when the value of the Scale is changed:
from tkinter import * # Python 3
from math import log10
def on_volume_change(volume):
if volume > 0:
LogVolume = log10(volume)
print("Volume =", volume)
print("Value sent to I2C =", LogVolume)
# send the volume to I2C
root = Tk()
root.title("Fidelity Science Audio Control GUI")
root.geometry("500x500")
Volume = IntVar(value=50)
Vol = Scale(root, variable=Volume, from_=0, to=100, tickinterval=10, orient=VERTICAL,
length=400, width=20, borderwidth=10, background="light blue", foreground="black",
troughcolor="light blue", label="Volume", command=on_volume_change)
Vol.grid(row=10, column=1)
# send the initial volume
on_volume_change(Volume.get())
root.mainloop()
I'm trying to start a counter that displays the value in a label on a separate display window.
The main window has a START, STOP and DISPLAY button.
The START must start the counter, STOP must stop it and the display window must open only when I click on DISPLAY button.
Here's what I have so far. The buttons seem to be unresponsive and the display window pops up without user intervention. How can I fix this?
import tkinter as tk
import time
import threading
import queue
def Run_Device(disp_q,flaq_q):
temp_q = queue.Queue()
temp_q.put(0)
while(flaq_q.empty()):
#time.sleep(0.2)
count = temp_q.get()
count += 1
temp_q.put(count)
disp_q.put(count)
else:
flaq_q.queue.clear()
def P_Window(disp_q):
pw = tk.Tk()
value_label = tk.Label(pw, text=disp_q.get(), relief='sunken', bg='lemon chiffon', font='Helvetica 16 bold')
value_label.pack()
def update_values():
value_label.config(text=disp_q.get())
value_label.after(1000,update_values)
update_values()
pw.mainloop()
def Stop_Dev(flaq_q):
flaq_q.put("Stop")
if __name__ == "__main__":
disp_q = queue.Queue()
flaq_q = queue.Queue()
t_device = threading.Thread(target=Run_Device, args=(disp_q, flaq_q), name="Device 1")
t_disp = threading.Thread(target=P_Window, args=(disp_q, ), name="Display 1")
window = tk.Tk()
start_button = tk.Button(window, text='Start', command=t_device.start(), bg='spring green', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
start_button.pack()
stop_button = tk.Button(window, text='Stop', command=lambda: Stop_Dev(flaq_q), bg='OrangeRed2', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
stop_button.pack()
disp_param_button = tk.Button(window, text='Display', command=t_disp.start(), bg='sky blue', font='Helvetica 12 bold', width=20, state='normal', relief='raised')
disp_param_button.pack()
window.mainloop()
I'm trying to learn how to use multithreading in tkinter so any feedback would be appreciated
There are two issues I see with your code. The first is just simple bugs, e.g.:
start_button = tk.Button(window, text='Start', command=t_device.start(), ...
here it should be command=t_device.start otherwise the command is the function returned by t_device.start()
The second is you've not dealt with various "What if ...?" scenarios, e.g. What if the user pushes 'Start' or 'Display' multiple times?
I've tried to address the above in my rework below:
import tkinter as tk
from time import sleep
from queue import Queue, Empty
from threading import Thread
FONT = 'Helvetica 16 bold'
def run_device():
count = 0
while flaq_q.empty():
count += 1
disp_q.put(count)
sleep(0.5)
while not flaq_q.empty(): # flaq_q.queue.clear() not documented
flaq_q.get(False)
def p_window():
global pw
if pw is None:
pw = tk.Toplevel()
value_label = tk.Label(pw, text=disp_q.get(), width=10, font=FONT)
value_label.pack()
def update_values():
if not disp_q.empty():
try:
value_label.config(text=disp_q.get(False))
except Empty:
pass
pw.after(250, update_values)
update_values()
elif pw.state() == 'normal':
pw.withdraw()
else:
pw.deiconify()
def stop_device():
if flaq_q.empty():
flaq_q.put("Stop")
def start_device():
global device
if device and device.is_alive():
return
while not disp_q.empty():
disp_q.get(False)
disp_q.put(0)
device = Thread(target=run_device)
device.start()
if __name__ == "__main__":
disp_q = Queue()
flaq_q = Queue()
root = tk.Tk()
pw = None
device = None
tk.Button(root, text='Start', command=start_device, width=20, font=FONT).pack()
tk.Button(root, text='Stop', command=stop_device, width=20, font=FONT).pack()
tk.Button(root, text='Display', command=p_window, width=20, font=FONT).pack()
root.mainloop()
I've left out some details to simplify the example. Even with its additional checks, it's still nowhere near perfect. (E.g. it hangs if you don't 'Stop' before you close the window, etc.)
You might be able to adapt something like the following:
import tkinter as tk
import threading
import queue
import time
def run_device():
for i in range(4):
print(i)
pass
def process_queue(MyQueue):
"""Check if we got a complete message from our thread.
Start the next operation if we are ready"""
try:
# when our thread completes its target function (task),
# the supplied message is added to the queue
msg = MyQueue.get(0)
# check message
print(msg)
except queue.Empty:
# .get failed, come back in 100 and check again
print('no message')
threading.Timer(0.001, lambda q=MyQueue: process_queue(q)).start()
class ThreadedTask(threading.Thread):
"""threaded task handler"""
def __init__(self, queue, target, msg):
threading.Thread.__init__(self)
# message to add to queue when the task (target function) completes
self.msg = msg
# function to run
self._target = target
# queue to store completion message
self.queue = queue
def run(self):
"""called when object is instantiated"""
# start users task
try:
self._target()
except Exception as e:
self.queue.put('Thread Fail')
return
# we are done, pass the completion message
self.queue.put(self.msg)
if __name__ == '__main__':
MyQueue = queue.Queue()
MyThread = ThreadedTask(MyQueue, run_device, 'Thread Task: Run')
MyThread.start()
process_queue(MyQueue)
So basically I am doing a school project and it involves classes and tkinter, I want to put my tick() function into the class so that I can have a clock in my program, however I can't seem to get it to work when I add it to a class?
Any help/advice would be appreciated.
import urllib.request
from tkinter import *
from tkinter import ttk
import time
class MainWindow(object):
def __init__(self):
self.root = Tk()
self.root.state("zoomed") #to make it full screen
self.root.title("Vehicle Window Fitting - Management System")
self.root.configure(bg="grey80")
self.frame_1 = Frame(self.root, width=1350, height=50)
self.frame_1.pack(side=TOP, fill=X, expand=1, anchor=N)
self.title_lbl = Label(self.frame_1, font=('arial', 12, 'bold'), text="Vehicle Window Fitting - Management System", bd=5, anchor=W)
self.title_lbl.pack(side=LEFT) #puts title on left side
self.clockFrame = Frame(self.frame_1, width=100, height=50, bd=4, relief="ridge")
self.clockFrame.pack(side=RIGHT)#puts clock frame on right
self.clockLabel = Label(self.clockFrame, font=('arial', 12, 'bold'), bd=5, anchor=E)
self.clockLabel.pack() #puts the number in the clock frame
def tick(self, curtime=''): #acts as a clock, changing the label when the time goes up
newtime = time.strftime('%H:%M:%S')
if newtime != curtime:
curtime = newtime
self.clockLabel.config(text=curtime)
self.clockLabel.after(200, tick, curtime)
(more code here but irrelevant)
then:
window = MainWindow()
clock = window.tick()
Everything is in place correctly but the clock won't change it is stationary. I have managed to get it working outside of a class but how do I implement it into a class?
fixed by changing
self.clockLabel.after(200, tick, curtime)
to
self.clockLabel.after(200, self.tick, curtime)
took me a long time to figure that one out haha
I'm using raspberry pi because i need a GUI to display my LED blinker counter project. I'm having an error in
IntVar()
It says that it has attribute error __trunc__ Here is my code:
import sys
import RPi.GPIO as GPIO
import time
import math
from Tkinter import *
GPIO.setmode(GPIO.BOARD)
GPIO.setup(16, GPIO.OUT)
root = Tk()
root.geometry("200x200")
root.title("Simple Counter")
counter = IntVar()
def onClick1(event=None):
counter.set(counter.get()+1)
def onClick2(event=None):
counter.set(counter.get()-1)
def count():
for x in range(0, counter):
GPIO.output(16, True)
time.sleep(0.5)
GPIO.output(16, False)
entry = Entry(root, textvariable=counter).pack()
button = Button(root, text="Press to Add", command=onClick1).pack()
button2 = Button(root, text="Press to Subtract", command=onClick2).pack()
button3 = Button(root, text="Press to Continue", command=count).pack()
root.mainloop()
The problem is in the line:
for x in range(0, counter):
it should be
for x in range(0, counter.get()):
like it is in your onClick functions.