Introduction: UART Controller With Tkinter and Python (GUI)

About: A hobbyist, student, tech enthusiast, full time programmer and python lover living and studying in Warsaw, Europe.

The Goal of this Tutorial is to create a simple application for accessing UART data from an external controller and displaying them on a GUI using python and the Tkinter framework. We will come across different widgets used in this tutorial and I will briefly explain the uses of such widgets.

There are plenty of other frameworks out there such as PyQt, Kivy, Pyforms, WxPython Etc. But Tkinter is quite an easy framework and it should get you started towards full stack development in the field of robotics, analytics or even IoT.

GUI's are a very important and a powerful tool for visualizing your data sets, the only contact from the external world and your project is through a thin slice of aesthetic application layer, which beautifies your creation for another person's ease of usage.

Step 1: Things You Need to Know.

Python is quite an interesting language that has got everyone captive with its ease and top performance in terms of string manipulation, data types, structures and objects and what not.

Here I have my first tutorial on Instructables with Python and Tkinter, Tkinter a simple GUI framework for generic purposes in Python. Tkinter can be used for a lot of purposes such as fast prototyping and for making testing applications and anything you can dream off.

Tkinter has lots of inbuilt widgets which we have made use of in this tutorial.

  • Progress Bars
  • Button(s)
  • Entry Widgets
  • Text Boxes
  • Frames.

Apart from learning widgets you will also come across multi-threading, Serial interface with Python, functions and simple data types.

Goal

The main intention is to write a simple application which can read serial data from an external controller and display the analog data on a console and show progress bar increments. An example Arduino code has also been put here to produce UART data. There is a whole world of applications for this such as reading sensor data, analytics, even industries use such a simple system to monitor there huge machines and I have taken a small chunk of their architecture to produce this project.

Step 2: Things Needed.

You might need a couple of basic things to get started with this project.

  • Computer (I've worked on Ubuntu)
  • Arduino (uno or clone or anything that works your way)
  • Usb Cable
  • Python 2.7
  • Tkinter Library Installed
  • Code Editor ( My Fav Sublime Text)
  • A lot of patience and love <3

Step 3: Begin

So open Your code editor and add a few things into your code and compile them to check if you have your modules ready to roll.

  • Import Tkinter
  • from Tkinter import *
  • import ttk
  • import threading
  • import time

All of these modules are required to run this project, deficient in any of these might be quite a hard time to get it to work. While import Tkinter the 'T' has to be a capital. We import everything from the modules to prevent it from throwing a lot of errors.

We do also have some amount of Exception handling which can handle a lot of errors that are thrown due to various reasons.

If you do have errors in importing any one of the modules drop in a comment and I shall try to get to you.

  • If you have a problem with Tkinter installation you can check this link, Tkinter import Error.
  • If you have a problem with the threading module in python check this link, Here.

Step 4: Write You Python Code

Start of with the basics such as instantiating the GUI object. Once the object has been created, give it a title. If your GUI has to pop up, you will have to put it in a loop, set the geometry and run it in the loop, like in the picture.

gui.mainloop() has to be at the end of everything.

Declare a couple of global variables at the top since, this is not a class we might be better off adding a few global variables. It wouldn't harm us ;)

serial_data = ''
filter_data = ''
update_period = 5
serial_object = None
gui = Tk()
gui.title("UART Interface")

First off lets get data and put this function in a thread.

def get_data():
"""This function serves the purpose of collecting data from the serial object and storing 
   the filtered data into a global variable.
   The function has been put into a thread since the serial event is a blocking function.
    """
    global serial_object
    global filter_data
    while(1):   
        try:
            serial_data = serial_object.readline().strip('\n').strip('\r')
            filter_data = serial_data.split(',')
            print filter_data
        except TypeError:
            pass

This is put in a while loop so that the serial object.readline() is reading data continuously over time.

Now we have our data, lets have our connect function, which starts a thread with a button click.

def connect():    
"""The function initiates the Connection to the UART device with the Port and Buad fed through the Entry
    boxes in the application.
    
    The radio button selects the platform, as the serial object has different key phrases 
    for Linux and Windows. Some Exceptions have been made to prevent the app from crashing,
    such as blank entry fields and value errors, this is due to the state-less-ness of the 
    UART device, the device sends data at regular intervals irrespective of the master's state.</p><p>    The other Parts are self explanatory.
    """
    version_ = button_var.get()
    print version_
    global serial_object
    port = port_entry.get()
    baud = baud_entry.get()  
    try:
      if version_ == 2:
          serial_object = serial.Serial('/dev/tty' + str(port), baud)
      elif version_ == 1:
          serial_object = serial.Serial('COM' + str(port), baud)    except ValueError:
      print "Enter Baud and Port"
     return    
    t1 = threading.Thread(target = get_data)
    t1.daemon = True
    t1.start()

This snippet basically is the second half of the GUI app that you see in the first picture. You can choose which version you are running this app on, whether its windows or Linux, because instantiation of the serial object on both the platforms are different, so we have a radio_button which chooses the version and based on the variable stored in the radio_button it creates a serial object. Once the object is created it creates a thread and starts collecting data with the get_data_ function runs it in daemon mode.

Since the while() loop in the get_data() function is a blocking function it is necessary to run on a separate thread or it will freeze the GUI.

Once the data is received and the connection has been established we create the update_gui() function to update our progress bars and text box. (have a look at the pictures above).

def update_gui():
"""" This function is an update function which is also threaded. The function assimilates the data
    and applies it to it corresponding progress bar. The text box is also updated every couple of seconds.</p><p>    A simple auto refresh function .after() could have been used, this has been avoid purposely due to 
    various performance issues.
"""
    global filter_data
    global update_period
    text.place(x = 15, y = 10)
    progress_1.place(x = 60, y = 100)
    progress_2.place(x = 60, y = 130)
    progress_3.place(x = 60, y = 160)
    progress_4.place(x = 60, y = 190)
    progress_5.place(x = 60, y = 220)
    new = time.time()
 
    while(1):
        if filter_data:    
            text.insert(END, filter_data)
            text.insert(END,"\n")
            try:
                progress_1["value"] = filter_data[0]
                progress_2["value"] = filter_data[1]
                progress_3["value"] = filter_data[2]
                progress_4["value"] = filter_data[3]
                progress_5["value"] = filter_data[4]</p><p>            
            except :
                pass            
                if time.time() - new >= update_period:
                text.delete("1.0", END)
                progress_1["value"] = 0
                progress_2["value"] = 0
                progress_3["value"] = 0
                progress_4["value"] = 0
                progress_5["value"] = 0
                new = time.time()

Step 5: Continue Here...

We now have the connect(), update_gui() and get_data() functions in place, we will also add a simple send_data() function to send some ascii characters from the computer to the hardware controller.

def send():    
"""This function is for sending data from the computer to the host controller.
   

The value entered in the the entry box is pushed to the UART. The data can be of any format, since
   the data is always converted into ASCII, the receiving device has to convert the data into the required f
   format.
    """
    send_data = data_entry.get()    
    if not send_data:
        print "Sent Nothing"
        serial_object.write(send_data)

This functions sends data entered on the small Entry bar. Though the data incoming and outgoing do not happen asynchronously, the data is sent in between the incoming buffers.

We also need a small disconnection feature to end the app correctly.

def disconnect():    
   """ 
    This function is for disconnecting and quitting the application.</p><p>    Sometimes the application throws a couple of errors while it is being shut down, the fix isn't out yet
    but will be pushed to the repo once done.  
    simple GUI.quit() calls.    
    """
    try:
        serial_object.close()     
    except AttributeError:
        print "Closed without Using it -_-"
     gui.quit()

Once this is added, the main loop is left to do, the code is extremely long and to help serve the purpose of a proper tutorial I have added my Git repo link, you can pull my code from there and run it through.

The order of the functions matter.

  • def connec()
  • def get_data()
  • def update_gui()
  • def send()
  • def disconnect()
  • mainloop()

The reason you might have to maintain the order is because the objects are created in an order and initialized, if the object is created after it is told to be used, your code will crash like a stack of cards.

The GitHub Link to the Entire Code

NOTE:

The code will not directly run on your computer. There might be a couple of errors, before flooding the comments look at the error, you might get indentation errors, if so re indent the code and convert tabs to spaces and compile them again.


The Arduino Code.

<p>void setup()<br>{
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}
uint8_t data[5] = {100, 111, 255, 123, 233};</p><p>String data_ =  {"100,123,1,  22,180"};
String data_2 = {"129,123,160,194,92"};
String data_3 = {"130,23, 15, 204,132"};
String data_4 = {"200,13, 255,224,142"};
String data_5 = {"234,233,145,0,122"};
String data_6=  {"245,50, 45, 101,142"};
String data_7 = {"40,99,  245,187,222"};</p><p>unsigned long long timer = 0;</p><p>void loop()
{
  if (millis() - timer > 3000)
  {
    
      Serial.println(data_);
      Serial.println(data_2);
      Serial.println(data_3);
      Serial.println(data_4);
      Serial.println(data_5);
      Serial.println(data_6);
      Serial.println(data_7);
      
    timer = millis();</p><p>    }
   
while(Serial.available())
{
  char c = Serial.read(); 
  if (c == 'd')
  {
    digitalWrite(13, HIGH);
    delay(100);
    digitalWrite(13, LOW);
    delay(100);
  }
  
  else
  {
    digitalWrite(13, LOW);
  }
 }
}</p>

This is only a sample sketch to produce data, but keep in mind that the format of data has to be of the same. You might have to convert your data to strings, concatenate them and send them. I will surely change this in the near future when I get the time. But this works great as well.

  • Baud - 9600
  • Port - /dev/ttyACM0'

Step 6: Lets Use It.

Run the App using Ctrl + B on sublime or any other compile option on your code editor.

Or type python file_name.py on the command line to run it.

  • First check the radio_boxes for the right option, Linux or Windows?
  • Type in the baud rate of 9600 which is fixed.
  • Type in the port name, on linux it is usually ACM0, USB0, USB1, or open up the arduino editor and check the name of the port.
  • Click Connect and you can see the data is displayed on the graphs and the text box is flooded with incoming data.
  • If you choose to quit, press disconnect and it will close automatically.

NOTE:

This has not been tested for windows. So please look out for basic errors on windows and let me know.

Initially the text box might have garbage data, but every 5 seconds the data is wiped out and a fresh set comes in. All these variables can be changed in the code.

Hope this tutorial was a worthy one. This is first tutorial ever, so please forgive me for any errors, I'm always open for newer ideas and improvisations. I'm gonna make another one with slider to control PWM pulses soon. Give it a thumbs up and let me know what you feel for this :)