document code and minor bug fix

This commit is contained in:
Eric Yu 2022-08-18 02:56:58 -07:00
parent 4613f9dcea
commit 54c05753c9
4 changed files with 102 additions and 54 deletions

View File

@ -10,6 +10,9 @@ import numpy as np
SENSORS_MAX = 4 # maximum sensor ports SENSORS_MAX = 4 # maximum sensor ports
def read(): def read():
"""
read the data from a board, if any. If "no device" selected generate random values for demo
"""
baud = 19200 baud = 19200
settings = json.load(open('settings.json', 'r')) settings = json.load(open('settings.json', 'r'))
resolution = settings["resolution"] # this is not in use, but just in case if we decide to switch back to analogRead() instead of ESP32's analogReadMiliVolts() on the firmware resolution = settings["resolution"] # this is not in use, but just in case if we decide to switch back to analogRead() instead of ESP32's analogReadMiliVolts() on the firmware
@ -23,8 +26,9 @@ def read():
if np.any(sensor_ports >= SENSORS_MAX): if np.any(sensor_ports >= SENSORS_MAX):
raise ValueError("Port range is 0-3!") raise ValueError("Port range is 0-3!")
if "- No Device -" in port: if "- No Device -" in port:
# generate random value
while True: while True:
dat_list = np.random.randint(0, 3300, len(sensor_ports)) # create a randomized voltage data dat_list = np.random.randint(0, v_in * 1000, SENSORS_MAX) # create a randomized voltage data
# take only the nonzero indices, and truncated to two decimal places to "filter" out some hardware errors # take only the nonzero indices, and truncated to two decimal places to "filter" out some hardware errors
dat_sel = np.trunc((np.take(dat_list, sensor_ports) / 1000) * 10**2) / 10**2 dat_sel = np.trunc((np.take(dat_list, sensor_ports) / 1000) * 10**2) / 10**2
r_arr = np.take(refRes, sensor_ports) * (v_in / dat_sel - 1) r_arr = np.take(refRes, sensor_ports) * (v_in / dat_sel - 1)
@ -39,7 +43,6 @@ def read():
else: else:
controller = serial.Serial(port, baudrate=baud) controller = serial.Serial(port, baudrate=baud)
# TODO: separate data in each run but still keep them in one csv file
while controller.isOpen(): while controller.isOpen():
try: try:
read_data = controller.readline().decode("utf-8") read_data = controller.readline().decode("utf-8")
@ -58,8 +61,8 @@ def read():
f = open(file_name, "a", newline="", encoding="utf-8") f = open(file_name, "a", newline="", encoding="utf-8")
f.write(dat + '\n') f.write(dat + '\n')
f.close() f.close()
# except KeyboardInterrupt as e: except KeyboardInterrupt as e:
# print(e.__class__.__name__) print(e.__class__.__name__)
# break break
except (json.decoder.JSONDecodeError, UnicodeDecodeError): except (json.decoder.JSONDecodeError, UnicodeDecodeError):
print('decoder error') print('decoder error')

View File

@ -1,13 +1,22 @@
from datetime import datetime from datetime import datetime
from matplotlib.animation import FuncAnimation
import os, json, traceback, wx
import numpy as np import numpy as np
import matplotlib import matplotlib
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import os, json, traceback, ui, wx matplotlib.use("WXAgg") # for JetBrains IDE to force use wxPython as backend UI for plotting
matplotlib.use("WXAgg") # for JetBrains IDE to force use the wx backend
class SerialPlotter: class SerialPlotter:
def __init__(self) -> None: def __init__(self, parent: wx.Frame = None) -> None:
"""
Dynamically plot a graph with moving window, if a finite window size set by the user. It reads the .csv file
read_arduino generated, use the last line as the latest data and update the graph accordingly. It will plot
multiple lines on the graph if there are more than one values on the last row of the .csv file
:param parent: optional, parent frame
"""
self.parent = parent
self.settings = json.load(open('settings.json', 'r')) self.settings = json.load(open('settings.json', 'r'))
self.sensors = len(self.settings['sensor_ports']) self.sensors = len(self.settings['sensor_ports'])
self.windowsize = self.settings['winSize'] self.windowsize = self.settings['winSize']
@ -15,47 +24,63 @@ class SerialPlotter:
self.colors = ['blue', 'orange', 'green', 'yellow'] 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 # 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._close_event) self.fig.canvas.mpl_connect('close_event', self.event_close)
self.timeStamps = {} self.timeStamps = {}
self.sensorsData = {} self.sensorsData = {}
self.timeElapsed = 0 # time have passed since the graph started, in seconds
for i in range(self.sensors): for i in range(self.sensors):
self.timeStamps[i] = ['']#*self.windowsize self.timeStamps[i] = ['']
self.sensorsData[i] = [0]#*self.windowsize self.sensorsData[i] = [0]
plt.tight_layout() 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
def _close_event(self, event) -> None: bring back the UI, if exist
""" 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"] 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") wx.MessageBox(f"File has saved as {os.path.split(file)[1]} under {os.path.split(file)[0]} directory!\n")
if self.parent:
self.parent.Show()
def animation(self, t:int) -> None: def animation(self, t: int) -> None:
""" render a frame of the animated graph """ """
render a frame of the animated graph
"""
try: try:
plt.cla() # clear previous frame plt.cla() # clear previous frame
# read the last line from the .csv file, the data start from the second colunm so omit index # 0 # 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") file = open(self.settings["file_name"], "r")
ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in file]) ndata = np.array([np.asarray(line.split(", ")[1:], dtype=np.float32) for line in file])
if len(ndata) > 0: if len(ndata) > 0:
row = ndata[-1] row = ndata[-1]
i = 0 i = 0
while i < self.sensors: while i < self.sensors:
# shift all data left by 1 index, pop out the leftmost value # shift all data left by 1 index, pop out the leftmost value, if windowsize is not 0 (infinite)
if self.windowsize > 0 and len(self.timeStamps[i]) > self.windowsize: # TODO: make sure the two lists have the same size # 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.timeStamps[i].pop(0)
self.sensorsData[i].pop(0) self.sensorsData[i].pop(0)
self.timeStamps[i].append(datetime.now().strftime('%H:%M:%S')) # 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]) self.sensorsData[i].append(row[i])
# plot a line # plot a line
# TODO: round the number to 1-2- decimal places # 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])}') self.axs.plot(self.timeStamps[i], self.sensorsData[i], color=self.colors[i],
label=f'sensor {i + 1}, latest: {np.floor(self.sensorsData[i][-1])} $\Omega$')
self.axs.set_xlabel('Time (seconds)')
self.axs.set_ylabel(u'Resistance ($\Omega$)')
i += 1 i += 1
self.timeElapsed += 1 # increment time
# Acknowledgement: https://stackoverflow.com/a/13589144 # Acknowledgement: https://stackoverflow.com/a/13589144
handles, labels = self.axs.get_legend_handles_labels() handles, labels = self.axs.get_legend_handles_labels()
@ -65,6 +90,6 @@ class SerialPlotter:
traceback.print_exc() traceback.print_exc()
def plotting(self) -> FuncAnimation: def plotting(self) -> FuncAnimation:
ani = FuncAnimation(self.fig, self.animation, blit=False) """ animate the dynamic plot """
ani = FuncAnimation(self.fig, self.animation, blit=False, interval=self.delay * 1000)
return ani return ani

42
test.py
View File

@ -1,8 +1,8 @@
from ui import Frame from ui import Frame
from multiprocessing import *
from read_arduino import * from read_arduino import *
from serial_plotter import * from serial_plotter import *
import os, sys, json, wx,importlib, warnings from multiprocessing import *
import os, sys, json, wx, importlib, warnings
import serial.tools.list_ports import serial.tools.list_ports
import numpy as np import numpy as np
@ -11,6 +11,7 @@ warnings.filterwarnings("ignore", category=DeprecationWarning)
# acknowledgement: https://stackoverflow.com/a/68666505. handles splash screens # acknowledgement: https://stackoverflow.com/a/68666505. handles splash screens
if '_PYIBoot_SPLASH' in os.environ and importlib.util.find_spec("pyi_splash"): if '_PYIBoot_SPLASH' in os.environ and importlib.util.find_spec("pyi_splash"):
import pyi_splash import pyi_splash
pyi_splash.update_text('UI Loaded ...') pyi_splash.update_text('UI Loaded ...')
pyi_splash.close() pyi_splash.close()
@ -22,9 +23,11 @@ global ani, t1
def main(): def main():
#################### USER INPUTS ########################### #################### USER INPUTS ###########################
resistors = [float(frame.r_ref_1.GetValue()), float(frame.r_ref_2.GetValue()), float(frame.r_ref_3.GetValue()), r_ref = np.array(
float(frame.r_ref_4.GetValue()), [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 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()) input_voltage = float(frame.input_voltage.GetValue())
bit_rate = float(frame.adjusted_volt.GetValue()) bit_rate = float(frame.adjusted_volt.GetValue())
port = frame.dev_list.GetValue() port = frame.dev_list.GetValue()
@ -38,7 +41,8 @@ def main():
dat_folder = "RecordedData" dat_folder = "RecordedData"
os.makedirs(dat_folder, exist_ok=True) 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") filename = os.path.join(os.getcwd(), dat_folder, f"{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.csv")
delay = 1000 # millisec pe r data, defined in the firmware 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): if not (len(resistors) == 5):
raise ValueError(f"expecting 5 resistor values, but got {len(resistors)}!!!") raise ValueError(f"expecting 5 resistor values, but got {len(resistors)}!!!")
@ -48,6 +52,13 @@ def main():
def gen_settings(resistors, input_voltage, bits, port, filename, window_size, delay): def gen_settings(resistors, input_voltage, bits, port, filename, window_size, delay):
""" """
export all inputs from main() to a .json file export all inputs from main() to a .json file
: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" name = "settings.json"
settings = {} settings = {}
@ -77,7 +88,7 @@ def run(e):
t1.start() t1.start()
# run the plotter. Note that we should not put the plotter class, or function, in another process since # 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 # matplot's FuncAnimation doesn't like that
plotter = SerialPlotter() plotter = SerialPlotter(frame)
ani = plotter.plotting() ani = plotter.plotting()
plotter.fig.show() plotter.fig.show()
@ -87,19 +98,27 @@ def run(e):
if __name__ == '__main__': if __name__ == '__main__':
sys.stdout.write("this is an alpha version of the design. This debug terminal will be gone on official release\n") console_title = 'Program Crashed! Here are the details' # this is there for now to redirect any errors
# Acknowledgement: https://stackoverflow.com/a/27694505 # Acknowledgement: https://stackoverflow.com/a/27694505
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
# On Windows calling this function is necessary. # On Windows calling this function is necessary.
freeze_support() freeze_support()
global t1 global t1
app = wx.App() app = wx.App(useBestVisual=True)
frame = Frame(None) 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
sys.stdout.write("this is an alpha version of the design. This debug terminal will be gone on official release\n")
ports = [comport.device for comport in serial.tools.list_ports.comports()] # get all available ports ports = [comport.device for comport in serial.tools.list_ports.comports()] # get all available ports
frame.dev_list.AppendItems(ports) frame.dev_list.AppendItems(ports)
frame.SetTitle("Cease your resistance! - alpha 0.2.0") frame.SetTitle("SeeDatResistance - Beta 0.1.0 RC2")
frame.btLaunch.Bind(wx.EVT_BUTTON, run) frame.btLaunch.Bind(wx.EVT_BUTTON, run)
if os.path.isfile("settings.json"): if os.path.isfile("settings.json"):
try:
sys.stdout.write("Found existing settings.json, auto-fill previous inputs!\n") sys.stdout.write("Found existing settings.json, auto-fill previous inputs!\n")
settings = json.load(open('settings.json', 'r')) settings = json.load(open('settings.json', 'r'))
frame.r_ref_1.SetValue(str(settings["refRes"][0])) frame.r_ref_1.SetValue(str(settings["refRes"][0]))
@ -111,7 +130,8 @@ if __name__ == '__main__':
frame.m_textCtrl26.SetValue(str(settings["winSize"])) frame.m_textCtrl26.SetValue(str(settings["winSize"]))
if settings["port"] in ports: # auto-select device port if exist if settings["port"] in ports: # auto-select device port if exist
frame.dev_list.SetValue(settings["port"]) frame.dev_list.SetValue(settings["port"])
except json.decoder.JSONDecodeError: # invalid settings file, ignore
pass
frame.Show() frame.Show()
app.MainLoop() app.MainLoop()
if 't1' in globals(): if 't1' in globals():

2
ui.py
View File

@ -16,7 +16,7 @@ import wx
class Frame ( wx.Frame ): class Frame ( wx.Frame ):
def __init__( self, parent ): def __init__( self, parent ):
wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 470,330 ), style = wx.DEFAULT_FRAME_STYLE ) wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = wx.EmptyString, pos = wx.DefaultPosition, size = wx.Size( 470,337 ), style = wx.DEFAULT_FRAME_STYLE )
self.SetSizeHintsSz( wx.Size( 470,330 ), wx.DefaultSize ) self.SetSizeHintsSz( wx.Size( 470,330 ), wx.DefaultSize )
self.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNTEXT ) ) self.SetForegroundColour( wx.SystemSettings.GetColour( wx.SYS_COLOUR_BTNTEXT ) )