""" 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__))
sys.path.append(DIR+'/.')
# GLOBALS
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')
PROGRAM_DATA = os.getenv('ALLUSERSPROFILE')
[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 = shelve.open(PROGRAM_DATA+'/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 shelve.open(PROGRAM_DATA+'/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.open)
self.actionSave.triggered.connect(self.save)
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.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.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 self.save()
else:
return False
else:
self.log.save()
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():
self.save()
[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.save()
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.show()
self.about_win.activateWindow()
[docs] def show_license(self):
""" Displays the License window. """
self.license_win.show()
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.show()
self.settings_win.activateWindow()
def show_device_info(self):
self.device_info_win = deviceInfoWindow(self.selected_gramophone)
self.device_info_win.show()
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:
self.save()
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 = self.save()
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:
self.save()
[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 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()
plt.show()
[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.read)
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()
WIN.show()
sys.exit(APP.exec_())
if __name__ == '__main__':
main()