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 4a91683..4e2bc11 100644 Binary files a/__pycache__/serial_plot.cpython-39.pyc and b/__pycache__/serial_plot.cpython-39.pyc differ diff --git a/__pycache__/serial_plotter.cpython-39.pyc b/__pycache__/serial_plotter.cpython-39.pyc new file mode 100644 index 0000000..f7c36a8 Binary files /dev/null and b/__pycache__/serial_plotter.cpython-39.pyc differ diff --git a/__pycache__/ui.cpython-39.pyc b/__pycache__/ui.cpython-39.pyc index dc99c7c..acac223 100644 Binary files a/__pycache__/ui.cpython-39.pyc and b/__pycache__/ui.cpython-39.pyc differ diff --git a/serial_plot.py b/serial_plot.py index 301516b..e88a9ac 100644 --- a/serial_plot.py +++ b/serial_plot.py @@ -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() +# 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 )