class Run_task_tab(QtGui.QWidget):
    def __init__(self, parent=None):
        super(QtGui.QWidget, self).__init__(parent)

        # Variables.
        self.GUI_main = self.parent()
        self.board = None  # Pycboard class instance.
        self.task = None  # Task currently uploaded on pyboard.
        self.task_hash = None  # Used to check if file has changed.
        self.data_dir = None
        self.connected = False  # Whether gui is conencted to pyboard.
        self.uploaded = False  # Whether selected task file is on board.
        self.fresh_task = None  # Whether task has been run or variables edited.
        self.running = False
        self.subject_changed = False
        self.variables_dialog = None

        # GUI groupbox.

        self.status_groupbox = QtGui.QGroupBox('Status')

        self.status_text = QtGui.QLineEdit('Not connected')
        self.status_text.setReadOnly(True)

        self.guigroup_layout = QtGui.QHBoxLayout()
        self.guigroup_layout.addWidget(self.status_text)
        self.status_groupbox.setLayout(self.guigroup_layout)

        # Board groupbox

        self.board_groupbox = QtGui.QGroupBox('Setup')

        self.board_label = QtGui.QLabel('Select:')
        self.board_select = QtGui.QComboBox()
        self.board_select.setEditable(True)
        self.board_select.setFixedWidth(100)
        self.connect_button = QtGui.QPushButton('Connect')
        self.config_button = QtGui.QPushButton('Config')

        self.boardgroup_layout = QtGui.QHBoxLayout()
        self.boardgroup_layout.addWidget(self.board_label)
        self.boardgroup_layout.addWidget(self.board_select)
        self.boardgroup_layout.addWidget(self.connect_button)
        self.boardgroup_layout.addWidget(self.config_button)
        self.board_groupbox.setLayout(self.boardgroup_layout)

        self.connect_button.clicked.connect(
            lambda: self.disconnect() if self.connected else self.connect())
        self.config_button.clicked.connect(self.open_config_dialog)

        # File groupbox

        self.file_groupbox = QtGui.QGroupBox('Data file')

        self.data_dir_label = QtGui.QLabel('Data dir:')
        self.data_dir_text = QtGui.QLineEdit(data_dir)
        self.data_dir_button = QtGui.QPushButton('...')
        self.data_dir_button.setFixedWidth(30)
        self.subject_label = QtGui.QLabel('Subject ID:')
        self.subject_text = QtGui.QLineEdit()
        self.subject_text.setFixedWidth(80)
        self.subject_text.setMaxLength(12)

        self.filegroup_layout = QtGui.QHBoxLayout()
        self.filegroup_layout.addWidget(self.data_dir_label)
        self.filegroup_layout.addWidget(self.data_dir_text)
        self.filegroup_layout.addWidget(self.data_dir_button)
        self.filegroup_layout.addWidget(self.subject_label)
        self.filegroup_layout.addWidget(self.subject_text)
        self.file_groupbox.setLayout(self.filegroup_layout)

        self.data_dir_text.textChanged.connect(self.test_data_path)
        self.data_dir_button.clicked.connect(self.select_data_dir)
        self.subject_text.textChanged.connect(self.test_data_path)

        # Task groupbox

        self.task_groupbox = QtGui.QGroupBox('Task')

        self.task_label = QtGui.QLabel('Task:')
        self.task_select = QtGui.QComboBox()
        self.upload_button = QtGui.QPushButton('Upload')
        self.variables_button = QtGui.QPushButton('Variables')

        self.taskgroup_layout = QtGui.QHBoxLayout()
        self.taskgroup_layout.addWidget(self.task_label)
        self.taskgroup_layout.addWidget(self.task_select)
        self.taskgroup_layout.addWidget(self.upload_button)
        self.taskgroup_layout.addWidget(self.variables_button)
        self.task_groupbox.setLayout(self.taskgroup_layout)

        self.task_select.currentTextChanged.connect(self.task_changed)
        self.upload_button.clicked.connect(self.setup_task)

        # Session groupbox.

        self.session_groupbox = QtGui.QGroupBox('Session')

        self.start_button = QtGui.QPushButton('Start')
        self.stop_button = QtGui.QPushButton('Stop')

        self.sessiongroup_layout = QtGui.QHBoxLayout()
        self.sessiongroup_layout.addWidget(self.start_button)
        self.sessiongroup_layout.addWidget(self.stop_button)
        self.session_groupbox.setLayout(self.sessiongroup_layout)

        self.start_button.clicked.connect(self.start_task)
        self.stop_button.clicked.connect(self.stop_task)

        # Log text and task plots.

        self.log_textbox = QtGui.QTextEdit()
        self.log_textbox.setFont(QtGui.QFont('Courier', 9))
        self.log_textbox.setReadOnly(True)

        self.task_plot = Task_plot()
        self.data_logger = Data_logger(print_func=self.print_to_log,
                                       data_consumers=[self.task_plot])

        # Main layout

        self.vertical_layout = QtGui.QVBoxLayout()
        self.horizontal_layout_1 = QtGui.QHBoxLayout()
        self.horizontal_layout_2 = QtGui.QHBoxLayout()
        self.horizontal_layout_3 = QtGui.QHBoxLayout()

        self.horizontal_layout_1.addWidget(self.status_groupbox)
        self.horizontal_layout_1.addWidget(self.board_groupbox)
        self.horizontal_layout_2.addWidget(self.file_groupbox)
        self.horizontal_layout_3.addWidget(self.task_groupbox)
        self.horizontal_layout_3.addWidget(self.session_groupbox)
        self.vertical_layout.addLayout(self.horizontal_layout_1)
        self.vertical_layout.addLayout(self.horizontal_layout_2)
        self.vertical_layout.addLayout(self.horizontal_layout_3)
        self.vertical_layout.addWidget(self.log_textbox, 20)
        self.vertical_layout.addWidget(self.task_plot, 80)
        self.setLayout(self.vertical_layout)

        # Create timers

        self.update_timer = QtCore.QTimer(
        )  # Timer to regularly call update() during run.
        self.update_timer.timeout.connect(self.update)

        # Initial setup.

        self.disconnect()  # Set initial state as disconnected.

    # General methods

    def print_to_log(self, print_string, end='\n'):
        self.log_textbox.moveCursor(QtGui.QTextCursor.End)
        self.log_textbox.insertPlainText(print_string + end)
        self.log_textbox.moveCursor(QtGui.QTextCursor.End)
        self.GUI_main.app.processEvents(
        )  # To update gui during long operations that print progress.

    def test_data_path(self):
        # Checks whether data dir and subject ID are valid.
        self.data_dir = self.data_dir_text.text()
        subject_ID = self.subject_text.text()
        if os.path.isdir(self.data_dir) and subject_ID:
            self.start_button.setText('Record')
            return True
        else:
            self.start_button.setText('Start')
            return False

    def refresh(self):
        # Called regularly when framework not running.
        if self.GUI_main.setups_tab.available_setups_changed:
            self.board_select.clear()
            self.board_select.addItems(self.GUI_main.setups_tab.setup_names)
        if self.GUI_main.available_tasks_changed:
            self.task_select.clear()
            self.task_select.addItems(sorted(self.GUI_main.available_tasks))
        if self.task:
            try:
                task_path = os.path.join(tasks_dir, self.task + '.py')
                if not self.task_hash == _djb2_file(
                        task_path):  # Task file modified.
                    self.task_changed()
            except FileNotFoundError:
                pass

    def open_config_dialog(self):
        '''Open the config dialog and update GUI as required by chosen config.'''
        self.GUI_main.config_dialog.exec_(self.board)
        self.task_changed()
        if self.GUI_main.config_dialog.disconnect:
            self.disconnect()

    # Widget methods.

    def connect(self):
        # Connect to pyboard.
        try:
            self.status_text.setText('Connecting...')
            self.board_select.setEnabled(False)
            self.variables_button.setEnabled(False)
            self.connect_button.setEnabled(False)
            self.repaint()
            port = self.GUI_main.setups_tab.get_port(
                self.board_select.currentText())
            self.board = Pycboard(port,
                                  print_func=self.print_to_log,
                                  data_logger=self.data_logger)
            self.connected = True
            self.config_button.setEnabled(True)
            self.task_groupbox.setEnabled(True)
            self.connect_button.setEnabled(True)
            self.connect_button.setText('Disconnect')
            self.status_text.setText('Connected')
        except SerialException:
            self.status_text.setText('Connection failed')
            self.print_to_log('Connection failed.')
            self.connect_button.setEnabled(True)
        if self.connected and not self.board.status['framework']:
            self.board.load_framework()

    def disconnect(self):
        # Disconnect from pyboard.
        if self.board: self.board.close()
        self.board = None
        self.task_groupbox.setEnabled(False)
        self.file_groupbox.setEnabled(False)
        self.session_groupbox.setEnabled(False)
        self.config_button.setEnabled(False)
        self.board_select.setEnabled(True)
        self.connect_button.setText('Connect')
        self.status_text.setText('Not connected')
        self.task_changed()
        self.connected = False

    def task_changed(self):
        self.uploaded = False
        self.upload_button.setText('Upload')
        self.start_button.setEnabled(False)

    def setup_task(self):
        try:
            task = self.task_select.currentText()
            if self.uploaded:
                self.status_text.setText('Resetting task..')
            else:
                self.status_text.setText('Uploading..')
                self.task_hash = _djb2_file(
                    os.path.join(tasks_dir, task + '.py'))
            self.start_button.setEnabled(False)
            self.variables_button.setEnabled(False)
            self.repaint()
            self.board.setup_state_machine(task, uploaded=self.uploaded)
            if self.variables_dialog:
                self.variables_button.clicked.disconnect()
                self.variables_dialog.deleteLater()
            self.variables_dialog = Variables_dialog(self, self.board)
            self.variables_button.clicked.connect(self.variables_dialog.exec_)
            self.variables_button.setEnabled(True)
            self.task_plot.set_state_machine(self.board.sm_info)
            self.file_groupbox.setEnabled(True)
            self.session_groupbox.setEnabled(True)
            self.start_button.setEnabled(True)
            self.stop_button.setEnabled(False)
            self.status_text.setText('Uploaded : ' + task)
            self.task = task
            self.fresh_task = True
            self.uploaded = True
            self.upload_button.setText('Reset')
        except PyboardError:
            self.status_text.setText('Error setting up state machine.')

    def select_data_dir(self):
        self.data_dir_text.setText(
            QtGui.QFileDialog.getExistingDirectory(self, 'Select data folder',
                                                   data_dir))

    def start_task(self):
        recording = self.test_data_path()
        if recording:
            if not self.fresh_task:
                reset_task = QtGui.QMessageBox.question(
                    self, 'Reset task',
                    'Task has already been run, variables may not have default values.\n\nReset task?',
                    QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
                if reset_task == QtGui.QMessageBox.Yes:
                    self.setup_task()
                    return
            subject_ID = str(self.subject_text.text())
            self.data_logger.open_data_file(self.data_dir, 'run_task',
                                            subject_ID)
        self.fresh_task = False
        self.running = True
        self.board.start_framework()
        self.task_plot.run_start(recording)
        self.task_select.setEnabled(False)
        self.upload_button.setEnabled(False)
        self.file_groupbox.setEnabled(False)
        self.start_button.setEnabled(False)
        self.board_groupbox.setEnabled(False)
        self.stop_button.setEnabled(True)
        self.print_to_log('\nRun started at: {}\n'.format(
            datetime.now().strftime('%Y/%m/%d %H:%M:%S')))
        self.update_timer.start(update_interval)
        self.GUI_main.refresh_timer.stop()
        self.status_text.setText('Running: ' + self.task)
        self.GUI_main.tab_widget.setTabEnabled(
            1, False)  # Disable experiments tab.
        self.GUI_main.tab_widget.setTabEnabled(2, False)  # Disable setups tab.

    def stop_task(self, error=False, stopped_by_task=False):
        self.running = False
        self.update_timer.stop()
        self.GUI_main.refresh_timer.start(self.GUI_main.refresh_interval)
        if not (error or stopped_by_task):
            self.board.stop_framework()
            QtCore.QTimer.singleShot(
                100, self.update)  # Catch output after framework stops.
        self.data_logger.close_files()
        self.task_plot.run_stop()
        self.board_groupbox.setEnabled(True)
        self.file_groupbox.setEnabled(True)
        self.start_button.setEnabled(True)
        self.task_select.setEnabled(True)
        self.upload_button.setEnabled(True)
        self.stop_button.setEnabled(False)
        self.status_text.setText('Uploaded : ' + self.task)
        self.GUI_main.tab_widget.setTabEnabled(1, True)  # Enable setups tab.
        self.GUI_main.tab_widget.setTabEnabled(2, True)  # Enable setups tab.

    # Timer updates

    def update(self):
        # Called regularly during run to process data from board and update plots.
        try:
            self.board.process_data()
            if not self.board.framework_running:
                self.stop_task(stopped_by_task=True)
        except PyboardError:
            self.print_to_log('\nError during framework run.')
            self.stop_task(error=True)
        self.task_plot.update()

    # Cleanup.

    def closeEvent(self, event):
        # Called when GUI window is closed.
        if self.board:
            self.board.stop_framework()
            self.board.close()
        event.accept()

    # Exception handling.

    def excepthook(self, ex_type, ex_value, ex_traceback):
        # Called whenever an uncaught exception occurs.
        if ex_type in (SerialException, SerialTimeoutException):
            self.print_to_log('\nError: Serial connection with board lost.')
        elif ex_type == PyboardError:
            self.print_to_log('\nError: Unable to execute command.')
        else:
            self.print_to_log(
                '\nError: uncaught exception of type: {}'.format(ex_type))
        if self.running:
            self.stop_task(error=True)
        self.disconnect()
Exemple #2
0
class Run_task_tab(QtGui.QWidget):
    def __init__(self, parent=None):
        super(QtGui.QWidget, self).__init__(parent)

        # Variables.
        self.GUI_main = self.parent()
        self.board = None  # Pycboard class instance.
        self.task = None  # Task currently uploaded on pyboard.
        self.task_hash = None  # Used to check if file has changed.
        self.data_dir = None  # Folder to save data files.
        self.custom_dir = False  # True if data_dir field has been changed from default.
        self.connected = False  # Whether gui is conencted to pyboard.
        self.uploaded = False  # Whether selected task file is on board.
        self.fresh_task = None  # Whether task has been run or variables edited.
        self.running = False
        self.subject_changed = False
        self.variables_dialog = None

        # GUI groupbox.

        self.status_groupbox = QtGui.QGroupBox("Status")

        self.status_text = QtGui.QLineEdit("Not connected")
        self.status_text.setReadOnly(True)

        self.guigroup_layout = QtGui.QHBoxLayout()
        self.guigroup_layout.addWidget(self.status_text)
        self.status_groupbox.setLayout(self.guigroup_layout)

        # Board groupbox

        self.board_groupbox = QtGui.QGroupBox("Setup")

        self.board_select = QtGui.QComboBox()
        self.board_select.addItems(["No setups found"])
        self.board_select.setSizeAdjustPolicy(0)
        self.connect_button = QtGui.QPushButton("Connect")
        self.connect_button.setIcon(QtGui.QIcon("gui/icons/connect.svg"))
        self.connect_button.setEnabled(False)
        self.config_button = QtGui.QPushButton("Config")
        self.config_button.setIcon(QtGui.QIcon("gui/icons/settings.svg"))

        self.boardgroup_layout = QtGui.QHBoxLayout()
        self.boardgroup_layout.addWidget(self.board_select)
        self.boardgroup_layout.addWidget(self.connect_button)
        self.boardgroup_layout.addWidget(self.config_button)
        self.board_groupbox.setLayout(self.boardgroup_layout)

        self.connect_button.clicked.connect(
            lambda: self.disconnect() if self.connected else self.connect())
        self.config_button.clicked.connect(self.open_config_dialog)

        # File groupbox

        self.file_groupbox = QtGui.QGroupBox("Data file")

        self.data_dir_label = QtGui.QLabel("Data dir:")
        self.data_dir_label.setAlignment(QtCore.Qt.AlignRight
                                         | QtCore.Qt.AlignVCenter)
        self.data_dir_text = QtGui.QLineEdit(dirs["data"])
        self.data_dir_button = QtGui.QPushButton()
        self.data_dir_button.setIcon(QtGui.QIcon("gui/icons/folder.svg"))
        self.data_dir_button.setFixedWidth(30)
        self.subject_label = QtGui.QLabel("Subject ID:")
        self.subject_text = QtGui.QLineEdit()

        self.filegroup_layout = QtGui.QGridLayout()
        self.filegroup_layout.addWidget(self.data_dir_label, 0, 0)
        self.filegroup_layout.addWidget(self.data_dir_text, 0, 1)
        self.filegroup_layout.addWidget(self.data_dir_button, 0, 2)
        self.filegroup_layout.addWidget(self.subject_label, 1, 0)
        self.filegroup_layout.addWidget(self.subject_text, 1, 1)
        self.file_groupbox.setLayout(self.filegroup_layout)

        self.data_dir_text.textChanged.connect(self.test_data_path)
        self.data_dir_text.textEdited.connect(
            lambda: setattr(self, "custom_dir", True))
        self.data_dir_button.clicked.connect(self.select_data_dir)
        self.subject_text.textChanged.connect(self.test_data_path)

        # Task groupbox

        self.task_groupbox = QtGui.QGroupBox("Task")

        self.task_select = TaskSelectMenu("select task")
        self.task_select.set_callback(self.task_changed)
        self.upload_button = QtGui.QPushButton("Upload")
        self.upload_button.setIcon(
            QtGui.QIcon("gui/icons/circle-arrow-up.svg"))
        self.variables_button = QtGui.QPushButton("Variables")
        self.variables_button.setIcon(QtGui.QIcon("gui/icons/filter.svg"))

        self.taskgroup_layout = QtGui.QGridLayout()
        self.taskgroup_layout.addWidget(self.task_select, 0, 0, 1, 2)
        self.taskgroup_layout.addWidget(self.upload_button, 1, 0)
        self.taskgroup_layout.addWidget(self.variables_button, 1, 1)
        self.task_groupbox.setLayout(self.taskgroup_layout)

        self.upload_button.clicked.connect(self.setup_task)

        # Session groupbox.

        self.session_groupbox = QtGui.QGroupBox("Session")

        self.start_button = QtGui.QPushButton("Start")
        self.start_button.setIcon(QtGui.QIcon("gui/icons/play.svg"))
        self.stop_button = QtGui.QPushButton("Stop")
        self.stop_button.setIcon(QtGui.QIcon("gui/icons/stop.svg"))

        self.task_info = TaskInfo()

        self.sessiongroup_layout = QtGui.QGridLayout()
        self.sessiongroup_layout.addWidget(self.task_info.print_label, 0, 1)
        self.sessiongroup_layout.addWidget(self.task_info.print_text, 0, 2, 1,
                                           3)
        self.sessiongroup_layout.addWidget(self.task_info.state_label, 1, 1)
        self.sessiongroup_layout.addWidget(self.task_info.state_text, 1, 2)
        self.sessiongroup_layout.addWidget(self.task_info.event_label, 1, 3)
        self.sessiongroup_layout.addWidget(self.task_info.event_text, 1, 4)
        self.sessiongroup_layout.addWidget(self.start_button, 0, 0)
        self.sessiongroup_layout.addWidget(self.stop_button, 1, 0)
        self.session_groupbox.setLayout(self.sessiongroup_layout)

        self.start_button.clicked.connect(self.start_task)
        self.stop_button.clicked.connect(self.stop_task)

        # Log text and task plots.

        self.log_textbox = QtGui.QTextEdit()
        self.log_textbox.setFont(QtGui.QFont("Courier New", log_font_size))
        self.log_textbox.setReadOnly(True)

        self.task_plot = Task_plot()
        self.data_logger = Data_logger(
            print_func=self.print_to_log,
            data_consumers=[self.task_plot, self.task_info])

        # Main layout

        self.vertical_layout = QtGui.QVBoxLayout()
        self.horizontal_layout_1 = QtGui.QHBoxLayout()
        self.horizontal_layout_2 = QtGui.QHBoxLayout()
        self.horizontal_layout_3 = QtGui.QHBoxLayout()

        self.horizontal_layout_1.addWidget(self.status_groupbox)
        self.horizontal_layout_1.addWidget(self.board_groupbox)
        self.horizontal_layout_2.addWidget(self.task_groupbox)
        self.horizontal_layout_2.addWidget(self.file_groupbox)
        self.horizontal_layout_3.addWidget(self.session_groupbox)
        self.vertical_layout.addLayout(self.horizontal_layout_1)
        self.vertical_layout.addLayout(self.horizontal_layout_2)
        self.vertical_layout.addLayout(self.horizontal_layout_3)
        self.vertical_layout.addWidget(self.log_textbox, 20)
        self.vertical_layout.addWidget(self.task_plot, 80)
        self.setLayout(self.vertical_layout)

        # Create timers

        self.update_timer = QtCore.QTimer(
        )  # Timer to regularly call update() during run.
        self.update_timer.timeout.connect(self.update)

        # Keyboard Shortcuts

        shortcut_dict = {
            "t":
            lambda: self.task_select.showMenu(),
            "u":
            lambda: self.setup_task(),
            "Space": (lambda: self.stop_task()
                      if self.running else self.start_task()
                      if self.uploaded else None),
        }

        init_keyboard_shortcuts(self, shortcut_dict)

        # Initial setup.

        self.disconnect()  # Set initial state as disconnected.

    # General methods

    def print_to_log(self, print_string, end="\n"):
        self.log_textbox.moveCursor(QtGui.QTextCursor.End)
        self.log_textbox.insertPlainText(print_string + end)
        self.log_textbox.moveCursor(QtGui.QTextCursor.End)
        self.GUI_main.app.processEvents(
        )  # To update gui during long operations that print progress.

    def test_data_path(self):
        # Checks whether data dir and subject ID are valid.
        self.data_dir = self.data_dir_text.text()
        subject_ID = self.subject_text.text()
        if os.path.isdir(self.data_dir) and subject_ID:
            self.start_button.setText("Record")
            self.start_button.setIcon(QtGui.QIcon("gui/icons/record.svg"))
            return True
        else:
            self.start_button.setText("Start")
            self.start_button.setIcon(QtGui.QIcon("gui/icons/play.svg"))
            return False

    def refresh(self):
        # Called regularly when framework not running.
        if self.GUI_main.setups_tab.available_setups_changed:
            self.board_select.clear()
            if self.GUI_main.setups_tab.setup_names:
                self.board_select.addItems(
                    self.GUI_main.setups_tab.setup_names)
                if not self.connected:
                    self.connect_button.setEnabled(True)
            else:  # No setups available to connect to.
                self.board_select.addItems(["No setups found"])
                self.connect_button.setEnabled(False)
        if self.GUI_main.available_tasks_changed:
            self.task_select.update_menu(dirs["tasks"])
        if self.GUI_main.data_dir_changed and not self.custom_dir:
            self.data_dir_text.setText(dirs["data"])
        if self.task:
            try:
                task_path = os.path.join(dirs["tasks"], self.task + ".py")
                if not self.task_hash == _djb2_file(
                        task_path):  # Task file modified.
                    self.task_changed()
            except FileNotFoundError:
                pass

    def open_config_dialog(self):
        """Open the config dialog and update GUI as required by chosen config."""
        self.GUI_main.config_dialog.exec_(self.board)
        self.task_changed()
        if self.GUI_main.config_dialog.disconnect:
            time.sleep(0.5)
            self.GUI_main.refresh()
            self.disconnect()
        if self.connected and self.board.status["framework"]:
            self.task_groupbox.setEnabled(True)

    # Widget methods.

    def connect(self):
        # Connect to pyboard.
        try:
            self.status_text.setText("Connecting...")
            self.board_select.setEnabled(False)
            self.variables_button.setEnabled(False)
            self.connect_button.setEnabled(False)
            self.repaint()
            port = self.GUI_main.setups_tab.get_port(
                self.board_select.currentText())
            self.board = Pycboard(port,
                                  print_func=self.print_to_log,
                                  data_logger=self.data_logger)
            self.connected = True
            self.config_button.setEnabled(True)
            self.connect_button.setEnabled(True)
            self.connect_button.setText("Disconnect")
            self.connect_button.setIcon(
                QtGui.QIcon("gui/icons/disconnect.svg"))
            self.status_text.setText("Connected")
            if self.board.status["framework"]:
                self.task_groupbox.setEnabled(True)
            else:
                self.print_to_log(
                    "\nLoad pyControl framework using 'Config' button.")
        except (SerialException, PyboardError):
            self.status_text.setText("Connection failed")
            self.print_to_log("Connection failed.")
            self.connect_button.setEnabled(True)
            self.board_select.setEnabled(True)

    def disconnect(self):
        # Disconnect from pyboard.
        if self.board:
            self.board.close()
        self.board = None
        self.task_groupbox.setEnabled(False)
        self.file_groupbox.setEnabled(False)
        self.session_groupbox.setEnabled(False)
        self.config_button.setEnabled(False)
        self.board_select.setEnabled(True)
        self.connect_button.setText("Connect")
        self.connect_button.setIcon(QtGui.QIcon("gui/icons/connect.svg"))
        self.status_text.setText("Not connected")
        self.task_changed()
        self.connected = False

    def task_changed(self, *args):
        self.uploaded = False
        self.upload_button.setText("Upload")
        self.upload_button.setIcon(
            QtGui.QIcon("gui/icons/circle-arrow-up.svg"))
        self.start_button.setEnabled(False)
        self.variables_button.setEnabled(False)

    def setup_task(self):
        task = self.task_select.text()
        if task == "select task":
            return
        try:
            if self.uploaded:
                self.status_text.setText("Resetting task..")
            else:
                self.status_text.setText("Uploading..")
                self.task_hash = _djb2_file(
                    os.path.join(dirs["tasks"], task + ".py"))
            self.start_button.setEnabled(False)
            self.variables_button.setEnabled(False)
            self.repaint()
            self.board.setup_state_machine(task, uploaded=self.uploaded)
            if self.variables_dialog:
                self.variables_button.clicked.disconnect()
                self.variables_dialog.deleteLater()
            self.task = task
            if "custom_variables_dialog" in self.board.sm_info["variables"]:
                custom_variables_name = eval(
                    self.board.sm_info["variables"]["custom_variables_dialog"])
                potential_dialog = Custom_variables_dialog(
                    self, custom_variables_name)
                if potential_dialog.using_custom_gui:
                    self.variables_dialog = potential_dialog
                    self.using_custom_gui = True
                else:
                    self.variables_dialog = Variables_dialog(self, self.board)
                    self.using_custom_gui = False
            else:
                self.variables_dialog = Variables_dialog(self, self.board)
                self.using_custom_gui = False
            self.variables_button.clicked.connect(self.variables_dialog.exec_)
            self.variables_button.setEnabled(True)
            self.task_plot.set_state_machine(self.board.sm_info)
            self.task_info.set_state_machine(self.board.sm_info)
            self.file_groupbox.setEnabled(True)
            self.session_groupbox.setEnabled(True)
            self.start_button.setEnabled(True)
            self.stop_button.setEnabled(False)
            self.status_text.setText("Uploaded : " + task)
            self.fresh_task = True
            self.uploaded = True
            self.upload_button.setText("Reset")
            self.upload_button.setIcon(QtGui.QIcon("gui/icons/refresh.svg"))

        except PyboardError:
            self.status_text.setText("Error setting up state machine.")

    def select_data_dir(self):
        new_path = QtGui.QFileDialog.getExistingDirectory(
            self, "Select data folder", dirs["data"])
        if new_path:
            self.data_dir_text.setText(new_path)
            self.custom_dir = True

    def start_task(self):
        recording = self.test_data_path()
        if recording:
            if not self.fresh_task:
                reset_task = QtGui.QMessageBox.question(
                    self,
                    "Reset task",
                    "Task has already been run, variables may not have default values.\n\nReset task?",
                    QtGui.QMessageBox.Yes,
                    QtGui.QMessageBox.No,
                )
                if reset_task == QtGui.QMessageBox.Yes:
                    self.setup_task()
                    return
            subject_ID = str(self.subject_text.text())
            setup_ID = str(self.board_select.currentText())
            self.data_logger.open_data_file(self.data_dir, "run_task",
                                            setup_ID, subject_ID)
            self.data_logger.copy_task_file(self.data_dir, dirs["tasks"],
                                            "run_task-task_files")
        self.fresh_task = False
        self.running = True
        self.board.start_framework()
        self.task_plot.run_start(recording)
        self.task_select.setEnabled(False)
        self.upload_button.setEnabled(False)
        self.file_groupbox.setEnabled(False)
        self.start_button.setEnabled(False)
        self.board_groupbox.setEnabled(False)
        self.stop_button.setEnabled(True)
        if self.using_custom_gui:
            self.variables_dialog.edit_action.setEnabled(False)
        self.print_to_log(
            f"\nRun started at: {datetime.now().strftime('%Y/%m/%d %H:%M:%S')}\n"
        )
        self.update_timer.start(update_interval)
        self.GUI_main.refresh_timer.stop()
        self.status_text.setText("Running: " + self.task)
        self.GUI_main.tab_widget.setTabEnabled(
            1, False)  # Disable experiments tab.
        self.GUI_main.tab_widget.setTabEnabled(2, False)  # Disable setups tab.

    def stop_task(self, error=False, stopped_by_task=False):
        self.running = False
        self.update_timer.stop()
        self.GUI_main.refresh_timer.start(self.GUI_main.refresh_interval)
        if not (error or stopped_by_task):
            self.board.stop_framework()
            time.sleep(0.05)
            self.board.process_data()
        self.data_logger.close_files()
        self.task_plot.run_stop()
        self.board_groupbox.setEnabled(True)
        self.file_groupbox.setEnabled(True)
        self.start_button.setEnabled(True)
        self.task_select.setEnabled(True)
        self.upload_button.setEnabled(True)
        self.stop_button.setEnabled(False)
        if self.using_custom_gui:
            self.variables_dialog.edit_action.setEnabled(True)
        self.status_text.setText("Uploaded : " + self.task)
        self.GUI_main.tab_widget.setTabEnabled(1, True)  # Enable setups tab.
        self.GUI_main.tab_widget.setTabEnabled(2, True)  # Enable setups tab.

    # Timer updates

    def update(self):
        # Called regularly during run to process data from board and update plots.
        try:
            self.board.process_data()
            if not self.board.framework_running:
                self.stop_task(stopped_by_task=True)
        except PyboardError:
            self.print_to_log("\nError during framework run.")
            self.stop_task(error=True)
        self.task_plot.update()

    # Cleanup.

    def closeEvent(self, event):
        # Called when GUI window is closed.
        if self.board:
            self.board.stop_framework()
            self.board.close()
        event.accept()

    # Exception handling.

    def excepthook(self, ex_type, ex_value, ex_traceback):
        # Called whenever an uncaught exception occurs.
        if ex_type in (SerialException, SerialTimeoutException):
            self.print_to_log("\nError: Serial connection with board lost.")
        elif ex_type == PyboardError:
            self.print_to_log("\nError: Unable to execute command.")
        else:
            self.print_to_log(
                f"\nError: uncaught exception of type: {ex_type}")
        if self.running:
            self.stop_task(error=True)
        self.disconnect()