Source code for Recorder.Recorder

""" Software made for recording velocity form a Femto-Gramophone
    in a triggered manner. Can be used in conjunction with two-photon
    microscoppes by Femtonics Inc. """

import os
import shelve
import sys
import time
from collections import deque
from statistics import mean

import h5py
import matplotlib.pyplot as plt
from PyQt5 import QtCore
from PyQt5.QtCore import QAbstractTableModel, QModelIndex, QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtSerialPort import QSerialPort
from PyQt5.QtWidgets import QApplication, QFileDialog, QHeaderView, QMessageBox
from PyQt5.uic import loadUiType

from GramophoneTools import Comms
from GramophoneTools.Recorder import logger

DIR = os.path.dirname(os.path.abspath(__file__))

ABOUT_WIN_BASE, ABOUT_WIN_UI = loadUiType(DIR+'/about.ui')
LICENSE_WIN_BASE, LICENSE_WIN_UI = loadUiType(DIR+'/license.ui')
SETTINGS_WIN_BASE, SETTINGS_WIN_UI = loadUiType(DIR+'/settings.ui')
DEVINFO_WIN_BASE, DEVINFO_WIN_UI = loadUiType(DIR+'/device_info.ui')
MAIN_WIN_BASE, MAIN_WIN_UI = loadUiType(DIR+'/main.ui')

[docs]class aboutWindow(ABOUT_WIN_BASE, ABOUT_WIN_UI): """ The window shown if the user clicks the About option in the Help menu. Displays basic info about the software. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.close_btn.clicked.connect(self.close)
[docs]class licenseWindow(LICENSE_WIN_BASE, LICENSE_WIN_UI): """ The window shown if the user clicks the License option in the Help menu. Displays the open source license the software is distributed under. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.close_btn.clicked.connect(self.close)
[docs]class settingsWindow(SETTINGS_WIN_BASE, SETTINGS_WIN_UI): def __init__(self, main_win, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.main_win = main_win self.cancel_btn.clicked.connect(self.close) self.apply_btn.clicked.connect(self.apply) def apply(self): self.save_settings() self.close() def load_settings(self): db ='/GramophoneTools/settings') self.main_win.settings['gramo_names'] = db.get('gramo_names', {}) self.main_win.settings['sampling_freq'] = db.get('sampling_freq', 50) self.main_win.settings['trigger_channel'] = db.get( 'trigger_channel', 1) db.close() self.freq_spinbox.setValue(self.main_win.settings['sampling_freq']) if self.main_win.settings['trigger_channel'] == 1: self.trigger_radio_1.setChecked(True) if self.main_win.settings['trigger_channel'] == 2: self.trigger_radio_2.setChecked(True) def save_settings(self): self.main_win.settings['sampling_freq'] = self.freq_spinbox.value() if self.trigger_radio_1.isChecked(): self.main_win.settings['trigger_channel'] = 1 if self.trigger_radio_2.isChecked(): self.main_win.settings['trigger_channel'] = 2 with'/GramophoneTools/settings') as db: db['gramo_names'] = self.main_win.settings['gramo_names'] db['sampling_freq'] = self.main_win.settings['sampling_freq'] db['trigger_channel'] = self.main_win.settings['trigger_channel']
[docs]class deviceInfoWindow(DEVINFO_WIN_BASE, DEVINFO_WIN_UI): def __init__(self, gram, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.gram = gram product_info = self.gram.product_info firmware_info = self.gram.firmware_info self.close_btn.clicked.connect(self.close) prod_name = 'Name: ' + product_info['name'] revision = 'Revision: ' + product_info['revision'] serial = 'Serial: ' + hex(product_info['serial']) production = 'Production ({}): {}'.format( product_info['production_format'], product_info['production']) self.product_label.setText( prod_name+'\n'+revision+'\n'+serial+'\n'+production) release = 'Relase:' + firmware_info['release'] build = 'Build:' + firmware_info['build'] date = 'Date ({}): {}'.format( firmware_info['date_format'], firmware_info['date']) ftime = 'Time ({}): {}'.format( firmware_info['time_format'], firmware_info['time']) self.firmware_label.setText(release+'\n'+build+'\n'+date+'\n'+ftime) self.refresh_sensor_label() self.refresh_btn.clicked.connect(self.refresh_sensor_label) def refresh_sensor_label(self): sensors = {} for key, val in self.gram.read_sensors().items(): sensors[self.gram.parameters[key].name] = val voltage3v3 = 'Voltage on 3.3V rail: ' + \ '{0:.2f}'.format(sensors['VSEN3V3'])+' V' voltage5v = 'Voltage on 5V rail: ' + \ '{0:.2f}'.format(sensors['VSEN5V'])+' V' mcu_temp = 'MCU temperature: ' + \ '{0:.2f}'.format(sensors['TSENMCU'])+' C' ext_temp = 'External temperature: ' + \ '{0:.2f}'.format(sensors['TSENEXT'])+' C' self.sensor_label.setText( voltage3v3+'\n'+voltage5v+'\n'+mcu_temp+'\n'+ext_temp)
[docs]class pyGramWindow(MAIN_WIN_BASE, MAIN_WIN_UI): """ The main window of the Gramophone reader. """ def __init__(self, devmode, log_file, *args, **kwargs): super().__init__(*args, **kwargs) self.setupUi(self) self.about_win = aboutWindow() self.license_win = licenseWindow() self.settings_win = settingsWindow(self) self.extra_windows = [] # Load settings file self.settings = {} self.settings_win.load_settings() # Properties self.recording = False self.timer_zero = 0 self.autosave = False # Live plot self.graph.setBackground(None) self.graph.hideButtons() self.graph.setMouseEnabled(x=False, y=False) self.graph.showAxis(axis='bottom', show=False) # self.graph.setLabel(axis='bottom', text='Time') # self.graph.showLabel(axis='bottom', show=False) self.graph.showGrid(x=False, y=True, alpha=1) self.graph.disableAutoRange(axis='y') self.graph.setYRange(-54000, 54000, padding=0, update=False) self.reset_graph() # Make initial file self._log = None if log_file is None: self.new_file() else: self.select_file(mode=None, filename=log_file) self.current_record = None # Buttons self.connect_btn.clicked.connect(self.connect_btn_cb) self.refresh_btn.clicked.connect(self.refresh_gram_list) self.plot_btn.clicked.connect(self.plot_btn_cb) self.select_all_btn.clicked.connect(self.records_table.selectAll) self.delete_btn.clicked.connect(self.delete_btn_cb) self.settings_btn.clicked.connect(self.show_settings) self.gram_info_btn.clicked.connect(self.show_device_info) self.out_1_btn.clicked.connect(self.toggle_out_1) self.out_2_btn.clicked.connect(self.toggle_out_2) self.out_3_btn.clicked.connect(self.toggle_out_3) self.out_4_btn.clicked.connect(self.toggle_out_4) # Menu actions self.actionNew_File.triggered.connect(self.new_file) self.actionNew_Window.triggered.connect(self.new_window) self.actionOpen.triggered.connect( self.actionSave.triggered.connect( self.actionSave_As.triggered.connect(self.save_as) self.actionAutosave.triggered.connect(self.set_autosave) self.actionQuit.triggered.connect(self.close) self.actionAbout.triggered.connect(self.show_about) self.actionLicense.triggered.connect(self.show_license) self.actionGramophone_manual.triggered.connect(self.show_manual) self.actionExport_to_xls.triggered.connect(self.xls_export) # Initialize GUI self.refresh_gram_list() # Gramophone self.gram = None self.connected = False # Developer options self.menuDEV.menuAction().setVisible(devmode) self.DEV_reset_gram_timer.triggered.connect(self.reset_gram_timer) self.DEV_make_dummy.triggered.connect(self.make_dummy) @property def log(self): return self._log @log.setter def log(self, value): if self.log is not None: self.log.close_log_file() self._log = value self.log_model = VelLogModel(self._log) self.log_model.dataChanged.connect(self.log_changed) self.log_model.rowsInserted.connect(self.update_table_size) self.records_table.setModel(self.log_model) self.records_table.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) # self.records_table.horizontalHeader().setResizeMode(QHeaderView.Fixed) #old # self.records_table.resizeColumnsToContents() self.update_table_size() self.update_title()
[docs] @pyqtSlot() def log_changed(self): """ Calll this function when the velocity log is modified and it should be saved. """ self.setWindowModified(True)
[docs] def update_title(self): ''' Updates the tile of the window with the name of the current file and a * character if there are unsaved changes made to said file. Called after the current file is changed or saved. ''' if self.log.filename is not None: self.setWindowTitle('Gramophone Recorder - ' + os.path.basename(self.log.filename) + '[*]') else: self.setWindowTitle('Gramophone Recorder - New log [*]')
[docs] def new_file(self): """ Creates a new blank velocity log file and sets it as the current working file. """ response = self.save_warning() if response != 'cancel': if response == 'save': self.counter_box.setValue(1) self.log = logger.VelocityLog() self.setWindowModified(False)
[docs] def new_window(self): """ Opens a new instance of the Gramophone recorder. New windows can be used for simultanious recording of an other Gramophone device. """ os.startfile('gramrec')
[docs] def open(self): """ Opens a velocity log from file and sets it as the current working file. """ response = self.save_warning() if response != 'cancel': if response == 'save': self.select_file(mode='open')
[docs] def save(self): ''' Saves the currently used log file. Returns True if the file is saved or False if saving was cancelled ''' if self.log.filename is None: if self.select_file(mode='save'): return else: return False else: self.setWindowModified(False) self.update_title() return True
[docs] def save_as(self): ''' Same as save, but shows a file selection dialog to save to an other file. ''' if self.select_file():
[docs] def set_autosave(self, val): """ Turns automatic saving on (True) or off (False) :param val: The sate to set :type val: bool """ self.autosave = val if val: self.autosave = self.actionAutosave.setChecked(self.autosave)
[docs] def select_file(self, mode='save', filename=None): ''' Displays a file selection dialog for velocity log files. Mode can be 'save' or 'open'. ''' if filename is None: options = QFileDialog.Options() fileformat = "pyGram logs (*.vlg)" if mode == 'save': # Show a save dialog filename, _ = QFileDialog.getSaveFileName( self, "Save velocity log", "", fileformat, options=options) if filename: self.log.open_log_file(filename, 'w') if mode == 'open': # Show an open dialog filename, _ = QFileDialog.getOpenFileName( self, "Open velocity log", "", fileformat, options=options) if filename: self.log = logger.VelocityLog.from_file(filename) else: self.log = logger.VelocityLog.from_file(filename) self.update_title() self.update_table_size() print('File selected:', filename) return bool(filename)
[docs] def show_about(self): """ Displays the About window. """ self.about_win.activateWindow()
[docs] def show_license(self): """ Displays the License window. """ self.license_win.activateWindow()
[docs] def show_manual(self): """ Opens the Gramophone User Guide in the default pdf reader. """ os.startfile( DIR+"/../docs/_downloads/Gramophone User Guide.pdf")
[docs] def show_settings(self): """ Opens a settings window. """ self.settings_win.activateWindow()
def show_device_info(self): self.device_info_win = deviceInfoWindow(self.selected_gramophone) def xls_export(self): options = QFileDialog.Options() fileformat = "Excel workbook (*.xlsx)" filename, _ = QFileDialog.getSaveFileName( self, "Save velocity log", "", fileformat, options=options) if filename: self.log.xls_export(filename) @pyqtSlot(bool) def toggle_out_1(self, value): self.gram.write_output(1, int(value)) @pyqtSlot(bool) def toggle_out_2(self, value): self.gram.write_output(2, int(value)) @pyqtSlot(bool) def toggle_out_3(self, value): self.gram.write_output(3, int(value)) @pyqtSlot(bool) def toggle_out_4(self, value): self.gram.write_output(4, int(value))
[docs] def reset_graph(self): """ Resets the live velocity plot to its starting state ie. erases the plot if there is already something on it. """ self.graph.clear() self.graph_time = deque([], maxlen=round( self.settings['sampling_freq'])*10) self.graph_vel = deque([], maxlen=round( self.settings['sampling_freq'])*10) self.vel_window = deque([], maxlen=10) self.curve = self.graph.plot( self.graph_time, self.graph_vel, pen='k', antialias=True) # , downsample=1, downsampleMethod='mean' ax_y = self.graph.getAxis('left') ax_y.setTickSpacing(major=5000, minor=1000)
[docs] @pyqtSlot() def connect_btn_cb(self): """ Callback for the connect button. """ if self.gram is None or not self.connected: self.connect() else: self.disconnect()
@property def selected_gramophone(self): return self.gram_list[int(self.gram_dropdown.currentData(0), 16)]
[docs] def connect(self): """ Connects to the currently selected Gramophone. """ if self.gram is None or not self.connected: self.connected = True self.gram = self.selected_gramophone self.reader = Reader(self.gram, self.settings['sampling_freq']) self.reader.start() self.reader.recorder_signal.connect(self.receiver) self.reader.device_error.connect(self.gramophone_error) self.update_conn_state()
[docs] def disconnect(self): """ Disconnects the currently connected Gramophone. """ if self.connected: self.connected = False self.reader.stop() self.reader.recorder_signal.disconnect(self.receiver) self.reader.device_error.disconnect(self.gramophone_error) self.update_conn_state()
[docs] @pyqtSlot() def refresh_gram_list(self): """ Refreshes the list of available Gramophones. """ self.gram_list = Comms.find_devices() product_serials = list(self.gram_list.keys()) if self.gram_list: self.connect_btn.setProperty("enabled", True) self.gram_info_btn.setProperty("enabled", True) self.gram_dropdown.clear() self.gram_dropdown.addItems(list(map(hex, product_serials))) else: self.gram_dropdown.clear() self.connect_btn.setProperty("enabled", False) self.gram_info_btn.setProperty("enabled", False)
[docs] @pyqtSlot() def plot_btn_cb(self): """ Callback for the Plot button. """ self.log_model.plot_record(self.selected_rows)
[docs] @pyqtSlot() def delete_btn_cb(self): """ Callback for the Delete button. """ if self.delete_warning(len(self.selected_rows)) == 'delete': while self.selected_rows: self.log_model.removeRows(self.selected_rows[0], 1) self.setWindowModified(True) else: pass
@property def selected_rows(self): """ A property that holds a list of the selected rows of the current velocity log. """ return [index.row() for index in self.records_table.selectionModel().selectedRows()]
[docs] def update_conn_state(self): """ Updates the GUI depending on wheter the software is currently connected to a Gramophone. """ self.reset_graph() self.update_timer(0) if self.connected: # Change GUI for connected state self.settings_win.close() self.gram_dropdown.setEnabled(False) self.refresh_btn.setEnabled(False) self.settings_btn.setEnabled(False) self.out_1_btn.setEnabled(True) self.out_2_btn.setEnabled(True) self.out_3_btn.setEnabled(True) self.out_4_btn.setEnabled(True) self.connect_btn.setProperty("text", "Disconnect") self.statusbar.showMessage('Connected to Gramophone ' + hex(self.gram.product_info['serial']), 3000) else: # Change GUI for disconnected state self.gram_dropdown.setEnabled(True) self.refresh_btn.setEnabled(True) self.settings_btn.setEnabled(True) self.out_1_btn.setEnabled(False) self.out_2_btn.setEnabled(False) self.out_3_btn.setEnabled(False) self.out_4_btn.setEnabled(False) self.connect_btn.setProperty("text", "Connect") self.refresh_gram_list() self.statusbar.showMessage('Disconnected from Gramophone ' + hex(self.gram.product_info['serial']), 3000)
@pyqtSlot(int, float, int, int, int, int, int, int) def receiver(self, timer, velocity, in_1, in_2, out_1, out_2, out_3, out_4): self.update_timer(timer/10) self.update_graph(-velocity) self.update_rec_state(in_1, in_2) self.update_output_state(out_1, out_2, out_3, out_4) if self.recording: if self.settings['trigger_channel'] == 1: current_state = in_1 if self.settings['trigger_channel'] == 2: current_state = in_2 self.current_record.append(timer/10, -velocity, current_state)
[docs] def update_timer(self, millis): """ Slot for the time_signal of the Gramophone. Updates the timer on the GUI. """ millis -= self.timer_zero seconds = int((millis / 1000) % 60) minutes = int((millis / (1000 * 60)) % 60) # hours=(millis/(1000*60*60))%24 self.time_label.setText(str(minutes).zfill( 2) + ':' + str(seconds).zfill(2))
[docs] def update_graph(self, velocity): ''' Slot for the vel_signal of the Gramophone. Shifts the graph to the right and appends with the value received. ''' # self.vel_window.append(velocity) self.graph_time.append(time.time()) # self.graph_vel.append(mean(self.vel_window)) self.graph_vel.append(velocity) if self.recording: self.curve.setPen(color='r', width=2) else: self.curve.setPen(color='k', width=2) # self.curve.setShadowPen(color=0.5, width=3) self.curve.setData(x=list(self.graph_time), y=list(self.graph_vel)) self.graph.setXRange( self.graph_time[-1] - 10, self.graph_time[-1]) # last 10 seconds
[docs] def update_rec_state(self, in_state_1, in_state_2): """ Slot for the rec_signal of the Grmaophone. Updates the state of recording and the GUI. """ if self.settings['trigger_channel'] == 1: target_state = in_state_1 if self.settings['trigger_channel'] == 2: target_state = in_state_2 if self.recording != bool(target_state): self.recording = bool(target_state) self.timer_zero = self.gram.read_time()/10 if self.recording: # self.gram.reset_time() self.current_record = logger.MemoryRecord( self.counter_box.value(), self.settings['sampling_freq'], self.gram.product_info['serial']) self.current_record.start() # Update GUI self.statusbar.showMessage('Recording started', 3000) self.connect_btn.setEnabled(False) self.counter_box.setEnabled(False) else: self.current_record.finish() self.log_model.add_record(self.current_record) # Update GUI self.statusbar.showMessage('Recording finished', 3000) self.log_changed() self.update_table_size() self.connect_btn.setEnabled(True) self.counter_box.setEnabled(True) self.counter_box.stepUp() if self.autosave:
def update_output_state(self, out_1, out_2, out_3, out_4): self.out_1_btn.setChecked(bool(out_1)) self.out_2_btn.setChecked(bool(out_2)) self.out_3_btn.setChecked(bool(out_3)) self.out_4_btn.setChecked(bool(out_4))
[docs] @pyqtSlot(str) def gramophone_error(self, error_message): """ Slot for the errorOccurred signal of the Gramophone. Displays a message box with the warning, disconnects if necessary. """ self.disconnect() msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Critical) msg_box.setWindowTitle("Connection error!") msg_box.setText("The connection to the Gramophone was lost.") msg_box.setInformativeText(error_message) msg_box.setStandardButtons(QMessageBox.Ok) msg_box.setDefaultButton(QMessageBox.Ok) msg_box.exec()
[docs] @pyqtSlot() def update_table_size(self): ''' Update all except last the row's width to fit content in the list of velocity recordings. ''' for I in range(len(self.log_model.headers)-1): self.records_table.resizeColumnToContents(I)
[docs] def record_warning(self): """ Displays a warning if the recording is in progress. Call it if the user tries to exit. """ if self.recording: msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Warning) msg_box.setWindowTitle("Recording in progress") msg_box.setText("Recording is currently in progress.") msg_box.setInformativeText("Are you sure you want to exit?") msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel) msg_box.setDefaultButton(QMessageBox.Cancel) choice = msg_box.exec() if choice == QMessageBox.Yes: return 'ignore' if choice == QMessageBox.Cancel: return 'cancel' else: return 'safe'
[docs] def save_warning(self): """ Displays a warning if there are unsaved changes to the log file. Call it if the user tries to exit. """ if self.isWindowModified(): msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Warning) msg_box.setWindowTitle("Unsaved changes") msg_box.setText("Do you want to save the changes" "to this log before closing it?") msg_box.setInformativeText("If you don't save, " "your changes will be lost.") msg_box.setStandardButtons( QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) msg_box.setDefaultButton(QMessageBox.Save) choice = msg_box.exec() if choice == QMessageBox.Save: return 'save' if choice == QMessageBox.Discard: return 'discard' if choice == QMessageBox.Cancel: return 'cancel' else: return 'safe'
[docs] def delete_warning(self, count): """ Displays a message box to ask for confirmation. Call it if the user is trying to delete velocity records. """ msg_box = QMessageBox(self) msg_box.setIcon(QMessageBox.Warning) msg_box.setWindowTitle("Delete records") msg_box.setText("Are you sure you want to delete the " + str(count)+" selected record(s).") # msg_box.setInformativeText("") msg_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) msg_box.setDefaultButton(QMessageBox.No) choice = msg_box.exec() if choice == QMessageBox.Yes: return 'delete' if choice == QMessageBox.Discard: return 'keep'
[docs] def closeEvent(self, event): ''' The built in close event of QMainWindow. User is trying to close the program. Ignore the passed event if you want to stop the user from doing so. ''' record_response = self.record_warning() if record_response == 'cancel': event.ignore() if record_response in ['ignore', 'safe']: save_response = self.save_warning() if save_response == 'save': saved = if not saved: print('not saved') event.ignore() else: print('saved') self.closeEvent(event) if save_response == 'cancel': event.ignore() if save_response in ['discard', 'safe']: self.quit()
[docs] def quit(self): """ Close all windows and disconnrect form the Gramohone. Call this right before exiting. """ # sys.exit() self.about_win.close() self.license_win.close() self.settings_win.close() if self.gram is not None: self.disconnect() self.log.close_log_file()
# Dev functions
[docs] @pyqtSlot() def reset_gram_timer(self): """ Function only available for developers. Sends a Reset internal clock command to the Gramophone. """ self.gram.reset_time()
[docs] @pyqtSlot() def make_dummy(self): """ Function only available for developers. Creates a dummy record in the current velocity log with randomized data. """ dummy = logger.DummyRecord() self.log_model.add_record(dummy) self.update_table_size() self.log_changed() if self.autosave:
[docs]class VelLogModel(QAbstractTableModel): """ QT modell for velocity logs. Used to make an interface between the GUI's table view and the Velocity log object """ def __init__(self, log, parent=None): super().__init__(parent) self.log = log # self.records = self.log.records self.headers = ['ID', 'Date', 'Start', 'Finish', 'Length', 'Mean velocity', 'Comment']
[docs] def rowCount(self, parent): """ Return the number of rows to be displayed. one row for each recording. """ return len(self.log.records)
[docs] def columnCount(self, parent): ''' Return the number of columns to be displayed. One column for each header. ''' return len(self.headers)
[docs] def data(self, index, role): """ Called to receive information about a cell of the table. """ if role == QtCore.Qt.DisplayRole: if index.column() == 0: return self.log.records[index.row()].rec_id if index.column() == 1: return self.log.records[index.row()].date_hr if index.column() == 2: return self.log.records[index.row()].start_time_hr if index.column() == 3: return self.log.records[index.row()].finish_time_hr if index.column() == 4: return self.log.records[index.row()].length_hr if index.column() == 5: return str(self.log.records[index.row()].mean_vel)[0:10] if index.column() == 6: return self.log.records[index.row()].comment if role == QtCore.Qt.EditRole: if index.column() == 0: return self.log.records[index.row()].rec_id if index.column() == 6: return self.log.records[index.row()].comment if role == QtCore.Qt.SizeHintRole: if index.column() == 0: return QtCore.QSize(40, 30) if index.column() == 1: return QtCore.QSize(65, 30) if index.column() == 2: return QtCore.QSize(50, 30) if index.column() == 3: return QtCore.QSize(50, 30) if index.column() == 4: return QtCore.QSize(50, 30) if index.column() == 5: return QtCore.QSize(85, 30) if index.column() == 6: return QtCore.QSize(100, 30)
[docs] def setData(self, index, value, role=QtCore.Qt.EditRole): """ Called to set the data in a cell of the table. """ if role == QtCore.Qt.EditRole: if index.column() == 0: self.log.records[index.row()].rec_id = value self.dataChanged.emit(index, index) return True if role == QtCore.Qt.EditRole: if index.column() == 6: self.log.records[index.row()].comment = value self.dataChanged.emit(index, index) return True return False
[docs] def headerData(self, section, orientation, role): """ Returns the haeder data the GUI is asking for. """ if role == QtCore.Qt.DisplayRole: if orientation == QtCore.Qt.Horizontal: return self.headers[section] if orientation == QtCore.Qt.Vertical: return section+1
[docs] def flags(self, index): """ Returns flag to decide which cells are editable. """ if index.column() in [0, 6]: # List of editable columns return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable else: return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
[docs] def add_record(self, record): """ Adds the given Record object the the current velocity log. """ record_count = len(self.log.records) self.beginInsertRows(QModelIndex(), record_count, record_count) self.log.records.append(record) self.endInsertRows() return True
[docs] def removeRows(self, row, count, parent=QModelIndex()): """ Removes 'count' number of rows starting at 'row' """ self.beginRemoveRows(parent, row, row+count-1) self.log.deleted += self.log.records[row:row+count] self.log.records = self.log.records[:row] + \ self.log.records[row+count:] self.endRemoveRows()
[docs] def plot_record(self, selection): """ Plots the selected record(s) using Matplotlib. """ plt.figure() selected_records = [self.log.records[row] for row in selection] for rec in selected_records: # print(rec.mean_vel) # print(rec.rec_id) plt.plot(rec.times, rec.velocities, label=str(rec.rec_id)+' '+rec.comment) plt.xlabel('Time (ms)') plt.ylabel('Velocity (a.u.)') plt.legend()
[docs]class Reader(QObject): """ A worker that can continnously send commands to the Gramophone with a given frequency. :param name: The name of this reader. Can be used for identification to stop a specific reader. :type name: str :param read_func: The read function that should be called. :type read_func: function :param frequency: The frequency at which the function should be called in Hz. :type frequency: float """ recorder_signal = pyqtSignal(int, float, int, int, int, int, int, int) device_error = pyqtSignal(str) def __init__(self, gram, frequency): super().__init__() self.read_func = gram.read_recorder_params self.frequency = frequency self.reading = None
[docs] @pyqtSlot() def read(self): """ Start calling the read function repeatedly. Slot for a QThread. """ self.reading = True while self.reading: try: result = self.read_func() except Comms.GramophoneError as err: print('Gramophone comm. error:', err) self.device_error.emit(str(err)) break else: gtime = result[0x05] vel = result[0x11] in_1 = result[0x20] in_2 = result[0x21] out_1 = result[0x30] out_2 = result[0x31] out_3 = result[0x32] out_4 = result[0x33] self.recorder_signal.emit( gtime, vel, in_1, in_2, out_1, out_2, out_3, out_4) time.sleep(1/self.frequency)
def start(self): self.thread = QThread() # self.readers.append((thread, reader)) self.moveToThread(self.thread) self.thread.started.connect( self.thread.start() def stop(self): self.reading = False self.thread.quit() self.thread.wait()
def main(devmode=False, log_file=None): if not os.path.exists(PROGRAM_DATA + '/GramophoneTools'): os.makedirs(PROGRAM_DATA + '/GramophoneTools') APP = QApplication(sys.argv) WIN = pyGramWindow(devmode, log_file) # WIN.show_elements() sys.exit(APP.exec_()) if __name__ == '__main__': main()