Redesign serial plot program
This commit is contained in:
parent
6773d3c134
commit
6ad44be21c
|
@ -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.
|
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
|
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.
|
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
|
`read_arduino.py` 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
|
using `serial_plotter.py`. `frontend.py` is the alternative version of the frontend writting using `tkinter`, and `serial_plot.py`
|
||||||
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`
|
was an older version of `serial_plotter.py`. They are not longer supported.
|
||||||
and is not longer supported.
|
|
||||||
|
|
||||||
## Issues
|
## 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`.
|
- 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
|
- "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
|
- 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
|
- 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):
|
## Todos & [Old Specs](https://docs.google.com/document/d/1Km2HZel7rILOvgHG5iXlUcXFx4Of1ot2I7Dbnq8aTwY/edit?usp=sharing):
|
||||||
- [ ] Fix Issues
|
- [ ] Fix Issues
|
||||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -4,6 +4,7 @@ import math, json, traceback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import matplotlib
|
import matplotlib
|
||||||
|
|
||||||
matplotlib.use("WXAgg") # to force use one of the matplot's UI backend instead of an IDE's choice
|
matplotlib.use("WXAgg") # to force use one of the matplot's UI backend instead of an IDE's choice
|
||||||
except ImportError:
|
except ImportError:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -35,35 +36,34 @@ def plotter():
|
||||||
if len(line) >= sensors:
|
if len(line) >= sensors:
|
||||||
i = 1
|
i = 1
|
||||||
timeStamps.append(line[0])
|
timeStamps.append(line[0])
|
||||||
# idk what this condition does.... Nathan wrote this without saying much
|
# get rid of the zeroth value if it exceeds pre-defined window size
|
||||||
if windowsize != 0 and len(timeStamps) > math.ceil(windowsize / delay):
|
if windowsize != 0 and len(timeStamps) > windowsize:
|
||||||
timeStamps.pop(0)
|
timeStamps.pop(0)
|
||||||
val_lists = []
|
val_lists = []
|
||||||
while i <= sensors:
|
while i <= sensors:
|
||||||
if not line[i]: # pass on invalid values
|
if not line[i]: # pass on invalid values
|
||||||
continue
|
continue
|
||||||
sensorsData[i - 1].append(float(line[i])) # cast it to float so the y-axis will not jump around
|
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
|
# get rid of the zeroth value if it exceeds pre-defined window size
|
||||||
if windowsize != 0 and len(sensorsData[i - 1]) > math.ceil(windowsize / delay):
|
if windowsize != 0 and len(sensorsData[i - 1]) > windowsize:
|
||||||
sensorsData[i - 1].pop(0)
|
sensorsData[i - 1].pop(0)
|
||||||
val_lists.append(sensorsData[i - 1])
|
val_lists.append(sensorsData[i - 1])
|
||||||
i += 1
|
i += 1
|
||||||
for j in range(len(val_lists)):
|
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 &
|
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),
|
# axs.annotate('%0.2f' % val_lists[j][-1], xy=(1, val_lists[j][-1]), xytext=(8, 0),
|
||||||
# xycoords=('axes fraction', 'data'), textcoords='offset points')
|
# xycoords=('axes fraction', 'data'), textcoords='offset points')
|
||||||
|
|
||||||
|
|
||||||
# Acknowledgement: https://stackoverflow.com/a/13589144
|
# Acknowledgement: https://stackoverflow.com/a/13589144
|
||||||
handles, labels = plt.gca().get_legend_handles_labels()
|
handles, labels = plt.gca().get_legend_handles_labels()
|
||||||
by_label = dict(zip(labels, handles))
|
by_label = dict(zip(labels, handles))
|
||||||
axs.legend(by_label.values(), by_label.keys(), loc='best')
|
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")
|
# ani.save("plot.gif")
|
||||||
# plt.ion()
|
# plt.ion()
|
||||||
plt.show(block=True)
|
# plt.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
# plotter()
|
# plotter()
|
||||||
|
|
|
@ -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()
|
|
|
@ -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
45
test.py
|
@ -1,10 +1,9 @@
|
||||||
from ui import Frame
|
from ui import Frame
|
||||||
from datetime import datetime
|
|
||||||
from threading import *
|
from threading import *
|
||||||
from read_arduino 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
|
import serial.tools.list_ports
|
||||||
from serial_plot import *
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
@ -12,6 +11,8 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
HEIGHT = 800
|
HEIGHT = 800
|
||||||
WIDTH = 800
|
WIDTH = 800
|
||||||
|
|
||||||
|
global ani
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
#################### USER INPUTS ###########################
|
#################### USER INPUTS ###########################
|
||||||
|
@ -67,52 +68,31 @@ def get_devices():
|
||||||
frame.dev_list.AppendItems(ports)
|
frame.dev_list.AppendItems(ports)
|
||||||
|
|
||||||
|
|
||||||
# for scheduler maybe?
|
def runPlot(e):
|
||||||
def task1():
|
global ani
|
||||||
print("run task 2")
|
plotter = SerialPlotter()
|
||||||
run_t2 = ["start", os.path.join(sys.exec_prefix, 'pythonw'), "serial_plot.pyw"]
|
ani = plotter.plotting()
|
||||||
out2 = subprocess.Popen(run_t2, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
|
plotter.fig.show()
|
||||||
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 run(e):
|
def run(e):
|
||||||
file = main()
|
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 = Thread(target=read, args=())
|
||||||
t1.setDaemon(True)
|
t1.setDaemon(True)
|
||||||
t1.start()
|
t1.start()
|
||||||
|
|
||||||
t2 = Thread(target=task1, args=())
|
runPlot(e)
|
||||||
t2.setDaemon(True)
|
frame.btLaunch.Enable(False)
|
||||||
t2.start()
|
|
||||||
frame.plot_but.Enable(True) # this might have some problem ... what happen if user spamming the plot button?
|
frame.plot_but.Enable(True) # this might have some problem ... what happen if user spamming the plot button?
|
||||||
if not frame.show_msg.GetValue():
|
if not frame.show_msg.GetValue():
|
||||||
frame.Hide()
|
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__':
|
if __name__ == '__main__':
|
||||||
app = wx.App()
|
app = wx.App()
|
||||||
frame = Frame(None)
|
frame = Frame(None)
|
||||||
get_devices()
|
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.btLaunch.Bind(wx.EVT_BUTTON, run)
|
||||||
frame.plot_but.Bind(wx.EVT_BUTTON,
|
frame.plot_but.Bind(wx.EVT_BUTTON,
|
||||||
runPlot) # There is one problem with this approch: what happen if user spamming the plot 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.m_textCtrl26.SetValue(str(settings["winSize"]))
|
||||||
|
|
||||||
frame.Show()
|
frame.Show()
|
||||||
frame.m_textCtrl26.SetValue("480")
|
|
||||||
app.MainLoop()
|
app.MainLoop()
|
||||||
|
|
2
ui.py
2
ui.py
|
@ -92,7 +92,7 @@ class Frame ( wx.Frame ):
|
||||||
self.sizer_prompt.Wrap( -1 )
|
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 )
|
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" )
|
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 )
|
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 )
|
||||||
|
|
Loading…
Reference in New Issue