Redesign serial plot program

This commit is contained in:
Eric Yu 2022-08-08 15:51:36 -07:00
parent 6773d3c134
commit 6ad44be21c
9 changed files with 97 additions and 117 deletions

View File

@ -27,10 +27,9 @@ files. The main program is `test.py`, which calls `UI.py` to prompt for input. I
as it is generated from wxFormBuilder from `sealammonia.fbp`, which could be modified if any frontend feature need to change.
the main program will generate `settings.json` which saves all required parameters needed to calculate and graph and save the
sensor's values. It also creates a .csv file under `RecordedData` directory to save all recorded+calculated data.
`read_arduino.pyw` read raw data from Arduino board and convert it to the correct resistance, which could be plotted out
using `serial_plot.pyw`. Note that all .pyw are still Python files and should be correctly opened using any IDE/text edtiors
as .py file does, except not going to pop up a black terminal when running directly. `frontend.py` is the alternative version of the frontend writting using `tkinter`
and is not longer supported.
`read_arduino.py` read raw data from Arduino board and convert it to the correct resistance, which could be plotted out
using `serial_plotter.py`. `frontend.py` is the alternative version of the frontend writting using `tkinter`, and `serial_plot.py`
was an older version of `serial_plotter.py`. They are not longer supported.
## Issues
- Newer hardware will require 3.3V. ~~Currently can only use 5V as input voltage.~~ May need to change the algorithm in `read_arduino.py`.
@ -38,6 +37,7 @@ and is not longer supported.
- "plot" button is completely disabled, should be enabled whenever is not plotting but is reading data
- Matplotlib cannot really run in multithread, this might going to be an issue for packing the program into an executable
- Csv file is only created after you close the UI
- axis window size is not correct
## Todos & [Old Specs](https://docs.google.com/document/d/1Km2HZel7rILOvgHG5iXlUcXFx4Of1ot2I7Dbnq8aTwY/edit?usp=sharing):
- [ ] Fix Issues

Binary file not shown.

Binary file not shown.

View File

@ -4,6 +4,7 @@ import math, json, traceback
try:
import matplotlib
matplotlib.use("WXAgg") # to force use one of the matplot's UI backend instead of an IDE's choice
except ImportError:
traceback.print_exc()
@ -35,35 +36,34 @@ def plotter():
if len(line) >= sensors:
i = 1
timeStamps.append(line[0])
# idk what this condition does.... Nathan wrote this without saying much
if windowsize != 0 and len(timeStamps) > math.ceil(windowsize / delay):
# get rid of the zeroth value if it exceeds pre-defined window size
if windowsize != 0 and len(timeStamps) > windowsize:
timeStamps.pop(0)
val_lists = []
while i <= sensors:
if not line[i]: # pass on invalid values
continue
sensorsData[i - 1].append(float(line[i])) # cast it to float so the y-axis will not jump around
# idk what this condition does.... Nathan wrote this without saying much
if windowsize != 0 and len(sensorsData[i - 1]) > math.ceil(windowsize / delay):
# get rid of the zeroth value if it exceeds pre-defined window size
if windowsize != 0 and len(sensorsData[i - 1]) > windowsize:
sensorsData[i - 1].pop(0)
val_lists.append(sensorsData[i - 1])
i += 1
for j in range(len(val_lists)):
axs.plot(val_lists[j], color=colors[j],
axs.plot(timeStamps, val_lists[j], color=colors[j],
label=f'sensor {j + 1}') # TODO: display sensor number to the actual arduino's port &
# axs.annotate('%0.2f' % val_lists[j][-1], xy=(1, val_lists[j][-1]), xytext=(8, 0),
# xycoords=('axes fraction', 'data'), textcoords='offset points')
# Acknowledgement: https://stackoverflow.com/a/13589144
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
axs.legend(by_label.values(), by_label.keys(), loc='best')
print(by_label.items())
ani = animate.FuncAnimation(plt.gcf(), func=animation) # , interval=delay * 500)
return animate.FuncAnimation(plt.gcf(), func=animation) # , interval=delay * 500)
# ani.save("plot.gif")
# plt.ion()
plt.show(block=True)
# plt.show(block=True)
# plotter()

View File

@ -1,69 +0,0 @@
import matplotlib.pyplot as plt
import matplotlib.animation as animate
import math, json, traceback
try:
import matplotlib
matplotlib.use("WXAgg") # to force use one of the matplot's UI backend instead of an IDE's choice
except ImportError:
traceback.print_exc()
def plotter():
a = json.load(open('settings.json', 'r'))
sensors = len(a['sensor_ports'])
windowsize = a['winSize']
delay = a["delay"] / 1000
file = open(a["file_name"], "r")
colors = ['blue', 'orange', 'green', 'yellow']
fig, axs = plt.subplots(1, 1)
timeStamps = []
sensorsData = []
i = 0
while i <= sensors:
sensorsData.append([])
i = i + 1
def animation(t):
next_line = file.readline()
if next_line:
line = next_line.split(',')
if len(line) >= sensors:
i = 1
timeStamps.append(line[0])
# idk what this condition does.... Nathan wrote this without saying much
if windowsize != 0 and len(timeStamps) > math.ceil(windowsize / delay):
timeStamps.pop(0)
val_lists = []
while i <= sensors:
if not line[i]: # pass on invalid values
continue
sensorsData[i - 1].append(float(line[i])) # cast it to float so the y-axis will not jump around
# idk what this condition does.... Nathan wrote this without saying much
if windowsize != 0 and len(sensorsData[i - 1]) > math.ceil(windowsize / delay):
sensorsData[i - 1].pop(0)
val_lists.append(sensorsData[i - 1])
i += 1
for j in range(len(val_lists)):
axs.plot(val_lists[j], color=colors[j],
label=f'sensor {j + 1}') # TODO: display sensor number to the actual arduino's port &
# axs.annotate('%0.2f' % val_lists[j][-1], xy=(1, val_lists[j][-1]), xytext=(8, 0),
# xycoords=('axes fraction', 'data'), textcoords='offset points')
# Acknowledgement: https://stackoverflow.com/a/13589144
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
axs.legend(by_label.values(), by_label.keys(), loc='best')
ani = animate.FuncAnimation(plt.gcf(), func=animation) # , interval=delay * 500)
# ani.save("plot.gif")
# plt.ion()
plt.show(block=True)
plotter()

70
serial_plotter.py Normal file
View File

@ -0,0 +1,70 @@
from datetime import datetime
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import os, json, traceback, ui, wx
matplotlib.use("WXAgg")
class SerialPlotter:
def __init__(self) -> None:
self.settings = json.load(open('settings.json', 'r'))
self.sensors = len(self.settings['sensor_ports'])
self.windowsize = self.settings['winSize']
self.delay = self.settings["delay"] / 1000
self.colors = ['blue', 'orange', 'green', 'yellow']
self.fig, self.axs = plt.subplots(1, 1, figsize=(7,5)) # TODO: make the figure size an UI option and pass into the settings.json
self.fig.canvas.mpl_connect('close_event', self._close_event)
self.timeStamps = {}
self.sensorsData = {}
for i in range(self.sensors):
self.timeStamps[i] = ['']#*self.windowsize
self.sensorsData[i] = [0]#*self.windowsize
plt.tight_layout()
def _close_event(self, event) -> None:
""" Actiions need to be executed when the graph has closed. Start a new .csv file to get read for new graph and bring back the UI, if hidden """
file = self.settings["file_name"]
wx.MessageBox(f"File has saved as {os.path.split(file)[1]} under {os.path.split(file)[0]} directory!\n")
def animation(self, t:int) -> None:
""" render a from of the animated graph """
# print("hi")
try:
plt.cla() # clear previous frame
file = open(self.settings["file_name"], "r")
ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in file])
if len(ndata) > 0:
row = ndata[-1]
i = 0
while i < self.sensors:
# shift all data left by 1 index, pop out the leftmost value
if self.windowsize > 0 and len(self.timeStamps[i]) > self.windowsize: # TODO: make sure the two lists have the same size
self.timeStamps[i].pop(0)
self.sensorsData[i].pop(0)
self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S'))
self.sensorsData[i].append(row[i])
# plot a line
# TODO: round the number to 1-2- decimal places
self.axs.plot(self.timeStamps[i] ,self.sensorsData[i], color=self.colors[i], label=f'sensor {i + 1}, latest: {np.int32(self.sensorsData[i][-1])}')
i += 1
# Acknowledgement: https://stackoverflow.com/a/13589144
handles, labels = self.axs.get_legend_handles_labels()
by_label = dict(zip(labels, handles))
self.axs.legend(by_label.values(), by_label.keys(), loc='best')
except:
traceback.print_exc()
def plotting(self) -> None:
ani = FuncAnimation(self.fig, self.animation, blit=False)
return ani

45
test.py
View File

@ -1,10 +1,9 @@
from ui import Frame
from datetime import datetime
from threading import *
from read_arduino import *
import os, json, sys, wx, subprocess, warnings
from serial_plotter import *
import os, json, wx, warnings
import serial.tools.list_ports
from serial_plot import *
import numpy as np
warnings.filterwarnings("ignore", category=DeprecationWarning)
@ -12,6 +11,8 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
HEIGHT = 800
WIDTH = 800
global ani
def main():
#################### USER INPUTS ###########################
@ -67,52 +68,31 @@ def get_devices():
frame.dev_list.AppendItems(ports)
# for scheduler maybe?
def task1():
print("run task 2")
run_t2 = ["start", os.path.join(sys.exec_prefix, 'pythonw'), "serial_plot.pyw"]
out2 = subprocess.Popen(run_t2, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
running = True
init = 0
while running:
cline = out2.stdout.readline()
print(cline.decode())
if not cline and init > 0:
print(cline.decode())
running = False
init += 1
if out2.returncode:
print('Something went wrong:', out2.returncode)
def runPlot(e):
global ani
plotter = SerialPlotter()
ani = plotter.plotting()
plotter.fig.show()
def run(e):
file = main()
save_diag = f"File has saved as {os.path.split(file)[1]} under {os.path.split(file)[0]} directory!\n"
t1 = Thread(target=read, args=())
t1.setDaemon(True)
t1.start()
t2 = Thread(target=task1, args=())
t2.setDaemon(True)
t2.start()
runPlot(e)
frame.btLaunch.Enable(False)
frame.plot_but.Enable(True) # this might have some problem ... what happen if user spamming the plot button?
if not frame.show_msg.GetValue():
frame.Hide()
wx.MessageBox(save_diag, "Info", style=wx.ICON_INFORMATION)
def runPlot(e):
plotter()
wx.MessageBox("Your data is being plotted", "Info", style=wx.ICON_INFORMATION)
if __name__ == '__main__':
app = wx.App()
frame = Frame(None)
get_devices()
frame.SetTitle("Cease your resistance! - alpha 0.1.0")
frame.SetTitle("Cease your resistance! - alpha 0.1.1")
frame.btLaunch.Bind(wx.EVT_BUTTON, run)
frame.plot_but.Bind(wx.EVT_BUTTON,
runPlot) # There is one problem with this approch: what happen if user spamming the plot button?
@ -128,5 +108,4 @@ if __name__ == '__main__':
frame.m_textCtrl26.SetValue(str(settings["winSize"]))
frame.Show()
frame.m_textCtrl26.SetValue("480")
app.MainLoop()

2
ui.py
View File

@ -92,7 +92,7 @@ class Frame ( wx.Frame ):
self.sizer_prompt.Wrap( -1 )
gbSizer8.Add( self.sizer_prompt, wx.GBPosition( 2, 0 ), wx.GBSpan( 1, 1 ), wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT, 5 )
self.m_textCtrl26 = wx.TextCtrl( v_entre.GetStaticBox(), wx.ID_ANY, u"480", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_textCtrl26 = wx.TextCtrl( v_entre.GetStaticBox(), wx.ID_ANY, u"20", wx.DefaultPosition, wx.DefaultSize, 0 )
self.m_textCtrl26.SetToolTipString( u"Window size for the plot window. If want infinite/maximum size, type 0" )
gbSizer8.Add( self.m_textCtrl26, wx.GBPosition( 2, 1 ), wx.GBSpan( 1, 1 ), wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER_HORIZONTAL|wx.TOP|wx.RIGHT|wx.LEFT, 5 )