Python Programming/GUI/Oscilloscope
This tutorial guides you to develop the Oscilloscope-like desktop application with Tkinter, Matplotlib and 3rd libary pySerial.
Features
Oscilloscope features: Plot the value receives from MCU with Play, Pause, Stop button.
Import csv file to plot static chart
- Import csv file included raw data sent from MCU
- Y-axis: MCU raw data, X-axis: line number from csv file
- Give alert if invalid csv file is imported
Pick your own drawChartIcon.png, oscilloscopeIcon.png, pause.png, play.png and stop.png for this program.
Existed issues
- Button stop doesn't work
- Serial port and baudrate must be initially hardcode
Source code
serialhandler.py
# serialhandler.py
from threading import Thread
import serial
import time
import collections
import struct
import numpy as np
import os
import math
import csv
from datetime import datetime
from tkinter import filedialog
from tkinter import messagebox
class SerialPlot:
def __init__(self, serial_port = '/dev/ttyUSB0', serial_baudrate = 38400, plot_length = 100, data_num_bytes = 2, window = None):
self.port = serial_port
self.baud = serial_baudrate
self.plot_max_length = plot_length
self.data_num_bytes = data_num_bytes
self.raw_data = bytearray(data_num_bytes)
self.data = collections.deque([0] * plot_length, maxlen=plot_length)
self.is_run = True
self.is_receiving = False
#Thread set up
self.serial_thread = None
self.plot_timer = 0
self.previous_timer = 0
print('Trying to connect to: ' + str(serial_port) + ' at ' + str(serial_baudrate) + ' BAUD.')
try:
self.serial_connection = serial.Serial(serial_port, serial_baudrate, timeout=4)
print('Connected to ' + str(serial_port) + ' at ' + str(serial_baudrate) + ' BAUD.')
except:
print("Failed to connect with " + str(serial_port) + ' at ' + str(serial_baudrate) + ' BAUD.')
def read_serial_start(self):
if self.serial_thread == None:
self.serial_thread = Thread(target=self.background_thread)
self.serial_thread.start()
# Block till we start receiving values
while self.is_receiving != True:
time.sleep(0.1)
def get_serial_data(self, frame, oscilloscope_lines, line_value_text, line_label, time_text):
currentTimer = time.perf_counter()
self.plot_timer = int((currentTimer - self.previous_timer) * 1000) # the first reading will be erroneous
self.previous_timer = currentTimer
time_text.set_text('Plot Interval = ' + str(self.plot_timer) + 'ms')
oscilloscope_lines.set_data(range(self.plot_max_length), self.data)
def background_thread(self): # retrieve data
time.sleep(1.0) # give some buffer time for retrieving data
self.serial_connection.reset_input_buffer()
while (self.is_run):
self.serial_connection.readinto(self.raw_data)
value, = struct.unpack('f', self.raw_data) # use 'h' for a 2 byte integer
if value > 100 or value < -100:
value = 0
self.serial_connection.reset_input_buffer()
self.data.append(value)
self.is_receiving = True
def close(self):
self.is_run = False
self.serial_thread.join()
self.serial_connection.close()
print('Disconnected...')
filehandler.py
from tkinter import filedialog
from tkinter import messagebox
from tkinter import *
import os, csv
class FileHandler():
def open_csv_file_to_load(self, window, static_chart_value):
static_chart_value['x_values'] = []
static_chart_value['y_values'] = []
currentDirectory = os.getcwd()
window.filename = filedialog.askopenfilename(initialdir = currentDirectory, title = "Select csv file", filetypes = [("csv files","*.csv")])
with open(window.filename, 'r') as csv_file:
csv_reader = csv.reader(csv_file)
for line in csv_reader:
if len(line) == 0: pass
else:
try:
static_chart_value['x_values'].append(int(line[0]))
# self.x_val.append(datetime.strptime(line[0], '%Y-%m-%d %H:%M:%S.%f')) # convert to datetime
static_chart_value['y_values'].append(float(line[1])) # convert to float
except:
messagebox.showinfo("Invalid value in csv file", " Invalid data type or character was found in csv file. Draw chart mode only accept csv file include datetime and float value")
guisetup.py
from tkinter import *
from tkinter.ttk import *
from tkinter import filedialog
from datetime import datetime
from tkinter import messagebox
from filehandler import FileHandler
import tkinter.ttk as comboBox
import os, csv, sys
import serial.tools.list_ports
class GUI:
def __init__(self):
self.window = Tk()
# Main GUI handler for main window
width_window = self.window.winfo_screenwidth()
height_window = self.window.winfo_screenheight()
# full screen window, appear at x=0, y=0
self.window.geometry("%dx%d+0+0"%(width_window, height_window))
self.note = Notebook(self.window)
self.draw_static_chart_tab = Frame(self.note)
self.draw_static_chart_tab.pack(side=TOP, fill=BOTH)
self.oscilloscope_tab = Frame(self.note)
self.file_handler = FileHandler()
self.static_chart_value = {
"x_values": [],
"y_values": []
}
def menu_setup(self):
self.menu_bar = Menu(self.window)
self.window.config(menu=self.menu_bar)
self.file_submenu = Menu(self.menu_bar) # File SubMenu to ask user to import csv file
self.tools_submenu = Menu(self.menu_bar) # Tool Submenu will display the connected PORT
# submenu File to import CSV file
self.menu_bar.add_cascade(label="File", menu=self.file_submenu)
# submenu
self.menu_bar.add_cascade(label="Tools", menu=self.tools_submenu)
# Import CSV file to draw static chart
self.file_submenu.add_command(
label="Open file",
command = lambda: self.file_handler.open_csv_file_to_load(
self.window,
self.static_chart_value
)
)
def tools_sub_menu_setup(self):
# List connected PORT
self.connected_port_menu = Menu(self.tools_submenu) # , postcommand= lambda: self.scanForExistedPorts(self.connectedPORTMenu))
# Tools sub menu
# Display connected COM Port
self.tools_submenu.add_cascade(label="Ports", menu=self.connected_port_menu)
# Load all existed images
def image_init(self):
draw_chart_icon = PhotoImage(file="icon/drawChartIcon.png", master=self.window)
self.draw_chart_icon_resize = draw_chart_icon.subsample(10, 10)
oscilloscope_icon = PhotoImage(file="icon/oscilloscopeIcon.png", master=self.window)
self.oscilloscope_icon_resize = oscilloscope_icon.subsample(16, 16)
play_icon = PhotoImage(file = "icon/play.png", master=self.window)
self.play_icon_resize = play_icon.subsample(20, 20)
pause_icon = PhotoImage(file = "icon/pause.png", master=self.window)
self.pause_icon_resize = pause_icon.subsample(20, 20)
stop_icon = PhotoImage(file = "icon/stop.png", master=self.window)
self.stop_icon_resize = stop_icon.subsample(20, 20)
def notebook_gui_setup(self):
self.note.add(self.draw_static_chart_tab, text = "Draw chart", image=self.draw_chart_icon_resize, compound=LEFT)
self.note.add(self.oscilloscope_tab, text= "Oscilloscope", image=self.oscilloscope_icon_resize, compound=LEFT)
self.note.pack(fill=BOTH, expand=True)
def widget_setup(self):
self.button_start_plotting = Button(self.oscilloscope_tab, image = self.play_icon_resize)
self.button_start_plotting.place(x=5, y = 5)
self.button_pause_plotting = Button(self.oscilloscope_tab, image = self.pause_icon_resize)
self.button_pause_plotting.place(x=40, y = 5)
self.button_stop_plotting = Button(self.oscilloscope_tab, image = self.stop_icon_resize)
self.button_stop_plotting.place(x=75, y = 5)
self.button_plot_static_chart = Button(self.draw_static_chart_tab, image = self.play_icon_resize)
self.button_plot_static_chart.place(x=5, y = 5)
def figure_setup(self, firgure_plot_static_chart, canvas_static_chart):
self.firgure_plot_static_chart = firgure_plot_static_chart
self.canvas_static_chart = canvas_static_chart
def buttonFunctionSetup(self, animation_object, animation):
self.button_start_plotting['command'] = lambda: animation_object.oscilloscope_start_mode(animation_object, animation)
self.button_pause_plotting['command'] = lambda: animation_object.oscilloscope_pause_mode(animation_object, animation)
self.button_plot_static_chart['command'] = lambda: animation_object.plot_static_chart(
self.static_chart_value["x_values"],
self.static_chart_value["y_values"],
self.firgure_plot_static_chart,
self.canvas_static_chart
)
def tkinter_mainloop(self):
self.window.mainloop()
animationplot.py
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from tkinter import *
from tkinter import messagebox
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
class AnimationPlot:
def __init__(self):
# plotting starts below
self.plot_interval = 1 # Period at which the plot animation updates [ms]
self.xmin = 0
self.max_plot_length = 6000
self.xmax = self.max_plot_length
self.ymin = -30
self.ymax = 30
def figure_setup(self, static_chart_tab, oscilloscope_tab):
#Figure set up in static chart tab
figure_static_chart = plt.Figure(figsize=(20,8), dpi=100)
self.firgure_plot_static_chart = figure_static_chart.add_subplot(111)
self.canvas_static_chart = FigureCanvasTkAgg(figure_static_chart, static_chart_tab)
NavigationToolbar2Tk(self.canvas_static_chart, static_chart_tab)
self.canvas_static_chart.get_tk_widget().place(x=0, y = 50)
# Figure set up in Oscilloscope mode
self.oscilloscope_figure = plt.figure(figsize=(20, 8))
oscilloscope_axes = plt.axes(xlim=(self.xmin, self.xmax), ylim=(float(self.ymin - (self.ymax - self.ymin) / 10), float(self.ymax + (self.ymax - self.ymin) / 10)))
oscilloscope_axes.set_title('Smart Energy tracking Oscilloscope')
oscilloscope_axes.set_xlabel("Time (ms)")
oscilloscope_axes.set_ylabel("Current value")
self.line_label = 'Current value'
self.time_text = oscilloscope_axes.text(0.70, 0.95, '', transform=oscilloscope_axes.transAxes)
self.oscilloscope_lines = oscilloscope_axes.plot([], [], label=self.line_label)[0]
self.line_value_text = oscilloscope_axes.text(0.50, 0.90, '', transform=oscilloscope_axes.transAxes)
canvas = FigureCanvasTkAgg(self.oscilloscope_figure, master=oscilloscope_tab)
canvas.get_tk_widget().place(x=0, y = 50)
oscilloscope_toolbar = NavigationToolbar2Tk(canvas, oscilloscope_tab)
def start_oscilloscope(self, getDataFunction):
return animation.FuncAnimation(self.oscilloscope_figure, getDataFunction, fargs=(self.oscilloscope_lines, self.line_value_text, self.line_label, self.time_text), interval=self.plot_interval)
"""
"""
def oscilloscope_start_mode(self, animation_object, animation):
animation.event_source.start()
animation_object.anim_running = True
"""
"""
def oscilloscope_pause_mode(self, animation_object, animation):
animation.event_source.stop()
animation_object.anim_running = False
"""
This function will be called in GUISetup.py
Usecase: Alert user when no csv is imported, else: start drawing
Attributes
----------
static_chart_tab: Tab to store the static chart with imported csv data
canvas_static_chart: canvas_static_chart to handle drawing
"""
def plot_static_chart(self, x_val, y_val, static_chart_tab, canvas_static_chart):
if len(x_val) == 0:
messagebox.showinfo("No csv file has been imported", "You haven't imported a csv file or the csv file has been corrupted")
else:
static_chart_tab.cla()
static_chart_tab.plot(x_val,y_val)
canvas_static_chart.draw()
main.py
from serialhandler import SerialPlot
from guisetup import GUI
from animationplot import AnimationPlot
draw_static_chart_tab, oscilloscope_tab = None, None
#Set up UI for App
def gui_init(gui_setup_object):
global draw_static_chart_tab, oscilloscope_tab
gui_setup_object.image_init()
gui_setup_object.menu_setup()
gui_setup_object.tools_sub_menu_setup()
gui_setup_object.notebook_gui_setup()
draw_static_chart_tab = gui_setup_object.draw_static_chart_tab
oscilloscope_tab = gui_setup_object.oscilloscope_tab
def main():
# portName = 'COM4' # for windows users
portName = 'COM5' # for windows users
# portName = '/dev/ttyUSB0'
# portName = '/dev/ttyACM0'
# portName = '/dev/ttyACM1'
baud_rate = 115200
max_plot_length = 6000
data_num_bytes = 4 # number of bytes of 1 data point
gui_object = GUI()
gui_init(gui_object)
gui_object.window.title("Smart energy tracking system")
animation_object = AnimationPlot()
#Realtime is in oscilloscopeTab so it is put in gui_setup_object.oscilloscopeTab
animation_object.figure_setup(draw_static_chart_tab, oscilloscope_tab)
gui_object.widget_setup()
gui_object.figure_setup(animation_object.firgure_plot_static_chart, animation_object.canvas_static_chart)
s = SerialPlot(portName, baud_rate, max_plot_length, data_num_bytes, gui_object.window) # initializes all required variables
s.read_serial_start() # starts background thread
oscilloscope_animate = animation_object.start_oscilloscope(s.get_serial_data)
gui_object.buttonFunctionSetup(animation_object, oscilloscope_animate)
# gui_setup_object.exitSetup(animation_object)
gui_object.tkinter_mainloop()
s.close()
if __name__ == '__main__':
main()