from ui import Frame from read_arduino import * from serial_plotter import * 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) # acknowledgement: https://stackoverflow.com/a/68666505. handles splash screens if '_PYIBoot_SPLASH' in os.environ and importlib.util.find_spec("pyi_splash"): import pyi_splash pyi_splash.update_text('UI Loaded ...') pyi_splash.close() HEIGHT = 800 WIDTH = 800 #global ani, t1 # TODO: add the option to change the font size of the graph def main(): #################### USER INPUTS ########################### r_ref = np.array( [frame.r_ref_1.GetValue(), frame.r_ref_2.GetValue(), frame.r_ref_3.GetValue(), frame.r_ref_4.GetValue(), 0]) # resisters for each An port, where n is an integer from 0-3. Use 0 if none. in Ohms r_ref[r_ref == ''] = '0' # correcting the emply values resistors = r_ref.astype(np.float32).tolist() # convert string to numbers input_voltage = float(frame.input_voltage.GetValue()) # bit_rate = float(frame.adjusted_volt.GetValue()) port = frame.dev_list.GetValue() # typical window size: 50 window_size = int(frame.m_textCtrl26.GetValue()) font_size = int(frame.fontSize.GetValue()) #################### END USER INPUTS ######################## # filename, disable customize name but should inform the user the current name of the data file on the front end # alternatively, could be default input if no other input given. But if there is might have problem... dat_folder = frame.dir.GetPath() os.makedirs(dat_folder, exist_ok=True) filename = os.path.join(os.getcwd(), dat_folder, f"{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.csv") delay = 1000 # millisec per data point, defined in the firmware # for now we have 5 reference resistors, there is an extra one (we set 4 on the UI) just in case if needed in the future if not (len(resistors) == 5): raise ValueError(f"expecting 5 resistor values, but got {len(resistors)}!!!") gen_settings(resistors, input_voltage, port, filename, window_size, font_size, delay) def gen_settings(resistors, input_voltage, port, filename, window_size, font_size, delay): """ export all inputs from main() to a .json file. We have everything stored in a .json file instead of having the program to call the values from the UI directly because 1. we dont have to import the ui.py all the time (so we can freely change ui.py to whatever else and only need to modify this one) and 2. we can have other program to access one variable from the ui at the same time. This adds flexibility to the program. We parameterize all the value for the same reason. :param resistors: list of reference resistances of the sensors :param input_voltage: Vin of the voltage divider :param bits: ADC resolution for the board. Usualy 12-bits for ESP32 (and 10 bits for Arduino Mega) :param port: serial port the board is connected to on the computer :param filename: file name and absolute directory which the serial data will be recorded to :param window_size: numbers of x values to display on the graph, or the x interval :param delay: amount of time to wait for a task's next start. 1000 ms for now to be consistent with the firmware """ name = "settings.json" settings = {} settings["refRes"] = resistors settings["sensor_ports"] = np.where(np.array(resistors) > 0)[0].tolist() settings["v_in"] = input_voltage settings["port"] = port # settings["resolution"] = bits settings["winSize"] = window_size settings["fontSize"] = font_size 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") 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_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 # place the read() function from read_arduino into another process to run it in background t1 = Process(target=read, args=()) t1.start() # 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) 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(): frame.Hide() if __name__ == '__main__': console_title = 'Program Crashed! Here are the details' # this is there for now to redirect any errors # Acknowledgement: https://stackoverflow.com/a/27694505 if sys.platform.startswith('win'): # On Windows calling this function is necessary. freeze_support() app = wx.App(useBestVisual=True) frame = Frame(None) app.SetTopWindow(frame) # set where the uncaught errors should be displayed console = wx.PyOnDemandOutputWindow(console_title) console.SetParent(frame) sys.stderr = console # instrad of print(), add something similar `sys.stdout = console` (without the two `) to print to allow us redirect the output elsewhere # BUT it only redirects the print() in this program, not the print() on other modules, like read_arduino.py print("this is an Beta version of the design. This debug terminal will be gone on official release\n") ports = [f"{comport.device}: {comport.description}" for comport in serial.tools.list_ports.comports()] # get all available ports frame.dev_list.AppendItems(ports) frame.SetTitle("SeeDatResistance - Beta 0.1.2") frame.btLaunch.Bind(wx.EVT_BUTTON, run) if os.path.isfile("settings.json"): try: print("Found existing settings.json, auto-fill previous inputs!\n") settings = json.load(open('settings.json', 'r')) frame.r_ref_1.SetValue(str(settings["refRes"][0])) frame.r_ref_2.SetValue(str(settings["refRes"][1])) frame.r_ref_3.SetValue(str(settings["refRes"][2])) frame.r_ref_4.SetValue(str(settings["refRes"][3])) frame.input_voltage.SetValue(str(settings["v_in"])) # frame.adjusted_volt.SetValue(str(settings["resolution"])) frame.m_textCtrl26.SetValue(str(settings["winSize"])) if settings["port"] in ports: # auto-select device port if exist frame.dev_list.SetValue(settings["port"]) except json.decoder.JSONDecodeError: # invalid settings file, ignore pass frame.Show() app.MainLoop() if 't1' in globals(): t1.terminate() # gracefully end the read_arduino process if it has been started