From 4489df7f62d7b08f8ec91fcb03735d17bc294efd Mon Sep 17 00:00:00 2001 From: Ingmar Jager Date: Tue, 5 Nov 2019 15:09:41 +0100 Subject: [PATCH] Show encoder number, accel and rise time after key press --- app/Pipfile | 2 + app/Pipfile.lock | 69 ++++++++++++----- app/analysis.py | 7 +- app/app.py | 65 +++++++++++----- app/comports.py | 58 +++++++++++--- app/views.py | 192 +++++++++++++++++++++++++++++++++-------------- 6 files changed, 289 insertions(+), 104 deletions(-) diff --git a/app/Pipfile b/app/Pipfile index 89fee01..3855cb4 100644 --- a/app/Pipfile +++ b/app/Pipfile @@ -9,6 +9,8 @@ verify_ssl = true pyside2 = "*" pyserial = "*" matplotlib = "*" +numpy = "*" +pandas = "*" [requires] python_version = "3.7" diff --git a/app/Pipfile.lock b/app/Pipfile.lock index fd1a7fc..7e48426 100644 --- a/app/Pipfile.lock +++ b/app/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "675a14f8a8ba3d2e6af1de89850566914938f85e1565030e91b747d3b32261f2" + "sha256": "2cf213ad413ed929972a04def051807070edd4b8e0dd3b6318451240cf807eed" }, "pipfile-spec": 6, "requires": { @@ -104,8 +104,34 @@ "sha256:f1df7b2b7740dd777571c732f98adb5aad5450aee32772f1b39249c8a50386f6", "sha256:ffca69e29079f7880c5392bf675eb8b4146479d976ae1924d01cd92b04cccbcc" ], + "index": "pypi", "version": "==1.17.3" }, + "pandas": { + "hashes": [ + "sha256:00dff3a8e337f5ed7ad295d98a31821d3d0fe7792da82d78d7fd79b89c03ea9d", + "sha256:22361b1597c8c2ffd697aa9bf85423afa9e1fcfa6b1ea821054a244d5f24d75e", + "sha256:255920e63850dc512ce356233081098554d641ba99c3767dde9e9f35630f994b", + "sha256:26382aab9c119735908d94d2c5c08020a4a0a82969b7e5eefb92f902b3b30ad7", + "sha256:33970f4cacdd9a0ddb8f21e151bfb9f178afb7c36eb7c25b9094c02876f385c2", + "sha256:4545467a637e0e1393f7d05d61dace89689ad6d6f66f267f86fff737b702cce9", + "sha256:52da74df8a9c9a103af0a72c9d5fdc8e0183a90884278db7f386b5692a2220a4", + "sha256:61741f5aeb252f39c3031d11405305b6d10ce663c53bc3112705d7ad66c013d0", + "sha256:6a3ac2c87e4e32a969921d1428525f09462770c349147aa8e9ab95f88c71ec71", + "sha256:7458c48e3d15b8aaa7d575be60e1e4dd70348efcd9376656b72fecd55c59a4c3", + "sha256:78bf638993219311377ce9836b3dc05f627a666d0dbc8cec37c0ff3c9ada673b", + "sha256:8153705d6545fd9eb6dd2bc79301bff08825d2e2f716d5dced48daafc2d0b81f", + "sha256:975c461accd14e89d71772e89108a050fa824c0b87a67d34cedf245f6681fc17", + "sha256:9962957a27bfb70ab64103d0a7b42fa59c642fb4ed4cb75d0227b7bb9228535d", + "sha256:adc3d3a3f9e59a38d923e90e20c4922fc62d1e5a03d083440468c6d8f3f1ae0a", + "sha256:bbe3eb765a0b1e578833d243e2814b60c825b7fdbf4cdfe8e8aae8a08ed56ecf", + "sha256:df8864824b1fe488cf778c3650ee59c3a0d8f42e53707de167ba6b4f7d35f133", + "sha256:e45055c30a608076e31a9fcd780a956ed3b1fa20db61561b8d88b79259f526f7", + "sha256:ee50c2142cdcf41995655d499a157d0a812fce55c97d9aad13bc1eef837ed36c" + ], + "index": "pypi", + "version": "==0.25.3" + }, "pyparsing": { "hashes": [ "sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80", @@ -123,33 +149,40 @@ }, "pyside2": { "hashes": [ - "sha256:02986ba0215691980d7b126049c9aa392ffa5174079113bc5e62684bde625cb6", - "sha256:433d8f0251ce3d7200ad5279b378165ad6babb1f10588ce7aae9df86c1e100d1", - "sha256:5c52c9e1916248c16c12ae925f167e6ca2580c514c0d46fca0933df6a371d204", - "sha256:8707fac6088dbf3c7262871a8fb5bf9276500f53ef67438b16c096cd510ce2e5", - "sha256:8d185ba5d84a885eb9aacf25cee4efe1f8e4abff63afb6bc10e7176e475db59e", - "sha256:e7aa2b4aa5f47c0f26c8907a4f308bca54ad8239bcabca904906b8f4a54e596e" + "sha256:589b90944c24046d31bf76694590a600d59d20130015086491b793a81753629a", + "sha256:63cc845434388b398b79b65f7b5312b9b5348fbc772d84092c9245efbf341197", + "sha256:7c57fe60ed57a3a8b95d9163abca9caa803a1470f29b40bff8ef4103b97a96c8", + "sha256:7c61a6883f3474939097b9dabc80f028887046be003ce416da1b3565a08d1f92", + "sha256:ed6d22c7a3a99f480d4c9348bcced97ef7bc0c9d353ad3665ae705e8eb61feb5", + "sha256:ede8ed6e7021232184829d16614eb153f188ea250862304eac35e04b2bd0120c" ], "index": "pypi", - "version": "==5.13.1" + "version": "==5.13.2" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" + ], + "version": "==2.8.1" + }, + "pytz": { + "hashes": [ + "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", + "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" ], - "version": "==2.8.0" + "version": "==2019.3" }, "shiboken2": { "hashes": [ - "sha256:14ca49878c8d545d1b74b0526af81e4ed5064133c682b57cabfa0ac16e7a2ccb", - "sha256:2aa481ce1097d10f74f7d0570d7f38fa5158e49b2146ae3aae5c8c56623681b1", - "sha256:cfec94e16b289f7abca6bf7dfc88b877098653855914c9f54d6477f47c68a93b", - "sha256:dfbb1f2ea86b4ddff9e91589bc93c9b6e2702ab09285cdc151daa8096442a665", - "sha256:f18ccc6f1870ab3fb7087412a745dfc4275c8afb1b645b88d982a5708032f3e3", - "sha256:f341940069fe764f2a9c3a06a9844c4c353c94227d3dbc304640f34b42c02d7a" + "sha256:5e84a4b4e7ab08bb5db0a8168e5d0316fbf3c25b788012701a82079faadfb19b", + "sha256:7c766c4160636a238e0e4430e2f40b504b13bcc4951902eb78cd5c971f26c898", + "sha256:81fa9b288c6c4b4c91220fcca2002eadb48fc5c3238e8bd88e982e00ffa77c53", + "sha256:ca08a3c95b1b20ac2b243b7b06379609bd73929dbc27b28c01415feffe3bcea1", + "sha256:e2f72b5cfdb8b48bdb55bda4b42ec7d36d1bce0be73d6d7d4a358225d6fb5f25", + "sha256:e6543506cb353d417961b9ec3c6fc726ec2f72eeab609dc88943c2e5cb6d6408" ], - "version": "==5.13.1" + "version": "==5.13.2" }, "six": { "hashes": [ diff --git a/app/analysis.py b/app/analysis.py index 6c335d4..cbcb475 100644 --- a/app/analysis.py +++ b/app/analysis.py @@ -10,8 +10,9 @@ class KeyPress: - def __init__(self, timestamps: list, positionData: list): + def __init__(self, encoder: int, timestamps: list, positionData: list): + self.encoder = encoder record_threshold_min_mm = 1 complete_threshold_mm = 15 self.timestamps, i = np.unique(np.array(timestamps), return_index=True) @@ -46,7 +47,9 @@ def metrics(self): rise_time = self.t[-1] - self.t[0] - return rise_time, average_acceleration + force_N = (DOWNWEIGHTS_g[self.encoder - 1] * average_acceleration) / 1000 + + return rise_time, average_acceleration, force_N def speed_data(self): time_s = self.t / 1000 diff --git a/app/app.py b/app/app.py index 3704c14..54fda2f 100644 --- a/app/app.py +++ b/app/app.py @@ -9,8 +9,43 @@ from tools import set_background_color from views import MainView +from comports import SerialConnection, SerialParser +from analysis import KeyPress + FPS = 20 + +class PianoApp(QtWidgets.QApplication): + + def __init__(self): + super(PianoApp, self).__init__() + + self.SerialConnection = SerialConnection() + + + self.window = MainWindow() + self.window.setWindowTitle("Piano Force Sensor") + + self.window.show() + signal.signal(signal.SIGINT, self.window.quit) + + self.toolbar = QtWidgets.QToolBar() + self.window.addToolBar(self.toolbar) + self.mainView = MainView(self.toolbar, self.SerialConnection.getDropdownWidget()) + self.window.setCentralWidget(self.mainView) + + self.mainView.refresh.connect(self.SerialConnection.refresh) + + self.parser = SerialParser() + self.SerialConnection.textStream.connect(self.parser.parse_line) + self.SerialConnection.textStream.connect(self.mainView.textOutputView.addText) + + # self.parser.newDataSet.connect + # self.parser.newDataSet.connect(estimateAcceleration) + + self.parser.newDataSet.connect(lambda i, t, p: self.mainView.resultsView.new_results(KeyPress(i, t,p))) + + class MainWindow(QtWidgets.QMainWindow): """ Class docstring @@ -33,14 +68,16 @@ def __init__(self): def _setupView(self): """Initialize Main Window""" - # self.setWindowIcon(QtGui.QIcon('assets/icon.png')) + self.setWindowIcon(QtGui.QIcon('assets/icon.jpeg')) self.setGeometry(50, 50, 1600, 900) - set_background_color(self, 'white') + # set_background_color(self, '#5a5d73') + set_background_color(self, 'gray') self._center() self.raise_() self.activateWindow() + def closeEvent(self, event): """Handle window close event""" if event: @@ -66,6 +103,13 @@ def _center(self): frameGm.moveCenter(centerPoint) self.move(frameGm.topLeft()) + # qRect = self.frameGeometry() + # centerPoint = QtWidgets.QDesktopWidget().availableGeometry().center() + # qRect.moveCenter(centerPoint) + # self.move(qRect.topLeft()) + + + def _update(self): """ Gui Thread poll """ if not self._running: @@ -75,22 +119,7 @@ def _update(self): pass if __name__ == "__main__": - app = QtWidgets.QApplication([]) - - - window = MainWindow() - - window.setWindowTitle("Piano Force Sensor") - - - window.show() - signal.signal(signal.SIGINT, window.quit) - # signal.signal(signal.SIGINT, signal.SIG_DFL) - - toolbar = QtWidgets.QToolBar() - window.addToolBar(toolbar) - widget = MainView(toolbar) - window.setCentralWidget(widget) + app = PianoApp() if sys.flags.interactive != 1: sys.exit(app.exec_()) diff --git a/app/comports.py b/app/comports.py index a6c408a..0e8df82 100644 --- a/app/comports.py +++ b/app/comports.py @@ -10,7 +10,7 @@ codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None) -class SerialManager(QtCore.QObject): +class SerialConnection(QtCore.QObject): # newCOMPorts = QtCore.Signal(list) @@ -18,19 +18,19 @@ class SerialManager(QtCore.QObject): textStream = QtCore.Signal(str) def __init__(self): - super(SerialManager, self).__init__() + super(SerialConnection, self).__init__() self.dropdown = QtWidgets.QComboBox() self.dropdown.currentIndexChanged.connect(self.selectCOMPort) self.availablePorts = None - self.refresh() self.serial = None - self.alive = None - self._reader_alive = None + self.alive = False + self._reader_alive = False self.receiver_thread = None + self.refresh() # self.rx_decoder = codecs.getincrementaldecoder('UTF-8')('replace') self.rx_decoder = codecs.getdecoder('UTF-8') #('replace') @@ -39,7 +39,7 @@ def __init__(self): @QtCore.Slot(str) def selectCOMPort(self, index): - if index: + if index > 0: port = self.availablePorts[index-1] print('select', port) self.change_port(port) @@ -47,7 +47,6 @@ def selectCOMPort(self, index): self._stop_reader() self.serial = None - def getDropdownWidget(self): return self.dropdown @@ -58,8 +57,6 @@ def refresh(self): self.dropdown.addItems([p.device for p in self.availablePorts]) # self.newCOMPorts.emit([p.device for p in self.availablePorts]) - - def change_port(self, port: serial.Serial): if port and self.serial and port != self.serial.port: @@ -82,9 +79,11 @@ def _start_reader(self): def _stop_reader(self): """Stop reader thread only, wait for clean exit of thread""" self._reader_alive = False - if hasattr(self.serial, 'cancel_read'): + if self.serial and hasattr(self.serial, 'cancel_read'): self.serial.cancel_read() - self.receiver_thread.join() + + if self.receiver_thread: + self.receiver_thread.join() def reader(self): """loop and copy serial->console""" @@ -103,3 +102,40 @@ def reader(self): self.alive = False # self.console.cancel() raise # XXX handle instead of re-raise? + + +START = 'Start Encoder' +END = 'End' + +class SerialParser(QtCore.QObject): + + + newDataSet = QtCore.Signal(int, list, list) + + def __init__(self): + super(SerialParser, self).__init__() + self.started = False + self.timestamps = [] + self.positions = [] + self.current_encoder = None + + @QtCore.Slot(str) + def parse_line(self, line: str): + + if line.startswith(END): + self.started = False + self.newDataSet.emit(self.current_encoder, self.timestamps, self.positions) + + if line.startswith(START): + self.current_encoder = int(line[len(START):]) + self.started = True + self.timestamps = [] + self.positions = [] + return + + if self.started: + res = line.split(":") + self.timestamps.append(int(res[0])) + self.positions.append(int(res[1])) + + diff --git a/app/views.py b/app/views.py index f80b36b..1d0ef5b 100644 --- a/app/views.py +++ b/app/views.py @@ -1,15 +1,15 @@ -import random from PySide2 import Qt, QtCore, QtWidgets, QtGui -import serial -from serial.tools import list_ports import matplotlib.pyplot as plt +import matplotlib.patches as patches + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure -from serialmanager import SerialManager -from parse import SerialParser +from analysis import KeyPress + +plt.rcParams['axes.grid'] = True class FilePicker(QtWidgets.QWidget): @@ -35,13 +35,39 @@ class ResultView(QtWidgets.QWidget): def __init__(self): super(ResultView, self).__init__() + self.current_keypress = None + self.layout = QtWidgets.QVBoxLayout(self) self.plot = MatplotlibWidget() self.layout.addWidget(self.plot) + + + buttons = QtWidgets.QHBoxLayout() + btn = QtWidgets.QPushButton('position') + btn.clicked.connect(lambda: self.plot.show_position(self.current_keypress)) + buttons.addWidget(btn) + btn = QtWidgets.QPushButton('speed') + btn.clicked.connect(lambda: self.plot.show_speed(self.current_keypress)) + buttons.addWidget(btn) + btn = QtWidgets.QPushButton('accel') + btn.clicked.connect(lambda: self.plot.show_accel(self.current_keypress)) + buttons.addWidget(btn) + + self.layout.addLayout(buttons) + # self.plot.update_plot(range(5)) + self.setStyleSheet("font-weight: bold; font-size: {}px".format(24)) self.forceResult = QtWidgets.QLabel('3.5 N') self.accelResult = QtWidgets.QLabel('20 mm/s^2') + self.encoder = QtWidgets.QLabel('-') + self.risetimeResult = QtWidgets.QLabel('- s') + + + valueLayout = QtWidgets.QHBoxLayout() + valueLayout.addWidget(QtWidgets.QLabel('Encoder')) + valueLayout.addWidget(self.encoder) + self.layout.addLayout(valueLayout) valueLayout = QtWidgets.QHBoxLayout() valueLayout.addWidget(QtWidgets.QLabel('Force')) @@ -55,6 +81,101 @@ def __init__(self): self.layout.addLayout(valueLayout) + valueLayout = QtWidgets.QHBoxLayout() + valueLayout.addWidget(QtWidgets.QLabel('Rise Time')) + valueLayout.addWidget(self.risetimeResult) + + self.layout.addLayout(valueLayout) + + + + @QtCore.Slot(KeyPress) + def new_results(self, k: KeyPress): + if k.valid(): + self.encoder.setText(str(k.encoder)) + self.current_keypress = k + rise_time, avg_accel, force = self.current_keypress.metrics() + self.plot.show_position(self.current_keypress) + self.accelResult.setText('{0:.2f} mm/s^2'.format(avg_accel)) + self.risetimeResult.setText('{0:.1f} ms'.format(rise_time)) + self.forceResult.setText('{0:.2f} N'.format(force)) + + +class MatplotlibWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + fig = Figure(figsize=(7, 5), dpi=65, facecolor=(1, 1, 1), edgecolor=(0, 0, 0)) + self.canvas = FigureCanvas(fig) + self.toolbar = NavigationToolbar(self.canvas, self) + lay = QtWidgets.QVBoxLayout(self) + lay.addWidget(self.toolbar) + lay.addWidget(self.canvas) + + self.ax = fig.add_subplot(111) + self.ax.set_xlabel('Time [ms]') + self.ax.set_ylabel('Position [mm]') + self.ax.set_title('Keypress position vs time') + + self.line1, *_ = self.ax.plot([]) + self.line2, *_ = self.ax.plot([]) + self.rect = None + + + def plot(self, x, y, z, title, xlabel, ylabel): + self.clear() + self.ax.set_xlabel(xlabel) + self.ax.set_ylabel(ylabel) + self.ax.set_title(title) + + self.line1.set_data(x, y) + + self.ax.set_xlim(min(x), max(x)) + self.ax.set_ylim(min(y), max(y)) + + if z is not None and len(z): + self.line2.set_data(x, z) + + self.canvas.draw() + + def clear(self): + if self.rect: + self.rect.remove() + self.rect = None + + # self.line1.set_data([]) + # self.line2.set_data([]) + + + + def show_speed(self, k: KeyPress): + self.clear() + t, speed, speed_fitted = k.speed_data() + self.plot(t, speed, speed_fitted, title='Speed vs time', xlabel='Time [ms]', ylabel='Speed [mm/s]') + + + def show_accel(self, k: KeyPress): + self.clear() + t, accel, accel_fitted = k.accel_data() + self.plot(t, accel, accel_fitted, title='Acceleration vs time', xlabel='Time [ms]', ylabel='Speed [mm/s^2]') + + + def show_position(self, k: KeyPress): + self.clear() + + t = k.timestamps + y = k.positionData + + self.plot(t, y, [], title='Position vs time', xlabel='Time [ms]', ylabel='Position [mm]') + + # Create a Rectangle patch + self.rect = patches.Rectangle((k.t[0],k.y[0]),k.dt(),k.dy(),linewidth=1, + edgecolor='r',facecolor='none', linestyle='--') + + # Add the patch to the Axes + self.ax.add_patch(self.rect) + self.canvas.draw() + class TextOutputView(QtWidgets.QWidget): @@ -87,65 +208,26 @@ def addText(self, text: str): self.output.append(text) - - - -class MatplotlibWidget(QtWidgets.QWidget): - def __init__(self, parent=None): - super().__init__(parent) - - fig = Figure(figsize=(7, 5), dpi=65, facecolor=(1, 1, 1), edgecolor=(0, 0, 0)) - self.canvas = FigureCanvas(fig) - self.toolbar = NavigationToolbar(self.canvas, self) - lay = QtWidgets.QVBoxLayout(self) - lay.addWidget(self.toolbar) - lay.addWidget(self.canvas) - - self.ax = fig.add_subplot(111) - self.line, *_ = self.ax.plot([]) - - # @Slot(list) - def update_plot(self, t, y): - self.line.set_data(t, y) - - self.ax.set_xlim(0, len(y)) - self.ax.set_ylim(min(y), max(y)) - self.canvas.draw() - class MainView(QtWidgets.QWidget): - def __init__(self, toolbar): - super(MainView, self).__init__() - - - - # availablePorts = list_ports.comports() - - # portsString = ''.join([p.device + '\n' for p in availablePorts]) + refresh = QtCore.Signal() - self.parser = SerialParser() + def __init__(self, toolbar, dropdown): + super(MainView, self).__init__() self.toolbar = toolbar - self.button = QtWidgets.QPushButton("Click me!") self.text = QtWidgets.QLabel("Text") self.text.setAlignment(QtCore.Qt.AlignCenter) - - self.serialManager = SerialManager() - self.serialManager.textStream.connect(self.parser.parse_line) - - # self.serialManager.newCOMPorts.connect(self.updateCOMPorts) - # self.serialManager.refresh() - self.toolbar.addWidget(QtWidgets.QLabel("Select COM Port:")) - self.toolbar.addWidget(self.serialManager.getDropdownWidget()) + self.toolbar.addWidget(dropdown) self.refreshBtn = QtWidgets.QPushButton('Refresh') - self.refreshBtn.clicked.connect(self.serialManager.refresh) + self.refreshBtn.clicked.connect(self.refresh) self.toolbar.addWidget(self.refreshBtn) self.filepicker = FilePicker() @@ -162,14 +244,14 @@ def __init__(self, toolbar): self.layout = QtWidgets.QHBoxLayout(self) - self.left = ResultView() - self.right = TextOutputView() + # left + self.resultsView = ResultView() - self.parser.newDataSet.connect(self.left.plot.update_plot) - self.serialManager.textStream.connect(self.right.addText) + # right + self.textOutputView = TextOutputView() - self.layout.addWidget(self.left) - self.layout.addWidget(self.right) + self.layout.addWidget(self.resultsView) + self.layout.addWidget(self.textOutputView) @QtCore.Slot(list) def updateCOMPorts(self, portlist):