0

I'm trying to make a Tkinter app that connects to a selected serial port, and displays both sent and receiving data to a text widget. I am not sure how to go about handling the constant checking of data from the serial port in such an app, as my initial approach results in the app window being unresponsive after a few seconds. More specifically, after the user selects a port and connects to it, I attempt to run 2 functions continuously. One reading any data from the serial port if there is any available( receive_data() ), and one for refreshing the text widget with any new data( refresh_comms() ). Any help on the matter is greatly appreciated, and comments on the code overall are welcome as well! Here is the code :

import serial
import tkinter as tk
from tkinter import ttk
import time
import threading




class Application(tk.Tk):
    rel_entrywidth_y= 0.02
    baud_rate_list = [1200, 1800, 2400, 4800, 7200, 9600, 14400, 19200]
    HEIGHT = 650
    WIDTH = 750
    
    def __init__(self):
        super().__init__()
        self.title("SERIAL COMMUNICATION")
        self.default_com = 'COM13'
        self.connect_state = "Not Connected"
        self.data = []
        self.temp = []
        self.createWidgets()





    def createWidgets(self):
        self.canvas = tk.Canvas(self, width=self.WIDTH, height=self.HEIGHT, bg='#adadad')
        self.canvas.pack()
        
        self.port_label = tk.Label(self.canvas, text="Select Port:", font="Courier 12", bg = '#adadad')
        self.port_label.place(anchor='nw', relx=0.01, rely=self.rel_entrywidth_y, relwidth=0.18, relheight=0.05)

        self.port_entry = tk.Entry(self.canvas, text='COM1', font="Courier 12", bg='#A6CCF0', justify='center')
        self.port_entry.insert(0, self.default_com)
        self.port_entry.place(anchor='nw', relx=0.21, rely=self.rel_entrywidth_y, relwidth=0.18, relheight=0.05)

        self.baud_rate_var = tk.StringVar(self.canvas)
        self.baud_rate_var.set(self.baud_rate_list[5])           # set default baud rate

        self.baud_label = tk.Label(self.canvas, text="  Baud Rate:", font="Courier 12", bg = '#adadad')
        self.baud_label.place(anchor='nw', relx=0.4, rely=self.rel_entrywidth_y, relwidth=0.18, relheight=0.05)

        self.baud_menu = tk.OptionMenu(self, self.baud_rate_var, *self.baud_rate_list)
        self.baud_menu.config(font='Courier 12', bg='#A6CCF0')
        self.baud_menu.place(anchor='nw', relx=0.6, rely=self.rel_entrywidth_y, relwidth=0.18, relheight=0.05)

        self.connect_button = tk.Button(self.canvas, text='Connect', font='Courier 12 bold', command=self.connect_func)
        self.connect_button.place(anchor='nw', relx=0.815, rely=self.rel_entrywidth_y, relwidth=0.15, relheight=0.05)

        self.canvas.create_line(0, 60, self.WIDTH, 60, width=1)

        #self.canvas.create_line(0, 570, WIDTH, 570, width=1)

        #self.separator1 = ttk.Separator(self.canvas, orient='horizontal', bg='#adadad')
        #self.separator1.place(relx=0, rely=0.5, relwidth=1, relheight=0.5)


        self.comm_text = tk.Text(self, width=40, height=10, font='Courier 12', state='disabled')
        self.comm_text.place(anchor='n', relx=0.63, rely=0.1, relwidth=0.7, relheight=0.75)
        self.comm_vsb = tk.Scrollbar(self, orient='vertical', command=self.comm_text.yview)
        self.comm_vsb.place(anchor='n', relx=0.985, rely=0.101, relwidth=0.03, relheight=0.749)

        self.send_button = tk.Button(self.canvas, text='Send', font='Courier 12 bold', command=threading.Thread(target=self.send_data).start())
        self.send_button.place(anchor='n', relx=0.9, rely=0.89, relwidth=0.15, relheight=0.07)
        
        self.data_entry = tk.Entry(self.canvas, font="Courier 14", bg='#d1d1d1', justify='left')
        self.data_entry.place(anchor='n', relx=0.41, rely=0.875, relwidth=0.8, relheight=0.1)

        
        self.img = tk.PhotoImage(file='serial.png')
        self.img = self.img.subsample(9)
        self.canvas.create_image(50, 500, anchor='nw', image=self.img)




    def connect_func(self):
        if self.connect_state == "Not Connected":
            self.com_port = self.port_entry.get()
            self.baud_rate = self.baud_rate_var.get()
            self.connect_button['text'] = "Close"
            self.update_idletasks()               # making sure it changes to "Close"
            print("COM PORT IS : ", self.com_port)
            print("BAUD RATE IS : ", self.baud_rate)
            try:
                self.ser = serial.Serial(self.com_port, self.baud_rate)
                self.connect_state = "Connected"
                print("Connected to", self.com_port, "successfully!")
                self.after(500, threading.Thread(target=self.receive_data()).start())
    
            except Exception as e:
                print("Error connecting to port ", self.com_port, " ... ", e)
                self.connect_button['text'] = "Connect"
                self.connect_state = "Not Connected"
                


        elif self.connect_state == "Connected":
            self.ser.close()
            print("Closing serial connection with port", self.com_port)
            self.connect_button['text'] = "Connect"
            self.connect_state = "Not Connected"
  



    def send_data(self):
        if self.connect_state == "Connected":
            self.send_data = self.data_entry.get()
            self.ser.write(self.send_data.encode())
            print("SENDING... : " + self.send_data)
            self.data.append(self.send_data)
            self.refresh_comms(self.send_data)
        else:
            print("Connect to a serial port first...")



    def refresh_comms(self, d):
        #print("temp : ", self.temp)
        #print("data : ", d)
        if len(self.temp) < len(d):             # if data has been added
            self.comm_text.configure(state='normal')
            self.comm_text.insert('end', d[len(d)-1] + '\n')
            self.comm_text.see('end')                     # scroll text
            self.comm_text.configure(state='disabled')
            self.update_idletasks()
            self.temp.append(d[len(d)-1]) 
        
        self.update_idletasks()
        self.after(500, self.receive_data)




    def receive_data(self):
        if self.ser.in_waiting:
            received = self.ser.readline().decode()
            self.data.append(str(self.com_port) + ' : ' + received)
            print("I received :", received)
        
        self.update_idletasks()
        self.after(500, self.refresh_comms(self.data))





app = Application()
app.resizable(False, False)

app.mainloop()
Bill B
  • 73
  • 3
  • 7
  • Please add the detailed information of the crash that occurred. Please add any other information that may be relevant or what you searched for and how. – kunif Feb 04 '21 at 15:33
  • I should rephrase. It is not actually crashing, but the window is not responsive and not movable (sometimes it might crash also). The issue as I understand it, is in the receive_data and refresh_comms functions. I am not certain about how one would go about reading data from the serial port and updating the window with that data continuously. – Bill B Feb 04 '21 at 16:34
  • Please add to the article, not the comment. People looking up or answering can't see what the information is, how it relates to the question description or the source code. – kunif Feb 04 '21 at 16:52
  • These articles may be helpful. [Reading serial input and printing to Tkinter GUI](https://stackoverflow.com/q/52010473/9014308), [update tkinter label from serial data whenever there's new data from serial port python 3.x](https://stackoverflow.com/q/17463521/9014308), [Serial communication with Tkinter](https://stackoverflow.com/q/28166070/9014308) – kunif Feb 05 '21 at 08:37

1 Answers1

0

To answer my own question, after some research on the topic, I made it work. This did the trick :

  1. Creating one thread for the receive_data function, and another for updating the gui, calling them both after I am connected to the serial port.
  2. Not having the two functions call one another, running separately. This is part of the code, for anyone curious for details:

calling the two functions in threads after connecting:

self.thread1 = threading.Thread(target=self.receive_data)
self.thread1.daemon = True
self.thread1.start()
                
self.thread2 = threading.Thread(target=self.update_gui)
self.thread2.daemon = True
self.thread2.start()

the receive_data functions and the update_gui function themselves:

    def update_gui(self):
        while True:
            try:
                if len(self.temp) < len(self.data) and self.ser != None:             # if data has been added
                    self.comm_text.configure(state='normal')
                    self.comm_text.insert('end', self.data[len(self.data)-1] + '\n')
                    self.comm_text.see('end')                     # scroll text
                    self.comm_text.configure(state='disabled')
                    self.temp.append(self.data[len(self.data)-1]) 
                time.sleep(0.2)
            except:
                pass


    def receive_data(self):
        while True:
            if self.ser != None:
                try:
                    self.received = self.ser.readline().decode().replace('\n', '')
                    self.data.append(str(self.com_port) + ': ' + self.received)
                    print("I received :", self.received)
                except:
                    pass
            time.sleep(0.2)
Bill B
  • 73
  • 3
  • 7