From 6ad44be21cfc2990433d0c6ef17c0d6acf6bdf02 Mon Sep 17 00:00:00 2001 From: Eric Yu Date: Mon, 8 Aug 2022 15:51:36 -0700 Subject: [PATCH] Redesign serial plot program --- README.md | 8 +-- __pycache__/serial_plot.cpython-39.pyc | Bin 1723 -> 1681 bytes __pycache__/serial_plotter.cpython-39.pyc | Bin 0 -> 2954 bytes __pycache__/ui.cpython-39.pyc | Bin 4515 -> 4514 bytes serial_plot.py | 20 +++---- serial_plot.pyw | 69 --------------------- serial_plotter.py | 70 ++++++++++++++++++++++ test.py | 45 ++++---------- ui.py | 2 +- 9 files changed, 97 insertions(+), 117 deletions(-) create mode 100644 __pycache__/serial_plotter.cpython-39.pyc delete mode 100644 serial_plot.pyw create mode 100644 serial_plotter.py diff --git a/README.md b/README.md index 00a7af4..0890868 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/__pycache__/serial_plot.cpython-39.pyc b/__pycache__/serial_plot.cpython-39.pyc index 4a9168396d1fd626fba75fe198c524d003f95375..4e2bc115d96594da3b04b50029a370543adcd8dd 100644 GIT binary patch delta 772 zcmYLG&ui2`82!Ham2A3+s}|aAr9VK?swgO>;<<+&yom*i6}C>hw!7`N$y5X<7F{LT zL&1`x2#F_eUV8K9!Lv6Z_)mBj^h;f7hIy0k%{OlzZ@&0PzTfhF59r%}{cCOi$p7p) zRB)Qnr=xFE2i$@dO&BOzWfQcTgcuE@%Atz&*?_e{yB~3YM&wf0bgf{ISj+~{hJmCh zv=?*BQ{KVwWh!#jh5`{R*R?T-`QBOr2_`JziAZFkHXJ>o>u=mx7-!3EkZ7#{E3|*e z?!#P=y~P&?F4;R@S-gh;Da)ZCQx@N0xMVR$jRnSxOtL=GwX)(hi+L(@ZkguI69cKz zyu-s~!%|6osw2W>>JXY&_Sz7Oq3Q3ISvnJS_f}HkG&>lFCgj}6e;a+e4q+&FOmZhp zRnJiN_>}oCZ$-{e*$D6%g4*vCa!y%Nh5wwzR2VJZI<2_MjtSgF|J*0LHIf7|WeYPO0*+agX zedUk+QtpPEJEoMIW_>%G7K?$i!duUyQa$N)dtsD46Vct7D*jspfG#TZFhGF;8>K%% zjEf8>*abXC!@~w){g0~})HR;X%1e`TUMaSs-cBjHy_POTZ+Cqs93{1IBJ0ST3v;r> zR;Y+5`Nn2DyjX2rui$D0mn*nNP&&Kao-ym8PenA9{gBgQd?uXFe#=>$$fneEy)s#f Y?Us2O-pH2J9h}czsoM?C!Y!XozhA5;i>; z$-}+S_t5?1 z*ragU{^!H5^Lru$E$T3|=#+KPY8HkpV5C{3v6u~c8?^lahZu-V>XI(y>@kaQ2yGZD z+EP0a3t1|<=)Fm0Mz$OgLZ(Y+A!3780tqH8;fY9Oq7rM%)@5CJYezh=&hOZiwv8a3nN>phErrwZu12$1p&a)>a*1N82Mn4!iTqXL`7NFcJX zYZmcTWt>2GeTRk5wKLn<91RjWwYe#3U7mAmF5L{8zK?%aYH{s7SioK|HVM3 zk|5bM)FC7C-;S6HBm7BxM2Y{!{8!-U#P_#QC^CLx9Z!uatU|;GIwCVZXwc@7@YfcKiC-lFXrn?0t0=I_ z=IKw6#YToRYzZf6Ias4r`Dt4z3O zVROW{I&JR)rS~qAAc)MX&OW%LXhsqxK^Yri;*DiOi4H*oL5LE8aSWI+Oax8_GzPQV&E>3S zXI4GEd@PnHIzSMBfD`0q8`#Y)*ZiEm<`N!*+;WM(>Lulsj117#UDc1OuB!fO(v_74 z!}s&Q{JUFkG4@YlmOmHFFVLzJ5Wxh`*^tGI)7Z&*?AYFoUE6y@KMw4io7aY69A2%f z$8|g3h#R(V#!cI|;+E}K;uY!6+QOT2&VJ2=FM`jR2+o~&RXBUBQ@ezA*5M`;sg^n$ z%3UxkJL97FtjLC`&dS30pZ@Myzu%*k%ipUT>_946%msthTsU8G;R^3_54JBj((1*Y z^ugD@azrTV;Df8#hG>GT2@ZQ&W@S&REX_a8ORc3MYXT+pSQS0Kq};*SNBb38^$!ru zW?WnC=gyo@S>kGM$_B1*H`zJyzU914hH+rW{w6$em-t$riQ3<2RjBKO2JC}{qH*Ew zJCHTeTG-JXtiY4t2CH+2vy67GS$adSL+YRN=j^k4XI!-UIBT#r$k(mz7N9ku0=f$}Zh7 zo>lJUWh#tUj&N3ZZI^L#u(gFzm1)2pQ6AG-iqhKzy)rLVW!zD&v1{BNA#Pxy(75SI z6$ibvI7+LyIUMCluPh4L)25Xy+tHx3+tcZARGH?Lrst_njc@N4w{+G&)Culij&;W| zUM2Gb!?V9L^+VY3-$&0M@Aor(IPUI$B2)4Hp7r4SpU5h!bXxRewO`qsO2{}Q)%NJr zgh`SWnNAYAMs){-@pZn&TYSxFIJcd3C*Xn82H$ojH($%i?dz>Q%EIf9UPFjxGx8}0 z$N;?RO?KvJK68Yl-GMjrr^rsq$uF7kmL$kUNP@3@dZ(g>>Qk@c$Bj^IqCqZHeYqwQXG6dh!_r2T!VBLyEt z2c@#Ji@j+fqOL+7M%}dc!XEnD-#m!2gXl050%}a1PDqM$C@1SXbQr0t(j(Y`fm+3V zC$(r?2&p2QR+CmFGKFB5>h$f#ZYNN;u*NvYC#La{tg5sxe_5Uwr>u-OO7)@ft5Kfm zj;nq^>-D!{_mqE&~WiDsiE@D9G< zlMe_(=yhtmdLRd4C!BK!@k87!qOqy-9B4A(`vykQnx2ECj_MCxk&Y5V5l?#`%Pg$|S;X`=w&g?FE z^Y^T99x~BJPOnb3kbX8v(&#{y!>By4*_PT`gJfFdqv{|uiV*BOpihz9L#uua(uX5H zd*_U6HsDiwWVi|yl|ML}CBkXF@h#38KTo?d2mV8jYq&Uh zw{uV3gZ|j<<*B-fk$q|E5k|&eTX4vm6z1k4wT(9F0+c?=-e z>VQyLeV=3=;ikGvLq99vn|KpIlH{_FZOI|fBbOEM8K&Viiwp?wPO?!PELND{C`G-( z)C+l9nYvbKPZH3W`Us$-6M53>)a|=hKY;;?(V zQ2GWynEET<)5hvu5axP31iWwXHhPb@ocrL`oI8BO-Qf+l4f%Z@@X4+J6UFZ&;;X+8 za=E4#N$=YJfn2y}NJk`{ySvljF{-r%HmVTnK9OCJjVyAK8bZdk9m}FE$mx#q!x*cI; zMI|ld?VD|X?Y<&uXKj%velZ@7P8Vs`z>gwXlV@FXCjE zks^>P0= 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() +# plotter() diff --git a/serial_plot.pyw b/serial_plot.pyw deleted file mode 100644 index fa498eb..0000000 --- a/serial_plot.pyw +++ /dev/null @@ -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() diff --git a/serial_plotter.py b/serial_plotter.py new file mode 100644 index 0000000..cea419a --- /dev/null +++ b/serial_plotter.py @@ -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 + diff --git a/test.py b/test.py index e5f0964..42341ef 100644 --- a/test.py +++ b/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() diff --git a/ui.py b/ui.py index 6cac331..18b7a74 100644 --- a/ui.py +++ b/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 )