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.
|
||||
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.
Binary file not shown.
|
@ -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()
|
||||
|
|
|
@ -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 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
2
ui.py
|
@ -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 )
|
||||
|
|
Loading…
Reference in New Issue