diff --git a/serial_plotter.py b/serial_plotter.py index 0ec953e..89546ab 100644 --- a/serial_plotter.py +++ b/serial_plotter.py @@ -26,9 +26,40 @@ class SerialPlotter: self.colors = ['blue', 'orange', 'green', 'yellow'] # TODO: make the figure size an UI option and pass into the settings.json - self.fig, self.axs = plt.subplots(1, 1, figsize=(9, 6)) - - self.fig.canvas.mpl_connect('close_event', self.event_close) + #self.fig, self.axs = plt.subplots(figsize=(9, 6)) + # self.fig_1 = plt.figure(1) + # self.axs_1 = plt.axes() + + # self.fig_2 = plt.figure(2) + # self.axs_2 = plt.axes() + if (self.sensors == 1) : + self.fig_1, self.axs_1 = plt.subplots() + + self.fig_1.canvas.mpl_connect('close_event', self.event_close) + elif (self.sensors == 2) : + self.fig_1, self.axs_1 = plt.subplots() + self.fig_2, self.axs_2 = plt.subplots() + + self.fig_1.canvas.mpl_connect('close_event', self.event_close) + self.fig_2.canvas.mpl_connect('close_event', self.event_close) + elif (self.sensors == 3) : + self.fig_1, self.axs_1 = plt.subplots() + self.fig_2, self.axs_2 = plt.subplots() + self.fig_3, self.axs_3 = plt.subplots() + + self.fig_1.canvas.mpl_connect('close_event', self.event_close) + self.fig_2.canvas.mpl_connect('close_event', self.event_close) + self.fig_3.canvas.mpl_connect('close_event', self.event_close) + elif (self.sensors == 4) : + self.fig_1, self.axs_1 = plt.subplots() + self.fig_2, self.axs_2 = plt.subplots() + self.fig_3, self.axs_3 = plt.subplots() + self.fig_4, self.axs_4 = plt.subplots() + + self.fig_1.canvas.mpl_connect('close_event', self.event_close) + self.fig_2.canvas.mpl_connect('close_event', self.event_close) + self.fig_3.canvas.mpl_connect('close_event', self.event_close) + self.fig_4.canvas.mpl_connect('close_event', self.event_close) self.timeStamps = {} self.sensorsData = {} @@ -40,7 +71,7 @@ class SerialPlotter: def event_close(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 + Actions 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 exist """ file = self.settings["file_name"] @@ -48,7 +79,7 @@ class SerialPlotter: if self.parent: self.parent.Show() - def animation(self, t: int) -> None: + def animation_1(self, t) -> None: """ render a frame of the animated graph. """ @@ -61,44 +92,222 @@ class SerialPlotter: 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 windowsize is not 0 (infinite) - # TODO: make sure the two lists have the same size - # if self.windowsize > 0 and len(self.timeStamps[i]) > self.windowsize - if 0 < self.windowsize < len(self.timeStamps[i]): - self.timeStamps[i].pop(0) - self.sensorsData[i].pop(0) + if 0 < self.windowsize < len(self.timeStamps[i]): + self.timeStamps[i].pop(0) + self.sensorsData[i].pop(0) # self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1 # version 2, if we decide to go with this one change the list type to list[int] from list[str] - self.timeStamps[i].append(str(self.timeElapsed)) + self.timeStamps[i].append(str(self.timeElapsed)) - self.sensorsData[i].append(row[i]) + self.sensorsData[i].append(row[i]) # plot a line # round the number to scientific notation - self.axs.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i], + self.axs_1.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i], label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$') - # TODO: make the font size an UI option from the settings.json # Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough) - plt.xticks(fontsize = max(self.fontSize, 10)) - plt.yticks(fontsize = max(self.fontSize, 10)) + #plt.xticks(fontsize = max(self.fontSize, 10)) + #plt.yticks(fontsize = max(self.fontSize, 10)) + self.axs_1.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10)) # make the label of two axes large enough - self.axs.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10)) - self.axs.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10)) - i += 1 + self.axs_1.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10)) + self.axs_1.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10)) + #i += 1 self.timeElapsed += 1 # increment time # Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?) - handles, labels = self.axs.get_legend_handles_labels() + #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', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough + + # handles, labels = self.axs_1.get_legend_handles_labels() + # by_label = dict(zip(labels, handles)) + # self.axs_1.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + + handles, labels = plt.gca().get_legend_handles_labels() by_label = dict(zip(labels, handles)) - self.axs.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough + plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) except: traceback.print_exc() - def plotting(self) -> FuncAnimation: + def animation_2(self, t: int) -> None: + """ + render a frame of the animated graph. + """ + # Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination + try: + plt.cla() # clear previous frame + # read the last line from the .csv file, the data start from the second column so omit index #0 + file = open(self.settings["file_name"], "r") + ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers + if len(ndata) > 0: + row = ndata[-1] + i = 1 + if 0 < self.windowsize < len(self.timeStamps[i]): + self.timeStamps[i].pop(0) + self.sensorsData[i].pop(0) + + # self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1 + + # version 2, if we decide to go with this one change the list type to list[int] from list[str] + self.timeStamps[i].append(str(self.timeElapsed)) + + self.sensorsData[i].append(row[i]) + # plot a line + # round the number to scientific notation + self.axs_2.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i], + label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$') + # TODO: make the font size an UI option from the settings.json + # Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough) + #self.axs_2.set_xticks(fontsize = max(self.fontSize, 10)) + #self.axs_2.set_yticks(fontsize = max(self.fontSize, 10)) + self.axs_2.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10)) + + # make the label of two axes large enough + self.axs_2.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10)) + self.axs_2.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10)) + #i += 1 + self.timeElapsed += 1 # increment time + + # Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?) + #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', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough + + # handles, labels = self.axs_2.get_legend_handles_labels() + # by_label = dict(zip(labels, handles)) + # self.axs_2.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) + plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + except: + traceback.print_exc() + + def animation_3(self, t: int) -> None: + """ + render a frame of the animated graph. + """ + # Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination + try: + plt.cla() # clear previous frame + # read the last line from the .csv file, the data start from the second column so omit index #0 + file = open(self.settings["file_name"], "r") + ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers + if len(ndata) > 0: + row = ndata[-1] + i = 2 + if 0 < self.windowsize < len(self.timeStamps[i]): + self.timeStamps[i].pop(0) + self.sensorsData[i].pop(0) + + # self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1 + + # version 2, if we decide to go with this one change the list type to list[int] from list[str] + self.timeStamps[i].append(str(self.timeElapsed)) + + self.sensorsData[i].append(row[i]) + # plot a line + # round the number to scientific notation + self.axs_3.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i], + label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$') + # TODO: make the font size an UI option from the settings.json + # Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough) + #self.axs_2.set_xticks(fontsize = max(self.fontSize, 10)) + #self.axs_2.set_yticks(fontsize = max(self.fontSize, 10)) + self.axs_3.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10)) + + # make the label of two axes large enough + self.axs_3.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10)) + self.axs_3.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10)) + #i += 1 + self.timeElapsed += 1 # increment time + + # Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?) + #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', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough + + # handles, labels = self.axs_2.get_legend_handles_labels() + # by_label = dict(zip(labels, handles)) + # self.axs_2.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) + plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + except: + traceback.print_exc() + + def animation_4(self, t: int) -> None: + """ + render a frame of the animated graph. + """ + # Some of them copied from the old code, so even Eric may not 100% know what they do. ChatGPT may give a better explaination + try: + plt.cla() # clear previous frame + # read the last line from the .csv file, the data start from the second column so omit index #0 + file = open(self.settings["file_name"], "r") + ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in islice(file, 1, None)])#read from the second row of the file ignoring headers + if len(ndata) > 0: + row = ndata[-1] + i = 3 + if 0 < self.windowsize < len(self.timeStamps[i]): + self.timeStamps[i].pop(0) + self.sensorsData[i].pop(0) + + # self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # version 1 + + # version 2, if we decide to go with this one change the list type to list[int] from list[str] + self.timeStamps[i].append(str(self.timeElapsed)) + + self.sensorsData[i].append(row[i]) + # plot a line + # round the number to scientific notation + self.axs_4.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i], + label=f'sensor {i + 1}, latest: {np.format_float_scientific(self.sensorsData[i][-1], precision = 2)} $\Omega$') + # TODO: make the font size an UI option from the settings.json + # Make the font size of values placed on x-axis and y-aixs equal to 16 (large enough) + #self.axs_2.set_xticks(fontsize = max(self.fontSize, 10)) + #self.axs_2.set_yticks(fontsize = max(self.fontSize, 10)) + self.axs_4.tick_params(axis='both', which='both', labelsize=max(self.fontSize, 10)) + + # make the label of two axes large enough + self.axs_4.set_xlabel('Time (seconds)', fontsize = max(self.fontSize, 10)) + self.axs_4.set_ylabel(u'Resistance ($\Omega$)', fontsize = max(self.fontSize, 10)) + #i += 1 + self.timeElapsed += 1 # increment time + + # Acknowledgement: https://stackoverflow.com/a/13589144 (can you imagine how long it took me to find this before ChatGPT?) + #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', fontsize = max(self.fontSize, 10)) # Make the legend on graph large enough + + # handles, labels = self.axs_2.get_legend_handles_labels() + # by_label = dict(zip(labels, handles)) + # self.axs_2.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + handles, labels = plt.gca().get_legend_handles_labels() + by_label = dict(zip(labels, handles)) + plt.legend(by_label.values(), by_label.keys(), loc='best', fontsize = max(self.fontSize, 10)) + except: + traceback.print_exc() + + def plotting_1(self) -> FuncAnimation: """ animate the dynamic plot """ - ani = FuncAnimation(self.fig, self.animation, blit=False, interval=self.delay * 1000, save_count=60) - return ani + ani_1 = FuncAnimation(self.fig_1, self.animation_1, blit=False, interval=self.delay * 1000, save_count=60) + return ani_1 + + def plotting_2(self) -> FuncAnimation: + """ animate the dynamic plot """ + ani_2 = FuncAnimation(self.fig_2, self.animation_2, blit=False, interval=self.delay * 1000, save_count=60) + return ani_2 + + def plotting_3(self) -> FuncAnimation: + """ animate the dynamic plot """ + ani_3 = FuncAnimation(self.fig_3, self.animation_3, blit=False, interval=self.delay * 1000, save_count=60) + return ani_3 + + def plotting_4(self) -> FuncAnimation: + """ animate the dynamic plot """ + ani_4 = FuncAnimation(self.fig_3, self.animation_3, blit=False, interval=self.delay * 1000, save_count=60) + return ani_4 \ No newline at end of file diff --git a/test.py b/test.py index 909e621..249a491 100644 --- a/test.py +++ b/test.py @@ -5,6 +5,7 @@ from multiprocessing import * import os, sys, json, wx, importlib, warnings import serial.tools.list_ports import numpy as np +import matplotlib.pyplot as plt warnings.filterwarnings("ignore", category=DeprecationWarning) @@ -18,7 +19,7 @@ if '_PYIBoot_SPLASH' in os.environ and importlib.util.find_spec("pyi_splash"): HEIGHT = 800 WIDTH = 800 -global ani, t1 +#global ani, t1 # TODO: add the option to change the font size of the graph def main(): @@ -76,6 +77,8 @@ def gen_settings(resistors, input_voltage, port, filename, window_size, font_siz settings["file_name"] = filename settings["delay"] = delay settings["font_size"] = frame.fontSize.GetValue() + global sensors + sensors = len(settings['sensor_ports']) # TODO: get the input voltage from the UI and save it to the settings.json open(name, 'w').write(json.dumps(settings, indent=4)) open(filename, "a", newline="", encoding="utf-8") @@ -86,7 +89,7 @@ def run(e): run the read_arduino.py and Serial Plotter in parallel, but the plotter is in the main thread and other one can be in a separate thread, since matplot's FuncAnimation doesn't like to not be in the main thread (Big L for them..) """ - global ani, t1 # the variables to call the plotter and the read_arduino.py, we want them to get tossed around between functions and even programs + global ani_1, t1, ani_2, ani_3, ani_4 # the variables to call the plotter and the read_arduino.py, we want them to get tossed around between functions and even programs main() if 't1' in globals(): t1.terminate() # end the previous serial reads, if there is any @@ -97,8 +100,23 @@ def run(e): # run the plotter. Note that we should not put the plotter class, or function, in another process since # matplot's FuncAnimation doesn't like that plotter = SerialPlotter(frame) - ani = plotter.plotting() - plotter.fig.show() + if (sensors == 1) : + ani_1 = plotter.plotting_1() + elif (sensors == 2): + ani_1 = plotter.plotting_1() + ani_2 = plotter.plotting_2() + elif (sensors == 3) : + ani_1 = plotter.plotting_1() + ani_2 = plotter.plotting_2() + ani_3 = plotter.plotting_3() + elif (sensors == 4) : + ani_1 = plotter.plotting_1() + ani_2 = plotter.plotting_2() + ani_3 = plotter.plotting_3() + ani_4 = plotter.plotting_4() + #plotter.fig_1.show() + #plotter.fig_2.show() + plt.show() frame.btLaunch.SetLabelText("Plot") # change the text on the "launch" button to "plot" if not frame.show_msg.GetValue():