Python Forum
Synchronizing tkinter with serial baud rate
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Synchronizing tkinter with serial baud rate
#1
I am creating an appplication, for displaying my arduino serial data that i receive.

However, my application has bugs.

Sometimes, after some time, the program gets characters mixed.

Here, i am uploading a picture:

[Image: 4IUulnu]
https://imgur.com/4IUulnu

The white part is my tkinter frame, the black is the terminal. As you can see, in the beginning everything looks good, but then, characters that should not be there start to appear.

This is the code for this:

def readSerial():
    global after_id
    try:
       ser_bytes = ser.readline()
       print(ser_bytes)
       ser_bytes = ser_bytes.decode("utf-8")
       print(ser_bytes)
    except UnicodeExceptionError:
       print("Oops")
    text.insert("end", ser_bytes)
    after_id=root.after(50,readSerial)
This behavior does not always happen (which is weird), but when it does, i have noticed that the text response is slow. So when i change the value of the potentiometer, it does not instatnly appear in the next measurement, but appears two-three measurements later.

So i suspect that after a while, all this buffered data gets accumulated and starts to mess with my program. But even if this was the case, i do not understand why it is happenning sometimes, and not always.

One of my assumptioms, is an asynchronicity between the baud rate, and the repeatability of the function that performs all the serial read - and tkinter writing.

What i mean isthat i have set up my serial object like this:
ser = serial.Serial(port = COMPort, baudrate=9600, timeout=0.1)
The baud rate is 9600

While the tkinter function repeats every 50 ms:
 after_id=root.after(50,readSerial)
The function keeps repeating after 50 ms.
Maybe 50 is not a correct value, when having 9600 baud rate.

Does anyone know the root of this problem? And also, why this problem happens, but not all the time?
Is my assumption correct?
If i am using 9600 baud rate, then what the correct value for the perdio of the readSerial() function?
Reply
#2
Your senses are superior to mine if you can tell the difference between 50 and 150 ms delay.

I would set the read timeout to zero and modify the readSerial() function to read all the "lines" in the buffer.
def readSerial():
    if reading_serial:
        while (ser_bytes := ser.readline()):
            try:
                text.insert("end", ser_bytes.decode("utf-8"))
            except UnicodeExceptionError:
                print("Oops")
        root.after(50,readSerial)
To stop the readSerial "loop" set reading_serial = False. That way you don't need to keep updating the after_id variable.
Reply
#3
Thanks. I tried your approach, but it does not work.

I get no error, but i see nothing printed on tkinter. Full code here:

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
from time import sleep


#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



# --- 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)
    #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():
    if reading_serial:
        while (ser_bytes := ser.readline()):
            try:
                text.insert("end", ser_bytes.decode("utf-8"))
                if vsb.get()[1]==1.0: #if the scrollbar is down to the bottom, then autoscroll
                   text.see("end")
            except UnicodeExceptionError:
                print("Oops")
        root.after(50,readSerial)




# 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():    
   global reading_serial
   reading_serial = True
   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
   readSerial() #Start Reading data


# this function is triggered, when button 'Measure this Sensor' is pressed, on frame 2
def measure_single(): 
   print(dropdown_value)   
   global stop_
   stop_=False
   button_stop['state']='normal'
   string_to_send = 'r' + ' ' + str(dropdown_value)#CHANGE
   ser.write(string_to_send.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():    
    global reading_serial
    reading_serial=False
    button_stop['state']='disabled'
    ser.write("c".encode())
    root.after_cancel(after_id) #do i need after_id to be global?
# --- 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 ---




reading_serial=False # 'Stop' flag. True when no measuring is happening

root.mainloop() #here we run our app




#Any code after mainloop() will be halted as long as the window stays alive, so placing the code after mainloop() would execute it after the window is closed.

ser.close()
# --- main ---
By the way, do youi happen to understand why this behaviour is happening on my original question?
Reply
#4
I tried my original code once again. 3 times the program crashed. The fourth time, after i pressed the 'Measure all sensors' for the third time, then it managed to work.

I don't understand why sometimes it works, and why sometimes it does not. It's the same code....
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020