#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This experiment was created using PsychoPy3 Experiment Builder (v2024.2.4),
    on March 05, 2025, at 11:52
If you publish work using this script the most relevant publication is:

    Peirce J, Gray JR, Simpson S, MacAskill M, Höchenberger R, Sogo H, Kastman E, Lindeløv JK. (2019) 
        PsychoPy2: Experiments in behavior made easy Behav Res 51: 195. 
        https://doi.org/10.3758/s13428-018-01193-y

"""

# --- Import packages ---
from psychopy import locale_setup
from psychopy import prefs
from psychopy import plugins
plugins.activatePlugins()
from psychopy import sound, gui, visual, core, data, event, logging, clock, colors, layout, hardware
from psychopy.tools import environmenttools
from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
                                STOPPED, FINISHED, PRESSED, RELEASED, FOREVER, priority)

import numpy as np  # whole numpy lib is available, prepend 'np.'
from numpy import (sin, cos, tan, log, log10, pi, average,
                   sqrt, std, deg2rad, rad2deg, linspace, asarray)
from numpy.random import random, randint, normal, shuffle, choice as randchoice
import os  # handy system and path functions
import sys  # to get file system encoding

import psychopy.iohub as io
from psychopy.hardware import keyboard

# --- Setup global variables (available in all functions) ---
# create a device manager to handle hardware (keyboards, mice, mirophones, speakers, etc.)
deviceManager = hardware.DeviceManager()
# ensure that relative paths start from the same directory as this script
_thisDir = os.path.dirname(os.path.abspath(__file__))
# store info about the experiment session
psychopyVersion = '2024.2.4'
expName = 'BBTK T-Pad Example Experiment serial read short timeout in Messaging Loop v2'  # from the Builder filename that created this script
# information about this experiment
expInfo = {
    'participant': '',
    'session': '001',
    'date|hid': data.getDateStr(),
    'expName|hid': expName,
    'psychopyVersion|hid': psychopyVersion,
}

# --- Define some variables which will change depending on pilot mode ---
'''
To run in pilot mode, either use the run/pilot toggle in Builder, Coder and Runner, 
or run the experiment with `--pilot` as an argument. To change what pilot 
#mode does, check out the 'Pilot mode' tab in preferences.
'''
# work out from system args whether we are running in pilot mode
PILOTING = core.setPilotModeFromArgs()
# start off with values from experiment settings
_fullScr = True
_winSize = [2560, 1440]
# if in pilot mode, apply overrides according to preferences
if PILOTING:
    # force windowed mode
    if prefs.piloting['forceWindowed']:
        _fullScr = False
        # set window size
        _winSize = prefs.piloting['forcedWindowSize']

def showExpInfoDlg(expInfo):
    """
    Show participant info dialog.
    Parameters
    ==========
    expInfo : dict
        Information about this experiment.
    
    Returns
    ==========
    dict
        Information about this experiment.
    """
    # show participant info dialog
    dlg = gui.DlgFromDict(
        dictionary=expInfo, sortKeys=False, title=expName, alwaysOnTop=True
    )
    if dlg.OK == False:
        core.quit()  # user pressed cancel
    # return expInfo
    return expInfo


def setupData(expInfo, dataDir=None):
    """
    Make an ExperimentHandler to handle trials and saving.
    
    Parameters
    ==========
    expInfo : dict
        Information about this experiment, created by the `setupExpInfo` function.
    dataDir : Path, str or None
        Folder to save the data to, leave as None to create a folder in the current directory.    
    Returns
    ==========
    psychopy.data.ExperimentHandler
        Handler object for this experiment, contains the data to save and information about 
        where to save it to.
    """
    # remove dialog-specific syntax from expInfo
    for key, val in expInfo.copy().items():
        newKey, _ = data.utils.parsePipeSyntax(key)
        expInfo[newKey] = expInfo.pop(key)
    
    # data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
    if dataDir is None:
        dataDir = _thisDir
    filename = u'data/%s_%s_%s' % (expInfo['participant'], expName, expInfo['date'])
    # make sure filename is relative to dataDir
    if os.path.isabs(filename):
        dataDir = os.path.commonprefix([dataDir, filename])
        filename = os.path.relpath(filename, dataDir)
    
    # an ExperimentHandler isn't essential but helps with data saving
    thisExp = data.ExperimentHandler(
        name=expName, version='',
        extraInfo=expInfo, runtimeInfo=None,
        originPath='D:\\PsychoPy TPad\\FINAL 3\\BBTK T-Pad Example Experiment serial read short timeout in Messaging Loop v2_lastrun.py',
        savePickle=True, saveWideText=True,
        dataFileName=dataDir + os.sep + filename, sortColumns='time'
    )
    thisExp.setPriority('thisRow.t', priority.CRITICAL)
    thisExp.setPriority('expName', priority.LOW)
    # return experiment handler
    return thisExp


def setupLogging(filename):
    """
    Setup a log file and tell it what level to log at.
    
    Parameters
    ==========
    filename : str or pathlib.Path
        Filename to save log file and data files as, doesn't need an extension.
    
    Returns
    ==========
    psychopy.logging.LogFile
        Text stream to receive inputs from the logging system.
    """
    # set how much information should be printed to the console / app
    if PILOTING:
        logging.console.setLevel(
            prefs.piloting['pilotConsoleLoggingLevel']
        )
    else:
        logging.console.setLevel('warning')
    # save a log file for detail verbose info
    logFile = logging.LogFile(filename+'.log')
    if PILOTING:
        logFile.setLevel(
            prefs.piloting['pilotLoggingLevel']
        )
    else:
        logFile.setLevel(
            logging.getLevel('error')
        )
    
    return logFile


def setupWindow(expInfo=None, win=None):
    """
    Setup the Window
    
    Parameters
    ==========
    expInfo : dict
        Information about this experiment, created by the `setupExpInfo` function.
    win : psychopy.visual.Window
        Window to setup - leave as None to create a new window.
    
    Returns
    ==========
    psychopy.visual.Window
        Window in which to run this experiment.
    """
    if PILOTING:
        logging.debug('Fullscreen settings ignored as running in pilot mode.')
    
    if win is None:
        # if not given a window to setup, make one
        win = visual.Window(
            size=_winSize, fullscr=_fullScr, screen=0,
            winType='pyglet', allowGUI=False, allowStencil=False,
            monitor='testMonitor', color=[-1,-1,-1], colorSpace='rgb',
            backgroundImage='', backgroundFit='none',
            blendMode='avg', useFBO=True,
            units='height',
            checkTiming=False  # we're going to do this ourselves in a moment
        )
    else:
        # if we have a window, just set the attributes which are safe to set
        win.color = [-1,-1,-1]
        win.colorSpace = 'rgb'
        win.backgroundImage = ''
        win.backgroundFit = 'none'
        win.units = 'height'
    if expInfo is not None:
        # get/measure frame rate if not already in expInfo
        if win._monitorFrameRate is None:
            win._monitorFrameRate = win.getActualFrameRate(infoMsg='Attempting to measure frame rate of screen, please wait...')
        expInfo['frameRate'] = win._monitorFrameRate
    win.hideMessage()
    # show a visual indicator if we're in piloting mode
    if PILOTING and prefs.piloting['showPilotingIndicator']:
        win.showPilotingIndicator()
    
    return win


def setupDevices(expInfo, thisExp, win):
    """
    Setup whatever devices are available (mouse, keyboard, speaker, eyetracker, etc.) and add them to 
    the device manager (deviceManager)
    
    Parameters
    ==========
    expInfo : dict
        Information about this experiment, created by the `setupExpInfo` function.
    thisExp : psychopy.data.ExperimentHandler
        Handler object for this experiment, contains the data to save and information about 
        where to save it to.
    win : psychopy.visual.Window
        Window in which to run this experiment.
    Returns
    ==========
    bool
        True if completed successfully.
    """
    # --- Setup input devices ---
    ioConfig = {}
    
    # Setup iohub keyboard
    ioConfig['Keyboard'] = dict(use_keymap='psychopy')
    
    # Setup iohub experiment
    ioConfig['Experiment'] = dict(filename=thisExp.dataFileName)
    
    # Start ioHub server
    ioServer = io.launchHubServer(window=win, **ioConfig)
    
    # store ioServer object in the device manager
    deviceManager.ioServer = ioServer
    
    # create a default keyboard (e.g. to check for escape)
    if deviceManager.getDevice('defaultKeyboard') is None:
        deviceManager.addDevice(
            deviceClass='keyboard', deviceName='defaultKeyboard', backend='iohub'
        )
    if deviceManager.getDevice('ready') is None:
        # initialise ready
        ready = deviceManager.addDevice(
            deviceClass='keyboard',
            deviceName='ready',
        )
    # return True if completed successfully
    return True

def pauseExperiment(thisExp, win=None, timers=[], playbackComponents=[]):
    """
    Pause this experiment, preventing the flow from advancing to the next routine until resumed.
    
    Parameters
    ==========
    thisExp : psychopy.data.ExperimentHandler
        Handler object for this experiment, contains the data to save and information about 
        where to save it to.
    win : psychopy.visual.Window
        Window for this experiment.
    timers : list, tuple
        List of timers to reset once pausing is finished.
    playbackComponents : list, tuple
        List of any components with a `pause` method which need to be paused.
    """
    # if we are not paused, do nothing
    if thisExp.status != PAUSED:
        return
    
    # start a timer to figure out how long we're paused for
    pauseTimer = core.Clock()
    # pause any playback components
    for comp in playbackComponents:
        comp.pause()
    # make sure we have a keyboard
    defaultKeyboard = deviceManager.getDevice('defaultKeyboard')
    if defaultKeyboard is None:
        defaultKeyboard = deviceManager.addKeyboard(
            deviceClass='keyboard',
            deviceName='defaultKeyboard',
            backend='ioHub',
        )
    # run a while loop while we wait to unpause
    while thisExp.status == PAUSED:
        # check for quit (typically the Esc key)
        if defaultKeyboard.getKeys(keyList=['escape']):
            endExperiment(thisExp, win=win)
        # sleep 1ms so other threads can execute
        clock.time.sleep(0.001)
    # if stop was requested while paused, quit
    if thisExp.status == FINISHED:
        endExperiment(thisExp, win=win)
    # resume any playback components
    for comp in playbackComponents:
        comp.play()
    # reset any timers
    for timer in timers:
        timer.addTime(-pauseTimer.getTime())


def run(expInfo, thisExp, win, globalClock=None, thisSession=None):
    """
    Run the experiment flow.
    
    Parameters
    ==========
    expInfo : dict
        Information about this experiment, created by the `setupExpInfo` function.
    thisExp : psychopy.data.ExperimentHandler
        Handler object for this experiment, contains the data to save and information about 
        where to save it to.
    psychopy.visual.Window
        Window in which to run this experiment.
    globalClock : psychopy.core.clock.Clock or None
        Clock to get global time from - supply None to make a new one.
    thisSession : psychopy.session.Session or None
        Handle of the Session object this experiment is being run from, if any.
    """
    # mark experiment as started
    thisExp.status = STARTED
    # make sure window is set to foreground to prevent losing focus
    win.winHandle.activate()
    # make sure variables created by exec are available globally
    exec = environmenttools.setExecEnvironment(globals())
    # get device handles from dict of input devices
    ioServer = deviceManager.ioServer
    # get/create a default keyboard (e.g. to check for escape)
    defaultKeyboard = deviceManager.getDevice('defaultKeyboard')
    if defaultKeyboard is None:
        deviceManager.addDevice(
            deviceClass='keyboard', deviceName='defaultKeyboard', backend='ioHub'
        )
    eyetracker = deviceManager.getDevice('eyetracker')
    # make sure we're running in the directory for this experiment
    os.chdir(_thisDir)
    # get filename from ExperimentHandler for convenience
    filename = thisExp.dataFileName
    frameTolerance = 0.001  # how close to onset before 'same' frame
    endExpNow = False  # flag for 'escape' or other condition => quit the exp
    # get frame duration from frame rate in expInfo
    if 'frameRate' in expInfo and expInfo['frameRate'] is not None:
        frameDur = 1.0 / round(expInfo['frameRate'])
    else:
        frameDur = 1.0 / 60.0  # could not measure, so guess
    
    # Start Code - component code to be run after the window creation
    
    # --- Initialize components for Routine "instructions" ---
    instructText = visual.TextStim(win=win, name='instructText',
        text='WELCOME A SIMPLE\nBBTK T-Pad\nTEST EXPERIMENT\n\nPress button 1 if green\nPress button 2 if red\n[q] will quit\n\nPress [SPACE] to start',
        font='Open Sans',
        pos=(0, 0), draggable=False, height=0.04, wrapWidth=None, ori=0.0, 
        color='white', colorSpace='rgb', opacity=None, 
        languageStyle='LTR',
        depth=0.0);
    ready = keyboard.Keyboard(deviceName='ready')
    startSquare = visual.Rect(
        win=win, name='startSquare',units='norm', 
        width=(0.4, 0.2)[0], height=(0.4, 0.2)[1],
        ori=0.0, pos=(0.9, 0), draggable=False, anchor='center',
        lineWidth=1.0,
        colorSpace='rgb', lineColor='black', fillColor='black',
        opacity=None, depth=-2.0, interpolate=True)
    
    # --- Initialize components for Routine "black_marker" ---
    blackSquare = visual.Rect(
        win=win, name='blackSquare',units='norm', 
        width=(0.2, 0.2)[0], height=(0.2, 0.2)[1],
        ori=0.0, pos=(0.9, 0), draggable=False, anchor='center',
        lineWidth=1.0,
        colorSpace='rgb', lineColor='black', fillColor='black',
        opacity=None, depth=0.0, interpolate=True)
    
    # --- Initialize components for Routine "white_marker" ---
    whiteSquare = visual.Rect(
        win=win, name='whiteSquare',units='norm', 
        width=(0.2, 0.2)[0], height=(0.2, 0.2)[1],
        ori=0.0, pos=(0.9, 0), draggable=False, anchor='center',
        lineWidth=1.0,
        colorSpace='rgb', lineColor='white', fillColor='white',
        opacity=None, depth=0.0, interpolate=True)
    image = visual.ImageStim(
        win=win,
        name='image', 
        image='default.png', mask=None, anchor='center',
        ori=0.0, pos=(0, 0), draggable=False, size=(0.5, 0.5),
        color=[1,1,1], colorSpace='rgb', opacity=None,
        flipHoriz=False, flipVert=False,
        texRes=128.0, interpolate=True, depth=-1.0)
    # Run 'Begin Experiment' code from handleKeys
    from psychopy import core
    import time
    from datetime import datetime
    import serial
    
    kb = keyboard.Keyboard()                                #setup keyboard
    
    ser = serial.Serial("COM7", 115200, timeout=0)          #open your T-Pad VCP serial port and have no timeout
    #ser = serial.Serial("COM7", 115200, timeout=0.000001)   #open your T-Pad VCP serial port and have a very low timeout value to stop blocking, i.e. 1 microsecond
    #ser = serial.Serial("COM7", 115200, timeout=0.00001)       #open your T-Pad VCP serial port and have a very low timeout value
    #ser = serial.Serial("COM7", 115200, timeout=0.0001)       #open your T-Pad VCP serial port and have a very low timeout value
    #ser = serial.Serial("COM7", 115200, timeout=0.001)       #open your T-Pad VCP serial port and have a very low timeout value
    #ser = serial.Serial("COM7", 115200, timeout=0.01)       #open your T-Pad VCP serial port and have a very low timeout value
    ser.write("X".encode())                                 #stop doing whatever it is doing and return to MOD0 or command mode
    core.wait(0.500)                                        #have a small delay to allow the T-Pad to go into Mode 0
    ser.write("MOD5\n".encode())                            #set to MOD5, i.e. XiD mode
    core.wait(0.500)                                        #you can also get to this mode by pressing the button on the rear of the T-Pad 5 times until it flashes green
    print("START", flush=True)                              #debugging if needed
    
    # --- Initialize components for Routine "end" ---
    endText = visual.TextStim(win=win, name='endText',
        text='Thank you for your participation.\n\nPress [q] to quit.',
        font='Open Sans',
        pos=(0, 0), draggable=False, height=0.04, wrapWidth=None, ori=0.0, 
        color='white', colorSpace='rgb', opacity=None, 
        languageStyle='LTR',
        depth=0.0);
    blackSquare2 = visual.Rect(
        win=win, name='blackSquare2',units='norm', 
        width=(0.4, 0.2)[0], height=(0.4, 0.2)[1],
        ori=0.0, pos=(0.9, 0), draggable=False, anchor='center',
        lineWidth=1.0,
        colorSpace='rgb', lineColor='black', fillColor='black',
        opacity=None, depth=-1.0, interpolate=True)
    
    # create some handy timers
    
    # global clock to track the time since experiment started
    if globalClock is None:
        # create a clock if not given one
        globalClock = core.Clock()
    if isinstance(globalClock, str):
        # if given a string, make a clock accoridng to it
        if globalClock == 'float':
            # get timestamps as a simple value
            globalClock = core.Clock(format='float')
        elif globalClock == 'iso':
            # get timestamps in ISO format
            globalClock = core.Clock(format='%Y-%m-%d_%H:%M:%S.%f%z')
        else:
            # get timestamps in a custom format
            globalClock = core.Clock(format=globalClock)
    if ioServer is not None:
        ioServer.syncClock(globalClock)
    logging.setDefaultClock(globalClock)
    # routine timer to track time remaining of each (possibly non-slip) routine
    routineTimer = core.Clock()
    win.flip()  # flip window to reset last flip timer
    # store the exact time the global clock started
    expInfo['expStart'] = data.getDateStr(
        format='%Y-%m-%d %Hh%M.%S.%f %z', fractionalSecondDigits=6
    )
    
    # --- Prepare to start Routine "instructions" ---
    # create an object to store info about Routine instructions
    instructions = data.Routine(
        name='instructions',
        components=[instructText, ready, startSquare],
    )
    instructions.status = NOT_STARTED
    continueRoutine = True
    # update component parameters for each repeat
    # create starting attributes for ready
    ready.keys = []
    ready.rt = []
    _ready_allKeys = []
    # store start times for instructions
    instructions.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
    instructions.tStart = globalClock.getTime(format='float')
    instructions.status = STARTED
    thisExp.addData('instructions.started', instructions.tStart)
    instructions.maxDuration = None
    # keep track of which components have finished
    instructionsComponents = instructions.components
    for thisComponent in instructions.components:
        thisComponent.tStart = None
        thisComponent.tStop = None
        thisComponent.tStartRefresh = None
        thisComponent.tStopRefresh = None
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED
    # reset timers
    t = 0
    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
    frameN = -1
    
    # --- Run Routine "instructions" ---
    instructions.forceEnded = routineForceEnded = not continueRoutine
    while continueRoutine:
        # get current time
        t = routineTimer.getTime()
        tThisFlip = win.getFutureFlipTime(clock=routineTimer)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *instructText* updates
        
        # if instructText is starting this frame...
        if instructText.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            instructText.frameNStart = frameN  # exact frame index
            instructText.tStart = t  # local t and not account for scr refresh
            instructText.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(instructText, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'instructText.started')
            # update status
            instructText.status = STARTED
            instructText.setAutoDraw(True)
        
        # if instructText is active this frame...
        if instructText.status == STARTED:
            # update params
            pass
        
        # *ready* updates
        waitOnFlip = False
        
        # if ready is starting this frame...
        if ready.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            ready.frameNStart = frameN  # exact frame index
            ready.tStart = t  # local t and not account for scr refresh
            ready.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(ready, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'ready.started')
            # update status
            ready.status = STARTED
            # keyboard checking is just starting
            waitOnFlip = True
            win.callOnFlip(ready.clock.reset)  # t=0 on next screen flip
            win.callOnFlip(ready.clearEvents, eventType='keyboard')  # clear events on next screen flip
        if ready.status == STARTED and not waitOnFlip:
            theseKeys = ready.getKeys(keyList=['space'], ignoreKeys=["escape"], waitRelease=False)
            _ready_allKeys.extend(theseKeys)
            if len(_ready_allKeys):
                ready.keys = _ready_allKeys[-1].name  # just the last key pressed
                ready.rt = _ready_allKeys[-1].rt
                ready.duration = _ready_allKeys[-1].duration
                # a response ends the routine
                continueRoutine = False
        
        # *startSquare* updates
        
        # if startSquare is starting this frame...
        if startSquare.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            startSquare.frameNStart = frameN  # exact frame index
            startSquare.tStart = t  # local t and not account for scr refresh
            startSquare.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(startSquare, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'startSquare.started')
            # update status
            startSquare.status = STARTED
            startSquare.setAutoDraw(True)
        
        # if startSquare is active this frame...
        if startSquare.status == STARTED:
            # update params
            pass
        
        # check for quit (typically the Esc key)
        if defaultKeyboard.getKeys(keyList=["escape"]):
            thisExp.status = FINISHED
        if thisExp.status == FINISHED or endExpNow:
            endExperiment(thisExp, win=win)
            return
        # pause experiment here if requested
        if thisExp.status == PAUSED:
            pauseExperiment(
                thisExp=thisExp, 
                win=win, 
                timers=[routineTimer], 
                playbackComponents=[]
            )
            # skip the frame we paused on
            continue
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            instructions.forceEnded = routineForceEnded = True
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in instructions.components:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()
    
    # --- Ending Routine "instructions" ---
    for thisComponent in instructions.components:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    # store stop times for instructions
    instructions.tStop = globalClock.getTime(format='float')
    instructions.tStopRefresh = tThisFlipGlobal
    thisExp.addData('instructions.stopped', instructions.tStop)
    # check responses
    if ready.keys in ['', [], None]:  # No response was made
        ready.keys = None
    thisExp.addData('ready.keys',ready.keys)
    if ready.keys != None:  # we had a response
        thisExp.addData('ready.rt', ready.rt)
        thisExp.addData('ready.duration', ready.duration)
    thisExp.nextEntry()
    # the Routine "instructions" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()
    
    # set up handler to look after randomisation of conditions etc
    trials = data.TrialHandler2(
        name='trials',
        nReps=1.0, 
        method='sequential', 
        extraInfo=expInfo, 
        originPath=-1, 
        trialList=data.importConditions('conditions.xlsx'), 
        seed=None, 
    )
    thisExp.addLoop(trials)  # add the loop to the experiment
    thisTrial = trials.trialList[0]  # so we can initialise stimuli with some values
    # abbreviate parameter names if possible (e.g. rgb = thisTrial.rgb)
    if thisTrial != None:
        for paramName in thisTrial:
            globals()[paramName] = thisTrial[paramName]
    if thisSession is not None:
        # if running in a Session with a Liaison client, send data up to now
        thisSession.sendExperimentData()
    
    for thisTrial in trials:
        currentLoop = trials
        thisExp.timestampOnFlip(win, 'thisRow.t', format=globalClock.format)
        if thisSession is not None:
            # if running in a Session with a Liaison client, send data up to now
            thisSession.sendExperimentData()
        # abbreviate parameter names if possible (e.g. rgb = thisTrial.rgb)
        if thisTrial != None:
            for paramName in thisTrial:
                globals()[paramName] = thisTrial[paramName]
        
        # --- Prepare to start Routine "black_marker" ---
        # create an object to store info about Routine black_marker
        black_marker = data.Routine(
            name='black_marker',
            components=[blackSquare],
        )
        black_marker.status = NOT_STARTED
        continueRoutine = True
        # update component parameters for each repeat
        # store start times for black_marker
        black_marker.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
        black_marker.tStart = globalClock.getTime(format='float')
        black_marker.status = STARTED
        thisExp.addData('black_marker.started', black_marker.tStart)
        black_marker.maxDuration = None
        # keep track of which components have finished
        black_markerComponents = black_marker.components
        for thisComponent in black_marker.components:
            thisComponent.tStart = None
            thisComponent.tStop = None
            thisComponent.tStartRefresh = None
            thisComponent.tStopRefresh = None
            if hasattr(thisComponent, 'status'):
                thisComponent.status = NOT_STARTED
        # reset timers
        t = 0
        _timeToFirstFrame = win.getFutureFlipTime(clock="now")
        frameN = -1
        
        # --- Run Routine "black_marker" ---
        # if trial has changed, end Routine now
        if isinstance(trials, data.TrialHandler2) and thisTrial.thisN != trials.thisTrial.thisN:
            continueRoutine = False
        black_marker.forceEnded = routineForceEnded = not continueRoutine
        while continueRoutine and routineTimer.getTime() < 2.0:
            # get current time
            t = routineTimer.getTime()
            tThisFlip = win.getFutureFlipTime(clock=routineTimer)
            tThisFlipGlobal = win.getFutureFlipTime(clock=None)
            frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
            # update/draw components on each frame
            
            # *blackSquare* updates
            
            # if blackSquare is starting this frame...
            if blackSquare.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
                # keep track of start time/frame for later
                blackSquare.frameNStart = frameN  # exact frame index
                blackSquare.tStart = t  # local t and not account for scr refresh
                blackSquare.tStartRefresh = tThisFlipGlobal  # on global time
                win.timeOnFlip(blackSquare, 'tStartRefresh')  # time at next scr refresh
                # add timestamp to datafile
                thisExp.timestampOnFlip(win, 'blackSquare.started')
                # update status
                blackSquare.status = STARTED
                blackSquare.setAutoDraw(True)
            
            # if blackSquare is active this frame...
            if blackSquare.status == STARTED:
                # update params
                pass
            
            # if blackSquare is stopping this frame...
            if blackSquare.status == STARTED:
                # is it time to stop? (based on global clock, using actual start)
                if tThisFlipGlobal > blackSquare.tStartRefresh + 2.0-frameTolerance:
                    # keep track of stop time/frame for later
                    blackSquare.tStop = t  # not accounting for scr refresh
                    blackSquare.tStopRefresh = tThisFlipGlobal  # on global time
                    blackSquare.frameNStop = frameN  # exact frame index
                    # add timestamp to datafile
                    thisExp.timestampOnFlip(win, 'blackSquare.stopped')
                    # update status
                    blackSquare.status = FINISHED
                    blackSquare.setAutoDraw(False)
            
            # check for quit (typically the Esc key)
            if defaultKeyboard.getKeys(keyList=["escape"]):
                thisExp.status = FINISHED
            if thisExp.status == FINISHED or endExpNow:
                endExperiment(thisExp, win=win)
                return
            # pause experiment here if requested
            if thisExp.status == PAUSED:
                pauseExperiment(
                    thisExp=thisExp, 
                    win=win, 
                    timers=[routineTimer], 
                    playbackComponents=[]
                )
                # skip the frame we paused on
                continue
            
            # check if all components have finished
            if not continueRoutine:  # a component has requested a forced-end of Routine
                black_marker.forceEnded = routineForceEnded = True
                break
            continueRoutine = False  # will revert to True if at least one component still running
            for thisComponent in black_marker.components:
                if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                    continueRoutine = True
                    break  # at least one component has not yet finished
            
            # refresh the screen
            if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
                win.flip()
        
        # --- Ending Routine "black_marker" ---
        for thisComponent in black_marker.components:
            if hasattr(thisComponent, "setAutoDraw"):
                thisComponent.setAutoDraw(False)
        # store stop times for black_marker
        black_marker.tStop = globalClock.getTime(format='float')
        black_marker.tStopRefresh = tThisFlipGlobal
        thisExp.addData('black_marker.stopped', black_marker.tStop)
        # using non-slip timing so subtract the expected duration of this Routine (unless ended on request)
        if black_marker.maxDurationReached:
            routineTimer.addTime(-black_marker.maxDuration)
        elif black_marker.forceEnded:
            routineTimer.reset()
        else:
            routineTimer.addTime(-2.000000)
        
        # --- Prepare to start Routine "white_marker" ---
        # create an object to store info about Routine white_marker
        white_marker = data.Routine(
            name='white_marker',
            components=[whiteSquare, image],
        )
        white_marker.status = NOT_STARTED
        continueRoutine = True
        # update component parameters for each repeat
        image.setImage(imageFile)
        # Run 'Begin Routine' code from handleKeys
        strSerialData = ""                          #used to store raw serial data received
        strDecodedSerialData = ""                   #decoded serial data in utf-8
        intCounter = 0 #counter that increments by +1 each time there is a change on anything on the T-pad, that could be a button going down or up. multiple changes count as 1 sample or a report
        KeysPressed = "" #array string to store which keys are pressed
        booProcessSerialReceived = 0 #boolean flag as to whether there has been a change that needed to be processed, i.e. displayed on screen
        strSerialData = ""
        strDecodedSerialData= ""
        strDecodedSerialDataBuffer = ""
        booXiDButton1Down= 0 #the boolean status of each input, i.e. 0 for button up or off and 1 for button down or on
        booXiDButton2Down= 0
        booXiDButton3Down= 0
        booXiDButton4Down= 0
        booXiDButton5Down= 0
        booXiDButton6Down= 0
        booXiDButton7Down= 0
        booXiDButton8Down= 0
        booXiDButton9Down= 0
        booXiDButton10Down= 0
        booXiDOpto1Active = 0
        booXiDOpto2Active = 0
        booXiDVKActive = 0
        booXiDTTLActive = 0
        strXiDChannel = "" #variable to store channel from XiD command sent over serial
        strXiDPressRelease = "" #variable to store Press or Release  from XiD command sent over serial
        strXiDSensorNo = "" #variable to store button or sensor number
        strXiDmSTimer = "" #variable to store the running external hardware mS timer built into the T-Pad
        strXiDStatusMsg = "" #variable to store the status of each button or sensor built up from receiving XiD commands over serial
        outputlength = 0 #variable to store the length of the last serial bytes sent from the T-Pad
        booRunning = 1
        
        #flush serial buffer and keyboard
        ser.flush()
        ser.reset_input_buffer()
        kb.clearEvents()
        
        whiteSquare.draw()
        image.draw()
        win.flip() #show the status stimulus image  on screen
        
        while booRunning == 1:
            KeysPressed = kb.getKeys(['q'], waitRelease=False, clear=True) #check which keys have been pressed
        
            if KeysPressed: #if there has been a key release then check which key it is?
                #print("Pressed: ", [(k.rt, k.name, k.duration) for k in KeysPressed])
                for k in KeysPressed:
                    if k.name == 'q':
                        ser.write("X".encode()) #stop doing whatever and return the T-pad to MOD0, i.e. mode 0 or command mode
                        core.wait(0.500) #add a small delay for the T-Pad to come out of whatever mode it is in
                        #flush serial buffer and keyboard
                        ser.flush()
                        ser.reset_input_buffer()
                        ser.close()
                        kb.clearEvents()
                        print("END", flush=True) #debugging if needed
                        core.quit() #quit PsychoPy
                        
            #strSerialData = ser.readline() #read a '\n' terminated line from the serial stream, if there is no return from the T-Pad carry on as using non blocking serial
            strSerialData = ser.read() #read a single byte from the serial stream, if there is no return from the T-Pad carry on as using non blocking serial
            strDecodedSerialData = strSerialData.decode() #decode into string
            strDecodedSerialDataBuffer += strDecodedSerialData #build up a XiD return from individual bytes until a full XiD command has been received terminated in a \n
        
            #outputlength = len(strDecodedSerialData) # work out how long the complete serial string is including CR + LF, i.e. plus 2 invisiable bytes at the end
        
            if "\n" in strDecodedSerialData: #and booSettingWhiteOptoSensitivity == 1: #check if there has been a return message from the T-Pad by looking for a terminating \n in the serial stream
                datRTC = datetime.now().strftime('%Y/%m/%d %H:%M:%S.%f')[:-3] #get the PC date and time we have detected there has been a change on the keyboard we need to report
                booProcessSerialReceived = 1 #flag that serial has been received and we need to process XiD later on in the messaging loop, i.e. display status of each button, opto etc.
        
                #strXiDChannel = "" + strDecodedSerialData[0] #channel
                #strXiDPressRelease = "" + strDecodedSerialData[2] #press|release
                #strXiDSensorNo = "" + strDecodedSerialData[4] #sensor
                #strXiDmSTimer = "" + strDecodedSerialData[6:] #running mS timer
        
                strXiDChannel = "" + strDecodedSerialDataBuffer[0] #channel
                strXiDPressRelease = "" + strDecodedSerialDataBuffer[2] #press|release
                strXiDSensorNo = "" + strDecodedSerialDataBuffer[4] #sensor
                strXiDmSTimer = "" + strDecodedSerialDataBuffer[6:] #running mS timer
                
                strDecodedSerialDataBuffer = "" #wipe the strDecodedSerialDataBuffer ready for the next complete XiD command
        
                if strXiDChannel == 'A': #channel A is for buttons 1 to 10
                    if strXiDPressRelease == 'P': #P is for button press/button down
                        if strXiDSensorNo == '1': #check the button number, i.e. button 1 in this case
                            booXiDButton1Down = 1 #flag button 1 as being pressed/down
                        if strXiDSensorNo == '2':
                            booXiDButton2Down = 1
                        if strXiDSensorNo == '3':
                            booXiDButton3Down = 1
                        if strXiDSensorNo == '4':
                            booXiDButton4Down = 1
                        if strXiDSensorNo == '5':
                            booXiDButton5Down = 1
                        if strXiDSensorNo == '6':
                            booXiDButton6Down = 1
                        if strXiDSensorNo == '7':
                            booXiDButton7Down = 1
                        if strXiDSensorNo == '8':
                            booXiDButton8Down = 1
                        if strXiDSensorNo == '9':
                            booXiDButton9Down = 1
                        if strXiDSensorNo == '0':
                            booXiDButton10Down = 1
                    if strXiDPressRelease == 'R': #R is for button release/button up
                        if strXiDSensorNo == '1': #check the button number, i.e. button 1 in this case
                            booXiDButton1Down = 0 #flag button 1 as being released/up
                        if strXiDSensorNo == '2':
                            booXiDButton2Down = 0
                        if strXiDSensorNo == '3':
                            booXiDButton3Down = 0
                        if strXiDSensorNo == '4':
                            booXiDButton4Down = 0
                        if strXiDSensorNo == '5':
                            booXiDButton5Down = 0
                        if strXiDSensorNo == '6':
                            booXiDButton6Down = 0
                        if strXiDSensorNo == '7':
                            booXiDButton7Down = 0
                        if strXiDSensorNo == '8':
                            booXiDButton8Down = 0
                        if strXiDSensorNo == '9':
                            booXiDButton9Down = 0
                        if strXiDSensorNo == '0':
                            booXiDButton10Down = 0
        
                if strXiDChannel == 'C': #channel C is for optos 1 & 2
                    if strXiDPressRelease == 'P': #P is for opto active/on
                        if strXiDSensorNo == '1': #check the opto number, i.e. opto 1 in this case
                            booXiDOpto1Active = 1
                        if strXiDSensorNo == '2':
                            booXiDOpto2Active = 1
                    if strXiDPressRelease == 'R': #R is for opto non active/off
                        if strXiDSensorNo == '1':
                            booXiDOpto1Active = 0
                        if strXiDSensorNo == '2':
                            booXiDOpto2Active = 0
        
                if strXiDChannel == 'M': #channel M is for voice key or mic
                    if strXiDPressRelease == 'P': #P is for microphone active/on
                        if strXiDSensorNo == '1': #micriohone 1
                            booXiDVKActive = 1
                    if strXiDPressRelease == 'R':  #R is for microphone non active/off
                        if strXiDSensorNo == '1':
                            booXiDVKActive = 0
        
                if strXiDChannel == 'B': #channel B is for TTL in
                    if strXiDPressRelease == 'P': #P is for TTL in active 5V/on
                        if strXiDSensorNo == '1':
                            booXiDTTLActive = 1
                    if strXiDPressRelease == 'R': #R is for TTL in non active 0V/off
                        if strXiDSensorNo == '1':
                            booXiDTTLActive = 0
        
            if booProcessSerialReceived == 1: #check if there has been a change on any key and if we need to update the status of each key on screen
                #datRTC = datetime.now().strftime('%Y/%m/%d %H:%M:%S.%f')[:-3] #get the PC date and time we have detected there has been a change on the keyboard we need to report
                #strXiDStatusMsg = "" #get ready to produce a status message to show the status of every line built up via receiving XiD commands over serial
                booProcessSerialReceived = 0 #flag that there is no more serial XiD commands to process
                if booXiDButton1Down == 1 or booXiDButton2Down == 1: #accept a response on button 1 or button 2 -- ignore other inputs
                    booRunning = 0
                    print("Button Pressed XiD: " + strXiDChannel + " " + strXiDPressRelease + " " + strXiDSensorNo + " " + strXiDmSTimer, flush=True) #debugging if needed
                    continueRoutine = False 
                    break
                else:
                    print("Last Raw XiD: " + strXiDChannel + " " + strXiDPressRelease + " " + strXiDSensorNo + " " + strXiDmSTimer, flush=True) #debugging if needed
        # store start times for white_marker
        white_marker.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
        white_marker.tStart = globalClock.getTime(format='float')
        white_marker.status = STARTED
        thisExp.addData('white_marker.started', white_marker.tStart)
        white_marker.maxDuration = None
        # keep track of which components have finished
        white_markerComponents = white_marker.components
        for thisComponent in white_marker.components:
            thisComponent.tStart = None
            thisComponent.tStop = None
            thisComponent.tStartRefresh = None
            thisComponent.tStopRefresh = None
            if hasattr(thisComponent, 'status'):
                thisComponent.status = NOT_STARTED
        # reset timers
        t = 0
        _timeToFirstFrame = win.getFutureFlipTime(clock="now")
        frameN = -1
        
        # --- Run Routine "white_marker" ---
        # if trial has changed, end Routine now
        if isinstance(trials, data.TrialHandler2) and thisTrial.thisN != trials.thisTrial.thisN:
            continueRoutine = False
        white_marker.forceEnded = routineForceEnded = not continueRoutine
        while continueRoutine:
            # get current time
            t = routineTimer.getTime()
            tThisFlip = win.getFutureFlipTime(clock=routineTimer)
            tThisFlipGlobal = win.getFutureFlipTime(clock=None)
            frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
            # update/draw components on each frame
            
            # *whiteSquare* updates
            
            # if whiteSquare is starting this frame...
            if whiteSquare.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
                # keep track of start time/frame for later
                whiteSquare.frameNStart = frameN  # exact frame index
                whiteSquare.tStart = t  # local t and not account for scr refresh
                whiteSquare.tStartRefresh = tThisFlipGlobal  # on global time
                win.timeOnFlip(whiteSquare, 'tStartRefresh')  # time at next scr refresh
                # add timestamp to datafile
                thisExp.timestampOnFlip(win, 'whiteSquare.started')
                # update status
                whiteSquare.status = STARTED
                whiteSquare.setAutoDraw(True)
            
            # if whiteSquare is active this frame...
            if whiteSquare.status == STARTED:
                # update params
                pass
            
            # *image* updates
            
            # if image is starting this frame...
            if image.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
                # keep track of start time/frame for later
                image.frameNStart = frameN  # exact frame index
                image.tStart = t  # local t and not account for scr refresh
                image.tStartRefresh = tThisFlipGlobal  # on global time
                win.timeOnFlip(image, 'tStartRefresh')  # time at next scr refresh
                # add timestamp to datafile
                thisExp.timestampOnFlip(win, 'image.started')
                # update status
                image.status = STARTED
                image.setAutoDraw(True)
            
            # if image is active this frame...
            if image.status == STARTED:
                # update params
                pass
            
            # check for quit (typically the Esc key)
            if defaultKeyboard.getKeys(keyList=["escape"]):
                thisExp.status = FINISHED
            if thisExp.status == FINISHED or endExpNow:
                endExperiment(thisExp, win=win)
                return
            # pause experiment here if requested
            if thisExp.status == PAUSED:
                pauseExperiment(
                    thisExp=thisExp, 
                    win=win, 
                    timers=[routineTimer], 
                    playbackComponents=[]
                )
                # skip the frame we paused on
                continue
            
            # check if all components have finished
            if not continueRoutine:  # a component has requested a forced-end of Routine
                white_marker.forceEnded = routineForceEnded = True
                break
            continueRoutine = False  # will revert to True if at least one component still running
            for thisComponent in white_marker.components:
                if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                    continueRoutine = True
                    break  # at least one component has not yet finished
            
            # refresh the screen
            if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
                win.flip()
        
        # --- Ending Routine "white_marker" ---
        for thisComponent in white_marker.components:
            if hasattr(thisComponent, "setAutoDraw"):
                thisComponent.setAutoDraw(False)
        # store stop times for white_marker
        white_marker.tStop = globalClock.getTime(format='float')
        white_marker.tStopRefresh = tThisFlipGlobal
        thisExp.addData('white_marker.stopped', white_marker.tStop)
        # the Routine "white_marker" was not non-slip safe, so reset the non-slip timer
        routineTimer.reset()
        thisExp.nextEntry()
        
    # completed 1.0 repeats of 'trials'
    
    if thisSession is not None:
        # if running in a Session with a Liaison client, send data up to now
        thisSession.sendExperimentData()
    # get names of stimulus parameters
    if trials.trialList in ([], [None], None):
        params = []
    else:
        params = trials.trialList[0].keys()
    # save data for this loop
    trials.saveAsExcel(filename + '.xlsx', sheetName='trials',
        stimOut=params,
        dataOut=['n','all_mean','all_std', 'all_raw'])
    
    # --- Prepare to start Routine "end" ---
    # create an object to store info about Routine end
    end = data.Routine(
        name='end',
        components=[endText, blackSquare2],
    )
    end.status = NOT_STARTED
    continueRoutine = True
    # update component parameters for each repeat
    # store start times for end
    end.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
    end.tStart = globalClock.getTime(format='float')
    end.status = STARTED
    thisExp.addData('end.started', end.tStart)
    end.maxDuration = None
    # keep track of which components have finished
    endComponents = end.components
    for thisComponent in end.components:
        thisComponent.tStart = None
        thisComponent.tStop = None
        thisComponent.tStartRefresh = None
        thisComponent.tStopRefresh = None
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED
    # reset timers
    t = 0
    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
    frameN = -1
    
    # --- Run Routine "end" ---
    end.forceEnded = routineForceEnded = not continueRoutine
    while continueRoutine:
        # get current time
        t = routineTimer.getTime()
        tThisFlip = win.getFutureFlipTime(clock=routineTimer)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *endText* updates
        
        # if endText is starting this frame...
        if endText.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            endText.frameNStart = frameN  # exact frame index
            endText.tStart = t  # local t and not account for scr refresh
            endText.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(endText, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'endText.started')
            # update status
            endText.status = STARTED
            endText.setAutoDraw(True)
        
        # if endText is active this frame...
        if endText.status == STARTED:
            # update params
            pass
        
        # *blackSquare2* updates
        
        # if blackSquare2 is starting this frame...
        if blackSquare2.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            blackSquare2.frameNStart = frameN  # exact frame index
            blackSquare2.tStart = t  # local t and not account for scr refresh
            blackSquare2.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(blackSquare2, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'blackSquare2.started')
            # update status
            blackSquare2.status = STARTED
            blackSquare2.setAutoDraw(True)
        
        # if blackSquare2 is active this frame...
        if blackSquare2.status == STARTED:
            # update params
            pass
        
        # if blackSquare2 is stopping this frame...
        if blackSquare2.status == STARTED:
            # is it time to stop? (based on global clock, using actual start)
            if tThisFlipGlobal > blackSquare2.tStartRefresh + 2.0-frameTolerance:
                # keep track of stop time/frame for later
                blackSquare2.tStop = t  # not accounting for scr refresh
                blackSquare2.tStopRefresh = tThisFlipGlobal  # on global time
                blackSquare2.frameNStop = frameN  # exact frame index
                # add timestamp to datafile
                thisExp.timestampOnFlip(win, 'blackSquare2.stopped')
                # update status
                blackSquare2.status = FINISHED
                blackSquare2.setAutoDraw(False)
        # Run 'Each Frame' code from handleEnd
        win.flip()
        
        KeysPressed = kb.getKeys(['q'], waitRelease=False, clear=True) #check which keys have been pressed
        
        if KeysPressed: #if there has been a key release then check which key it is?
            datRTC = datetime.now().strftime('%Y/%m/%d %H:%M:%S.%f')[:-3] #get the PC date and time we have detected there has been a change on the keyboard we need to report
            #print("Pressed: ", [(k.rt, k.name, k.duration) for k in KeysPressed])
            for k in KeysPressed:
                if k.name == 'q':
                    core.quit()
                    break
        
        
        # check for quit (typically the Esc key)
        if defaultKeyboard.getKeys(keyList=["escape"]):
            thisExp.status = FINISHED
        if thisExp.status == FINISHED or endExpNow:
            endExperiment(thisExp, win=win)
            return
        # pause experiment here if requested
        if thisExp.status == PAUSED:
            pauseExperiment(
                thisExp=thisExp, 
                win=win, 
                timers=[routineTimer], 
                playbackComponents=[]
            )
            # skip the frame we paused on
            continue
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            end.forceEnded = routineForceEnded = True
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in end.components:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()
    
    # --- Ending Routine "end" ---
    for thisComponent in end.components:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    # store stop times for end
    end.tStop = globalClock.getTime(format='float')
    end.tStopRefresh = tThisFlipGlobal
    thisExp.addData('end.stopped', end.tStop)
    thisExp.nextEntry()
    # the Routine "end" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()
    # Run 'End Experiment' code from handleKeys
    ser.write("X".encode()) #stop doing whatever and return the T-pad to MOD0, i.e. mode 0 or command mode
    core.wait(0.500) #add a small delay for the T-Pad to come out of whatever mode it is in
    #flush serial buffer and keyboard
    ser.flush()
    ser.reset_input_buffer()
    ser.close()
    kb.clearEvents()
    print("END", flush=True) #debugging if needed
    
    # mark experiment as finished
    endExperiment(thisExp, win=win)


def saveData(thisExp):
    """
    Save data from this experiment
    
    Parameters
    ==========
    thisExp : psychopy.data.ExperimentHandler
        Handler object for this experiment, contains the data to save and information about 
        where to save it to.
    """
    filename = thisExp.dataFileName
    # these shouldn't be strictly necessary (should auto-save)
    thisExp.saveAsWideText(filename + '.csv', delim='auto')
    thisExp.saveAsPickle(filename)


def endExperiment(thisExp, win=None):
    """
    End this experiment, performing final shut down operations.
    
    This function does NOT close the window or end the Python process - use `quit` for this.
    
    Parameters
    ==========
    thisExp : psychopy.data.ExperimentHandler
        Handler object for this experiment, contains the data to save and information about 
        where to save it to.
    win : psychopy.visual.Window
        Window for this experiment.
    """
    if win is not None:
        # remove autodraw from all current components
        win.clearAutoDraw()
        # Flip one final time so any remaining win.callOnFlip() 
        # and win.timeOnFlip() tasks get executed
        win.flip()
    # return console logger level to WARNING
    logging.console.setLevel(logging.WARNING)
    # mark experiment handler as finished
    thisExp.status = FINISHED
    logging.flush()


def quit(thisExp, win=None, thisSession=None):
    """
    Fully quit, closing the window and ending the Python process.
    
    Parameters
    ==========
    win : psychopy.visual.Window
        Window to close.
    thisSession : psychopy.session.Session or None
        Handle of the Session object this experiment is being run from, if any.
    """
    thisExp.abort()  # or data files will save again on exit
    # make sure everything is closed down
    if win is not None:
        # Flip one final time so any remaining win.callOnFlip() 
        # and win.timeOnFlip() tasks get executed before quitting
        win.flip()
        win.close()
    logging.flush()
    if thisSession is not None:
        thisSession.stop()
    # terminate Python process
    core.quit()


# if running this experiment as a script...
if __name__ == '__main__':
    # call all functions in order
    expInfo = showExpInfoDlg(expInfo=expInfo)
    thisExp = setupData(expInfo=expInfo)
    logFile = setupLogging(filename=thisExp.dataFileName)
    win = setupWindow(expInfo=expInfo)
    setupDevices(expInfo=expInfo, thisExp=thisExp, win=win)
    run(
        expInfo=expInfo, 
        thisExp=thisExp, 
        win=win,
        globalClock='float'
    )
    saveData(thisExp=thisExp)
    quit(thisExp=thisExp, win=win)
