#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This experiment was created using PsychoPy3 Experiment Builder (v2024.2.4),
    on March 05, 2025, at 11:47
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 = 'TPad Opto Set v42'  # from the Builder filename that created this script
# information about this experiment
expInfo = {
    'participant': f"{randint(0, 999999):06.0f}",
    '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\\TPad Opto Set v43_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')


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.0000, -1.0000, -1.0000], 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.0000, -1.0000, -1.0000]
        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('key_resp') is None:
        # initialise key_resp
        key_resp = deviceManager.addDevice(
            deviceClass='keyboard',
            deviceName='key_resp',
        )
    # 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 "welcome" ---
    welcome_text = visual.TextStim(win=win, name='welcome_text',
        text='This example will show you how to automatically set the optimum sensitivity for Opto 1 on the T-Pad.\n\nPlease position Opto 1 mid screen on the right hand side of your monitor.\n\nPress space to continue\n',
        font='Open Sans',
        pos=(0, 0), draggable=False, height=0.05, wrapWidth=None, ori=0.0, 
        color='white', colorSpace='rgb', opacity=None, 
        languageStyle='LTR',
        depth=0.0);
    key_resp = keyboard.Keyboard(deviceName='key_resp')
    
    # --- Initialize components for Routine "trial" ---
    # Run 'Begin Experiment' code from code
    #the goal of this example script is to show you how to set the optimum sensitivity for Opto 1.
    #this needs to be set to an optimum level so that the opto triggers as early as possible on a white opto marker
    #which is shown when you display a visual stimulus in your experiment.
    #
    #conversely it needs to detect a black when a visual stimulus is not shown. that is, indicate an off as quickly as possible.
    #
    #first we need to determine the level where a black opto marker will falsely trigger an on,
    #i.e. as though a white block had been detected even though a black opto marker was shown.
    #this is done by setting levels from 0 up to 255 and checking the message received from the T-Pad.
    #
    #second we need to determine the level where a white opto marker will falsely trigger an off,
    #i.e. as though a black block had been detected even though a white opto marker was shown.
    #this is done by setting levels from 255 down to 0 and checking the message received from the T-Pad.
    #
    #to detect both onsets (white opto marker) and offsets (black opto marker) optimally the ideal sensitivity level needs to be
    #somewhere in the middle.
    #
    #to set the optimum level the formula below is used which sets the optimum level somewhere just above the midpoint of the two:
    #
    #intOpto1Sensitivity = int(intBlackOptoSensitivity - ((intBlackOptoSensitivity - intWhiteOptoSensitivity) / 2.5))
    #
    
    from psychopy import core
    import time
    import serial
    
    kb = keyboard.Keyboard()                                #setup keyboard
    
    ser = serial.Serial("COM7", 115200, timeout=0)          #open your T-Pad VCP serial port and have a very low timeout value to stop blocking, i.e. zero
    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("AAO1 0\n".encode())                          #set the initial sensitivity of Opto 1 to zero, i.e. always off for now
    core.wait(0.500) 
    print("START", flush=True)                              #debugging if needed
    
    # 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 "welcome" ---
    # create an object to store info about Routine welcome
    welcome = data.Routine(
        name='welcome',
        components=[welcome_text, key_resp],
    )
    welcome.status = NOT_STARTED
    continueRoutine = True
    # update component parameters for each repeat
    # create starting attributes for key_resp
    key_resp.keys = []
    key_resp.rt = []
    _key_resp_allKeys = []
    # store start times for welcome
    welcome.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
    welcome.tStart = globalClock.getTime(format='float')
    welcome.status = STARTED
    thisExp.addData('welcome.started', welcome.tStart)
    welcome.maxDuration = None
    # keep track of which components have finished
    welcomeComponents = welcome.components
    for thisComponent in welcome.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 "welcome" ---
    welcome.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
        
        # *welcome_text* updates
        
        # if welcome_text is starting this frame...
        if welcome_text.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            welcome_text.frameNStart = frameN  # exact frame index
            welcome_text.tStart = t  # local t and not account for scr refresh
            welcome_text.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(welcome_text, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'welcome_text.started')
            # update status
            welcome_text.status = STARTED
            welcome_text.setAutoDraw(True)
        
        # if welcome_text is active this frame...
        if welcome_text.status == STARTED:
            # update params
            pass
        
        # *key_resp* updates
        waitOnFlip = False
        
        # if key_resp is starting this frame...
        if key_resp.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            key_resp.frameNStart = frameN  # exact frame index
            key_resp.tStart = t  # local t and not account for scr refresh
            key_resp.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(key_resp, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'key_resp.started')
            # update status
            key_resp.status = STARTED
            # keyboard checking is just starting
            waitOnFlip = True
            win.callOnFlip(key_resp.clock.reset)  # t=0 on next screen flip
            win.callOnFlip(key_resp.clearEvents, eventType='keyboard')  # clear events on next screen flip
        if key_resp.status == STARTED and not waitOnFlip:
            theseKeys = key_resp.getKeys(keyList=['space'], ignoreKeys=["escape"], waitRelease=False)
            _key_resp_allKeys.extend(theseKeys)
            if len(_key_resp_allKeys):
                key_resp.keys = _key_resp_allKeys[-1].name  # just the last key pressed
                key_resp.rt = _key_resp_allKeys[-1].rt
                key_resp.duration = _key_resp_allKeys[-1].duration
                # a response ends the routine
                continueRoutine = 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
            welcome.forceEnded = routineForceEnded = True
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in welcome.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 "welcome" ---
    for thisComponent in welcome.components:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    # store stop times for welcome
    welcome.tStop = globalClock.getTime(format='float')
    welcome.tStopRefresh = tThisFlipGlobal
    thisExp.addData('welcome.stopped', welcome.tStop)
    # check responses
    if key_resp.keys in ['', [], None]:  # No response was made
        key_resp.keys = None
    thisExp.addData('key_resp.keys',key_resp.keys)
    if key_resp.keys != None:  # we had a response
        thisExp.addData('key_resp.rt', key_resp.rt)
        thisExp.addData('key_resp.duration', key_resp.duration)
    thisExp.nextEntry()
    # the Routine "welcome" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()
    
    # --- Prepare to start Routine "trial" ---
    # create an object to store info about Routine trial
    trial = data.Routine(
        name='trial',
        components=[],
    )
    trial.status = NOT_STARTED
    continueRoutine = True
    # update component parameters for each repeat
    # Run 'Begin Routine' code from code
    #setup variables to be used and inistialise them
    strSerialData = ""                          #used to store raw serial data received
    strDecodedSerialData = ""                   #decoded serial data in utf-8
    booStopMessageLoop = 0                      #boolean flag as to whether main messaging loop should be running, default is that it should
    intBlackOptoCounter = 0                     #variable to store the value of the black opto level being sent out to the T-Pad, start at minimum and work up to 255
    intWhiteOptoCounter = 255                   #variable to store the value of the white opto level being sent out to the T-Pad, start at 255 and work down to 0
    booSettingBlackOptoSensitivity = 0          #boolean flag set to 1 if setting black opto sensitivity
    booSettingWhiteOptoSensitivity = 0          #boolean flag set to 1 if setting white opto sensitivity
    intBlackOptoSensitivity = 0                 #value between 0 - 255 for optimum black sensitivity
    intWhiteOptoSensitivity = 0                 #value between 0 - 255 for optimum white sensitivity
    intOpto1Sensitivity = 0                     #final optimum sensitivity value setting for Opto 1
    intSerialSentCount = 0                      #incrementing counter for the current serial command sent to the T-Pad. used to ensure that sent commands and returns match up and for debugging
    intSerialRecdCount = 0                      #incrementing counter for the last serial command received from the T-Pad after something was sent to it
    strLastSerialReceived = ""                  #last message received from the T-Pad over serial
    strTemp = ""                                #temporary variable used then formatting output
    
    #setup white strDecodedSerialData block to help you position Opto 1 over the correct opto marker on your screen
    white_block = visual.Rect(win, width=0.10, height=0.10)
    white_block.pos = (0.87, 0)
    white_block.fillColor = 'white'
    
    #setup BLACK opto marker, i.e. when a visual stimulus is off or not shown on screen
    black_stimulus = visual.Rect(win, width=0.10, height=0.10)
    black_stimulus.pos = (0.87, 0)
    black_stimulus.fillColor = 'black'
    
    #setup WHITE opto marker, i.e. when a visual stimulus is on or shown shown on screen
    white_stimulus = visual.Rect(win, width=0.10, height=0.10)
    white_stimulus.pos = (0.87, 0)
    white_stimulus.fillColor = 'white'
    
    #setup strDecodedSerialData text
    text = visual.TextStim(win=win, name='text',
    text='',
    font='Arial',
    pos=(0, 0), height=0.04, wrapWidth=None, ori=0, 
    color='white', colorSpace='rgb', opacity=1, 
    languageStyle='LTR',
    depth=0.0, alignHoriz='center')
    
    #flush serial buffer and keyboard
    ser.flush()
    ser.reset_input_buffer()
    kb.clearEvents()
    
    #prompt text
    text.text = "Position Opto 1 over the WHITE opto patch shown.\n\n"
    text.text += "Once you are happy press [s] to find optimum sensitivity level for Opto 1.\n\n"
    text.text += "Press [q] to quit or [s] to restart."
    text.draw() #prepare the prompt text
    white_block.draw() #prepare a solid white block to help you position Opto 1 over where the stimulus opto markers will appear on screen
    win.flip() #show the prompt text and white block on screen
    
    #The message loop is an obligatory section of code in every program that uses a graphical user interface under Microsoft Windows.
    #Windows programs that have a GUI are event-driven. Windows maintains an individual message queue for each thread that has created a window.
    #Usually only the first thread creates windows. Windows places messages into that queue whenever mouse activity occurs on that thread's
    #window, whenever keyboard activity occurs while that window has focus, and at other times. A process can also add messages to its own queue.
    #To accept user input, and for other reasons, each thread with a window must continuously retrieve messages from its queue, and act on them.
    #A programmer makes the process do that by writing a loop that calls GetMessage (which blocks for a message and retrieves it), and then calls
    #DispatchMessage (which dispatches the message), and repeats indefinitely. This is the message loop. There usually is a message loop in the
    #main program, which runs on the main thread, and additional message loop in each created modal dialog. Messages for every window of the
    #process pass through its message queue, and are handled by its message loop.A message loop is one kind of event loop.
    #
    #From: https://en.wikipedia.org/wiki/Message_loop_in_Microsoft_Windows#:~:text=A%20programmer%20makes%20the%20process,This%20is%20the%20message%20loop.
    #
    #In this case rather than DispatchMessage we are sending serial commands to the T-Pad, showing text and black or white stimulus blocks on screen
    #or checking for key presses. So we are doing multiple things in the main messaging loop until we quit. This is done to make processing non blocking
    #and also so that we can receive or send commands and do other things out of order, i.e. in a pseudo multi-tasking way.
    
    while (booStopMessageLoop == 0): #keep looping forever in a non blocking messaging loop until the [q] key is pressed on the keyboard to quit
        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
        strDecodedSerialData = strSerialData.decode() #decode into string
    
        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
            strLastSerialReceived = strDecodedSerialData[0] #if there has been a return message that in this case should be a 0 or a 1 grab the first character without anything else, i.e. just the 0 or 1
            intSerialRecdCount = intSerialRecdCount + 1 #increment the serial message return count so we can pair up with what was sent when checking returns
            #print("Recd: (" + str(intSerialRecdCount) + ") " + strLastSerialReceived, flush=True) #debugging if needed
    
        keys = kb.getKeys(waitRelease=False, clear=True) #check to see if any keys have been pressed in the main messaging loop, i.e. [q] for quite or [s] for set opimumum sensitivity for Opto 1
        for thisKey in keys: #if there have been any keys pressed and they are in the key buffer loop through each key and check what it is
            if thisKey == 'q': #check to see if [q] for quit the main messaging loop has been pressed
                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()
                kb.clearEvents()
                booStopMessageLoop = 1 #set the flag to quite the main messaging loop and also quit PsychoPy
                print("END", flush=True) #debugging if needed
                core.quit() #quit PsychoPy
            if thisKey == 's': #check to see if [s] for set optimum sensitivity for Opto 1 has been pressed
                text.text = "Finding optimum BLACK sensitivity level"
                text.draw() #prepare the prompt text
                black_stimulus.draw() #prepare a solid black stimulus marker to set the black level
                win.flip() #show black text and black stimulus marker on screen
                core.wait(5.000) #wait 5 seconds for Opto 1 to stabilise on black
                booSettingBlackOptoSensitivity = 1 #set flags to say we are setting black and not white
                booSettingWhiteOptoSensitivity = 0
                intBlackOptoCounter = 0 #set initial black level to 0
                #reset the serial command sent and recived count to zero as we have not sent anything to or received anything from the T-Pad over serial
                intSerialSentCount = 0
                intSerialRecdCount = 0
                strLastSerialReceived = "" #clear the last serial message returned from the T-Pad
    
        #when setting the optimum black sensitivity level for Opto 1 you need to find a level on a black opto marker where black will return a 1 for on or active
        #rather than a 0 zero for off or stimulus not shown. this is the maximum level where we can distinguish black from white. in short black should always
        #return a 0 zero for off or stimulus not shown.
        if booSettingBlackOptoSensitivity == 1: #if setting the black opto sensitivity then check and set black sensitivity
            if intBlackOptoCounter < 255: #if we are less than the maximum sensitivity then check and set black sensitivity 
                if strLastSerialReceived == "1": #check to see if the last return message from the T-Pad over serial was a 1, i.e. stimulus marker detected?
                    intBlackOptoSensitivity = intBlackOptoCounter #set the black sensitivity to the current level, i.e. the first level where a 1, on, or visual stimulus detected
                    text.text = "Optimum BLACK sensitivity level = " + str(intBlackOptoCounter) #show the level where a black marker triggered a false on or stimulus shown
                    text.draw() #prepare the prompt text
                    white_stimulus.draw() #prepare a solid white stimulus block to set the white level next
                    win.flip() #show the prompt text and white block on screen
                    core.wait(2.000) #wait 2 seconds for Opto 1 to stabilise on white and show black set
                    text.text = "Finding optimum WHITE sensitivity level"
                    text.draw() #prepare the prompt text
                    white_stimulus.draw() #prepare a solid white stimulus marker to set the white level next
                    booSettingBlackOptoSensitivity = 0 #black has been set so don't set again
                    booSettingWhiteOptoSensitivity = 1 #set the white trigger to true so that we check and set the white opto marker level next
                    intWhiteOptoCounter = 255 #set the initial white level to the maximum of 255
                    strLastSerialReceived = "" #clear the last serial message returned from the T-Pad
                    win.flip() #show the prompt text and white block on screen
                    core.wait(2.000) #wait 2 seconds for Opto 1 to stabilise on white
                else: #if a "0" was returned on a black opto marker then increase the level by +1 for the next test, remember for black we start at 0 and can go up to a maximum level of 255
                    if intSerialRecdCount == intSerialSentCount: #if we have received a strLastSerialReceived return from the T-Pad then check it, i.e. the return pair from what we sent
                        intBlackOptoCounter = intBlackOptoCounter + 1 #increase the level for black by +1
                        strTemp = "AAO1 {:1d}\n".format(intBlackOptoCounter) #format the command to send to the T-Pad over serial
                        ser.write(strTemp.encode()) #send the command to the T-Pad over serial after encoding fo a string of bytes, e.g. utf-8
                        intSerialSentCount = intSerialSentCount + 1 #increment the count of serial commands sent so we can look for a return from the T-Pad to equal this count
                        #print("Sent: (" + str(intSerialSentCount) + ") " + "AAO1 {:1d}".format(intBlackOptoCounter), flush=True) #debugging if needed
                        core.wait(0.025) #wait 25 mS for the T-Pad to process the command -- this isn't strictly needed as we are constantly checking for a \n in any return from the T-pad before we do anything else, i.e. a return pair
            if intBlackOptoCounter == 255 and (intSerialRecdCount == intSerialSentCount): #if we have gone all the way up to the max black level, i.e. 255 and we have received a message back from the T-Pad start setting white and set black to 255
                intBlackOptoSensitivity = intBlackOptoCounter #black set to 255
                text.text = "Optimum BLACK sensitivity level = " + str(intBlackOptoCounter) #show the level where a black marker triggered a false on or stimulus shown
                text.draw() #prepare the prompt text
                white_stimulus.draw() #prepare a solid white stimulus block to set the white level next
                win.flip() #show the prompt text and white block on screen
                core.wait(2.000) #wait 2 seconds for Opto 1 to stabilise on white and show black set
                text.text = "Finding optimum WHITE sensitivity level"
                text.draw() #prepare the prompt text
                white_stimulus.draw() #prepare a solid white stimulus marker to set the white level next
                booSettingBlackOptoSensitivity = 0 #black has been set so don't set again
                booSettingWhiteOptoSensitivity = 1 #set the white trigger to true so that we check and set the white opto marker level next
                intWhiteOptoCounter = 255 #set initial white level to 255
                strLastSerialReceived = "" #clear the last serial message returned from the T-Pad
                win.flip() #show white stimulus marker on screen
                core.wait(2.000) #wait 2 seconds for Opto 1 to stabilise on white
    
        if booSettingWhiteOptoSensitivity == 1: #if setting the white opto sensitivity then check and set white sensitivity
            if intWhiteOptoCounter > 0: #if we are greater than the minimum sensitivity then check and set white sensitivity 
                if strLastSerialReceived == "0": #check to see if the last return message from the T-Pad over serial was a 0, i.e. stimulus marker NOT detected?
                    intWhiteOptoSensitivity = intWhiteOptoCounter #set the white sensitivity to the current level, i.e. the first level where a 0, on, or visual stimulus NOT detected
                     #set the optimum Opto 1 sensitivity somewhere between the black and white levels using this formula.
                     #that is where a level is so high it will flasely trigger on black opto marker and where it will falsely go off on a white marker.
                    intOpto1Sensitivity = int(intBlackOptoSensitivity - ((intBlackOptoSensitivity - intWhiteOptoSensitivity) / 2.5))
                    strTemp = "AAO1 {:1d}\n".format(intOpto1Sensitivity) #send that optimum level back to the T-Pad to set -- this isn't strictly required as that was the last level set and checked.
                    ser.write(strTemp.encode()) #send the command to the T-Pad over serial after encoding fo a string of bytes, e.g. utf-8
                    core.wait(0.025) #wait 25 mS for the T-Pad to process the command -- this isn't strictly needed as we are constantly checking for a \n in any return from the T-pad before we do anything else, i.e. a return pair
                    #prompt text to show what levels have been checked and where the optimum sensitivity level is for Opto 1, i.e. where it can differentiate black from white opto markers, i.e. no visual stimulus and visual stimulus shown
                    text.text = "Optimum BLACK sensitivity level = " + str(intBlackOptoCounter) + "\n\n"
                    text.text += "Optimum WHITE sensitivity level = " + str(intWhiteOptoCounter) + "\n\n"
                    text.text += "Optimum Opto 1 sensitivity set to = " + str(intOpto1Sensitivity) + "\n\n"
                    text.text += "You should use a sensitivity of "+ str(intOpto1Sensitivity) + " for Opto 1 in your experiments." + "\n\n"
                    text.text += "Remember if you change your screens brightness or contrast you should rerun this sensitivity setting check." + "\n\n"
                    text.text += "Press [q] to quit or [s] to restart."
                    text.draw() #prepare the prompt text
                    white_stimulus.draw() #prepare a solid white stimulus marker
                    booSettingBlackOptoSensitivity = 0 #set the boolean flags to say we are no longer checking for black or white opto markers and we have set the optimum sensitivity level for Opto 1
                    booSettingWhiteOptoSensitivity = 0
                    strLastSerialReceived = "" #clear the last serial message returned from the T-Pad
                    win.flip() #show levels on screen
                else:
                    if intSerialRecdCount == intSerialSentCount: #if we have received a strLastSerialReceived return from the T-Pad then check it, i.e. the return pair from what we sent
                        intWhiteOptoCounter = intWhiteOptoCounter - 1 #increase the level for white by -1
                        strTemp = "AAO1 {:1d}\n".format(intWhiteOptoCounter) #format the command to send to the T-Pad over serial
                        ser.write(strTemp.encode()) #send the command to the T-Pad over serial after encoding fo a string of bytes, e.g. utf-8
                        intSerialSentCount = intSerialSentCount + 1 #increment the serial message return count so we can pair up with what was sent when checking returns
                        #print("Sent: (" + str(intSerialSentCount) + ") " + "AAO1 {:1d}".format(intWhiteOptoCounter), flush=True)  #debugging if needed
                        core.wait(0.025) #wait 25 mS for the T-Pad to process the command -- this isn't strictly needed as we are constantly checking for a \n in any return from the T-pad before we do anything else, i.e. a return pair
            if intWhiteOptoCounter == 0 and (intSerialRecdCount == intSerialSentCount): #if we have gone all the way down to the min white level, i.e. 0 and we have received a message back from the T-Pad and set white to 0
                intWhiteOptoSensitivity = intWhiteOptoCounter #set the white sensitivity to the current level, i.e. the first level where a 0, on, or visual stimulus NOT detected
                #set the optimum Opto 1 sensitivity somewhere between the black and white levels using this formula.
                #that is where a level is so high it will flasely trigger on black opto marker and where it will falsely go off on a white marker.
                intOpto1Sensitivity = int(intBlackOptoSensitivity - ((intBlackOptoSensitivity - intWhiteOptoSensitivity) / 2.5))
                strTemp = "AAO1 {:1d}\n".format(intOpto1Sensitivity) #send that optimum level back to the T-Pad to set -- this isn't strictly required as that was the last level set and checked.
                ser.write(strTemp.encode()) #send the command to the T-Pad over serial after encoding fo a string of bytes, e.g. utf-8
                core.wait(0.025) #wait 25 mS for the T-Pad to process the command -- this isn't strictly needed as we are constantly checking for a \n in any return from the T-pad before we do anything else, i.e. a return pair
                text.text = "Optimum BLACK sensitivity level = " + str(intBlackOptoCounter) + "\n\n"
                text.text += "Optimum WHITE sensitivity level = " + str(intWhiteOptoCounter) + "\n\n"
                text.text += "Optimum Opto 1 sensitivity set to = " + str(intOpto1Sensitivity) + "\n\n"
                text.text += "You should use a sensitivity of "+ str(intOpto1Sensitivity) + " for Opto 1 in your experiments." + "\n\n"
                text.text += "Remember if you change your screens brightness or contrast you should rerun this sensitivity setting check." + "\n\n"
                text.text += "Press [q] to quit or [s] to restart."
                text.draw() #prepare the prompt text
                white_stimulus.draw() #prepare a solid white stimulus marker
                booSettingBlackOptoSensitivity = 0 #set the boolean flags to say we are no longer checking for black or white opto markers and we have set the optimum sensitivity level for Opto 1
                booSettingWhiteOptoSensitivity = 0
                strLastSerialReceived = "" #clear the last serial message returned from the T-Pad
                win.flip() #show levels on screen
    
    # store start times for trial
    trial.tStartRefresh = win.getFutureFlipTime(clock=globalClock)
    trial.tStart = globalClock.getTime(format='float')
    trial.status = STARTED
    thisExp.addData('trial.started', trial.tStart)
    trial.maxDuration = None
    # keep track of which components have finished
    trialComponents = trial.components
    for thisComponent in trial.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 "trial" ---
    trial.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
        
        # 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
            trial.forceEnded = routineForceEnded = True
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in trial.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 "trial" ---
    for thisComponent in trial.components:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    # store stop times for trial
    trial.tStop = globalClock.getTime(format='float')
    trial.tStopRefresh = tThisFlipGlobal
    thisExp.addData('trial.stopped', trial.tStop)
    thisExp.nextEntry()
    # the Routine "trial" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()
    # Run 'End Experiment' code from code
    print("END", flush=True) #debugging if needed
    ser.write("X".encode()) #stop doing whatever it is doing and return to MOD0 or command mode
    time.sleep(0.500) #have a small delay to allow the T-Pad to go into Mode 0
    #flush serial buffer and keyboard
    ser.flush()
    ser.reset_input_buffer()
    ser.close()
    kb.clearEvents()
    
    
    # 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


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()
    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)
