diff --git a/.github/workflows/build_executables.yml b/.github/workflows/build_executables.yml new file mode 100644 index 0000000..46f0624 --- /dev/null +++ b/.github/workflows/build_executables.yml @@ -0,0 +1,118 @@ +name: Build Executables + +on: + + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build-windows: + runs-on: windows-latest + steps: + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + with: + detached: true + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} + - uses: conda-incubator/setup-miniconda@v3 + with: + auto-update-conda: true + activate-environment: kivy20 + python-version: "3.10" + channels: conda-forge + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive # Initializes and updates submodules + - name: create env + shell: bash -l {0} + run: conda install -y kivy=2.3 pyinstaller=4.10 requests glfw + - name: pip installs + shell: bash -l {0} + run: pip install kivy-deps.sdl2 kivy-deps.glew pyo + - name: install smile + shell: bash -l {0} + run: pip install -e smile + - name: create files + shell: bash -l {0} + run: echo $SI > serverinfo.txt && echo $ULCRT > cert.pem + - name: package cogmood + shell: bash -l {0} + run: | + cd package + export KIVY_GL_BACKEND=angle_sdl2 + python -m PyInstaller cogmood_windows.spec + - name: save exe + uses: actions/upload-artifact@v3 + with: + name: SUPREME + path: package/dist/SUPREME.exe + + + build-macos: + runs-on: macos-14 + steps: + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + with: + detached: true + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} + + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive # Initializes and updates submodules + + - name: Install Homebrew dependencies + run: | + brew update + brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" # Specify your Python version + + - name: Check if Python is a fat binary + run: | + if lipo -info $(which python3) | grep -q "Architectures in the fat file"; then + lipo -info $(which python3) + echo "Python is a fat binary." + export PATH=/Users/runner/hostedtoolcache/Python/3.10.11/arm64/bin:$PATH + else + echo "Python is NOT a fat binary." + exit 1 # Optional: Exit with error if you require a fat binary + fi + + - name: Install dependencies + run: | + export PATH=/Users/runner/hostedtoolcache/Python/3.10.11/arm64/bin:$PATH + mkdir python_packages + python3 -m pip install --upgrade pip + python3 -m pip install --target $PWD/python_packages --only-binary=:all: --platform macosx_10_13_universal2 -r requirements.txt + export PYTHONPATH=$PWD/python_packages:$PYTHONPATH + + + + - name: Build executable with PyInstaller + run: | + export PATH=/Users/runner/hostedtoolcache/Python/3.10.11/arm64/bin:$PATH + export PYTHONPATH=$PWD/python_packages:$PYTHONPATH + cd package + python3 -m PyInstaller --noconfirm cogmood_mac.spec + + - name: Upload macOS executable + uses: actions/upload-artifact@v4 + with: + name: SUPREME + path: package/dist/SUPREME diff --git a/.github/workflows/mac-build.yml b/.github/workflows/mac-build.yml deleted file mode 100644 index cb94a91..0000000 --- a/.github/workflows/mac-build.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: mac build -on: - push: - branches: - - master - workflow_dispatch: -jobs: - mac-build: - runs-on: macos-10.15 - environment: build - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - activate-environment: kivy20 - python-version: 3.9 - channels: conda-forge - - uses: actions/checkout@v3 - with: - submodules: true - - name: create env - shell: bash -l {0} - run: conda install -y kivy=2.0 pyinstaller=4.10 requests - - name: pip installs - shell: bash -l {0} - run: pip install pyo pyperclip - - name: install smile - shell: bash -l {0} - run: pip install -e smile - - name: create files - shell: bash -l {0} - env: - SIMAC: ${{ secrets.SIMAC }} - ULCRTMAC: ${{ secrets.ULCRTMAC }} - run: echo "$SIMAC" > serverinfo.txt && echo "$ULCRTMAC" > cert.pem - - name: package cogmood - shell: bash -l {0} - run: cd package && python -m PyInstaller cogmood_mackivy20.spec - - name: save exe - uses: actions/upload-artifact@v3 - with: - name: mac package - path: package/dist/test diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml deleted file mode 100644 index 325ec9e..0000000 --- a/.github/workflows/windows-build.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: windows build -on: - workflow_dispatch: -jobs: - windows-build: - runs-on: windows-latest - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - activate-environment: kivy20 - python-version: 3.9 - channels: conda-forge - - uses: actions/checkout@v3 - with: - submodules: true - - name: create env - shell: bash -l {0} - run: conda install -y kivy=2.0 pyinstaller=4.10 requests - - name: pip installs - shell: bash -l {0} - run: pip install kivy-deps.sdl2 kivy-deps.glew pyo - - name: install smile - shell: bash -l {0} - run: pip install -e smile - - name: create files - shell: bash -l {0} - run: echo $SI > serverinfo.txt && echo $ULCRT > cert.pem - - name: package cogmood - shell: bash -l {0} - run: cd package && python -m PyInstaller cogmood_winkivy20.spec - - name: save exe - uses: actions/upload-artifact@v3 - with: - name: windows exe - path: package/dist/test.exe diff --git a/.gitignore b/.gitignore index b897001..ee158d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Stimuli extracted for AssBind +tasks/AssBind/stim/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -31,7 +34,6 @@ MANIFEST # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest -*.spec # Installer logs pip-log.txt @@ -127,3 +129,6 @@ dmypy.json # Pyre type checker .pyre/ + +# MacOS +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 4974dd6..4c3ef09 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,40 @@ # SUPREMEMOOD +## Updated Non-Conda Build Instructions +**Note:** Must use Git Bash or WSL on Windows for compatibility with Make + + +Create virtual environment: +```bash +python -m venv .venv +``` + +Activate venv: +- Windows: +```bash +source .venv/Scripts/activate +``` + +- Mac: +```bash +source .venv/bin/activate +``` + +Install requirements: +```bash +pip install -r requirements.txt +``` + +Go to package directory: +```bash +cd package +``` + +Use Makefile to call PyInstaller (The Makefile will check for MacOS or Windows or other & call PyInstaller or output message if OS not supported): +```bash +make +``` + ## Windows build instructions ### Kivy 2.0 Pysinstaller 5.0 took out some tk hooks that kivy 2.0 depends on. kivy 2.1 fixes that, and could be used with pyinstaller 5.0, but there's a mouse issue with kivy 2.1. @@ -8,7 +43,7 @@ After cloning this repo ```commandline conda create -p ./env_kivy20 -c conda-forge python=3.9 kivy=2.0 pyinstaller=4.10 requests conda activate ./env_kivy20 -pip install kivy-deps.sdl2 kivy-deps.glew pyo pyperclip +pip install kivy-deps.sdl2 kivy-deps.glew pyo pip install -e cogmood/smile cd cogmood/package python -m PyInstaller cogmood_winkivy20.spec @@ -20,7 +55,7 @@ After cloning this repo ```commandline conda create -p ./env_kivy20 -c conda-forge python=3.9 kivy=2.0 pyinstaller=4.10 requests conda activate ./env_kivy20 -pip install pyo pyperclip +pip install pyo pip install -e cogmood/smile cd cogmood/package python -m PyInstaller cogmood_mackivy20.spec diff --git a/backend_executable_utils.py b/backend_executable_utils.py new file mode 100644 index 0000000..ef0e91c --- /dev/null +++ b/backend_executable_utils.py @@ -0,0 +1,151 @@ +import logging +import os +import plistlib +import shutil +from typing import Optional +from pathlib import Path +from pefile import PE, DIRECTORY_ENTRY + +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + + +def edit_app_worker_id(app_path: str, new_worker_id: str, output_app_path: Optional[str] = None) -> None: + """ + Modifies the 'WorkerID' field in the Info.plist of a macOS .app bundle. + + Args: + app_path (str): Path to the original .app bundle whose 'WorkerID' will be modified. + new_worker_id (str): New 'WorkerID' value to replace the existing one. + output_app_path (Optional[str]): Path to save the modified .app bundle. If not provided, + the original bundle will be overwritten. + + Raises: + FileNotFoundError: If the specified .app bundle or Info.plist file does not exist. + ValueError: If the Info.plist file cannot be loaded or parsed. + """ + + # Construct the path to the Info.plist file inside the original .app bundle + plist_path = Path(app_path) / 'Contents' / 'Info.plist' + + # Ensure the .app bundle and Info.plist exist + if not plist_path.exists(): + raise FileNotFoundError(f"The file {plist_path} does not exist.") + + try: + # If an output path is specified, copy the original .app bundle to the new location + if output_app_path: + output_path = Path(output_app_path) + if output_path.exists(): + raise FileExistsError( + f"The output path {output_app_path} already exists.") + shutil.copytree(app_path, output_app_path) + # Update plist path to the new location + plist_path = output_path / 'Contents' / 'Info.plist' + else: + logging.info( + "No output path specified. The original .app bundle will be modified.") + + # Load the plist file + with plist_path.open('rb') as plist_file: + plist_data = plistlib.load(plist_file) + + # Log current WorkerID, if present + current_worker_id = plist_data.get('WorkerID', None) + if current_worker_id: + logging.info(f"Current WorkerID: {current_worker_id}") + + # Update the WorkerID + plist_data['WorkerID'] = new_worker_id + + # Write the updated plist back + with plist_path.open('wb') as plist_file: + plistlib.dump(plist_data, plist_file) + + logging.info(f"Successfully updated WorkerID to {new_worker_id}") + + except Exception as e: + raise ValueError(f"Failed to modify the plist file: {e}") + + +def edit_exe_worker_id(exe_file_path: str, new_worker_id: str, output_file_path: Optional[str] = None) -> None: + """ + Modifies the 'WorkerID' field in the version information of an executable. + + Args: + exe_file_path (str): Path to the executable whose 'WorkerID' will be modified. + new_worker_id (str): New 'WorkerID' value to replace the existing one. + **Must** be less than or equal to the length of the current 'WorkerID' for a successful update. + output_file_path (str): **Optional**. Path where the modified executable will be saved. + If not provided, the original executable will be overwritten. + + Raises: + FileNotFoundError: If the specified executable does not exist. + ValueError: If the PE file cannot be loaded or parsed. + """ + + if not os.path.exists(exe_file_path): + raise FileNotFoundError(f"The file {exe_file_path} does not exist.") + + output_file_path = Path(output_file_path or exe_file_path) + + try: + pe: PE = PE(exe_file_path, fast_load=True) + pe.parse_data_directories( + directories=[DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) + except Exception as e: + raise ValueError(f"Failed to load or parse the PE file: {e}") + + # Access the WorkerID and update it if found + if hasattr(pe, 'FileInfo'): + for file_info in pe.FileInfo: + for info in file_info: + if info.name == 'StringFileInfo': + version_info_dict: dict = info.StringTable[0].entries + worker_id_bytes: bytes = version_info_dict.get( + b'WorkerID', None) + + if worker_id_bytes: + logging.info( + f"Found WorkerID: {worker_id_bytes.decode('utf-8')}") + + # Calculate the original size in bytes and the new value size + original_size: int = len(worker_id_bytes) + new_worker_id_bytes: bytes = new_worker_id.encode( + 'utf-8') + new_size: int = len(new_worker_id_bytes) + + if new_size <= original_size: + # Create the new value padded with null bytes up to the original size + padded_new_worker_id: bytes = new_worker_id_bytes + \ + b'\x00' * (original_size - new_size) + + # Update the value with the padded version + version_info_dict[b"WorkerID"] = padded_new_worker_id + + # Write the updated attribute to the output file + pe.write(output_file_path) + logging.info( + f"Successfully updated WorkerID to {new_worker_id}") + else: + logging.error( + f"Error: New value '{new_worker_id}' is larger than the existing space of {original_size} bytes.") + return + + logging.error(f"Error: WorkerID not found in {exe_file_path}") + + +if __name__ == "__main__": + # Testing app WorkerID editing + edit_app_worker_id( + app_path="package/dist/SUPREME.app", + new_worker_id="new_worker_id_value", + # output_app_path="/path/to/output/app_bundle.app" # Optional + ) + + # Testing exe WorkerID editing + # edit_exe_worker_id( + # exe_file_path="package\\dist\\test.exe", + # new_worker_id='"sample_24_char_WorkerID".sample_validation_signature', + # output_file_path="output.exe" # Optional + # ) diff --git a/config.py b/config.py index a59674e..eac1cd4 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,16 @@ # -*- coding: utf-8 -*- import os - +import sys +import platform + +CURRENT_OS: str = platform.system() +API_BASE_URL: str = 'http://localhost:5000' +RUNNING_FROM_EXECUTABLE: bool = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') +# WORKER_ID_PLACEHOLDER_VALUE is the placeholder value assigned to the WorkerID field +# in the executables when we build them. It should be replaced by actual ID when +# the executable is prepared for distribution. +WORKER_ID_PLACEHOLDER_VALUE = '"------------------------".---------------------------' EXP_NAME = "SUPREMEMOOD" BACKGROUND_COLOR = (.35, .35, .35, 1.0) @@ -19,17 +28,12 @@ DISPLAY_FONT = 45 BUTTON_WH = 200 -TASKS = [[['cab', True], ['flkr', True], ['rdm', True], ['bart', True]], - [['cab', False], ['flkr', False], ['rdm', False], ['bart', False]],] - -#TASKS = [[['rdmf', False], ['bart', False], ['barta', False],['barts', False], ]] NUM_REPS = 1 FMRI_TASKS = [['cab', 'flkr', 'rdm', 'bart'], ['cab', 'flkr', 'rdm'], ['cab', 'flkr', 'rdm']] FMRI_REPS = 1 SHUFFLE_TASKS = True -HAPPY_SPEED = .5 RESP_KEYS = ['F', 'J'] CONT_KEY = ['SPACEBAR'] FMRI_TR = ['T'] @@ -55,16 +59,6 @@ TIME_BETWEEN_HAPPY = 15 TIME_JITTER_HAPPY = 10 -HAPPY_FONT_SIZE = 25 -HAPPY_INC_BASE = .02 -HAPPY_INC_START = .2 -HAPPY_MOD = 20. -HAPPY_RANGE = 10 -NON_PRESS_INT = .1 -PRESS_INT = .016 -SLIDER_WIDTH = 1000 -RESP_HAPPY = ["F", "J"] - INST_FONT = 25 INST_TEXT = "You will perform 4 tasks in this experiment, and some of the tasks more than once. Instructions will be displayed prior to each task. Please read the instructions for each task very carefully.\n\nFor each task, you will make responses by pressing the %s button with one hand, and the %s button with your other hand.\n\nSet aside 40 minutes to an hour. You are able to take short breaks between each task.\n\nOnce the experiment fully begins, you may end the experiment by pressing the escape key. The window will close, and your data up till that point will be sent to us and your payment will be forfeit. Press any key to proceed." diff --git a/error_screen.py b/error_screen.py new file mode 100644 index 0000000..a8b3d6a --- /dev/null +++ b/error_screen.py @@ -0,0 +1,24 @@ +from smile.common import Subroutine, BoxLayout, Label, scale +from config import SSI_FONT_SIZE + +@Subroutine +def error_screen(self, error, message): + with BoxLayout(orientation='vertical', spacing=20): + Label(name="error_details", + text=error, + text_size=(scale(700), None), font_size=scale(SSI_FONT_SIZE), + size_hint=(1, None), + valign='middle', + halign='center') + Label(name="error_message", + text=message, + text_size=(scale(700), None), font_size=scale(SSI_FONT_SIZE), + size_hint=(1, None), + valign='middle', + halign='center') + Label(name="error_exit_instructions", + text="Press escape to exit.", + text_size=(scale(700), None), font_size=scale(SSI_FONT_SIZE), + size_hint=(1, None), + valign='middle', + halign='center') diff --git a/inscructions/AssociativeBinding/ASSBIND0001.png b/inscructions/AssociativeBinding/ASSBIND0001.png deleted file mode 100644 index bf5aa92..0000000 Binary files a/inscructions/AssociativeBinding/ASSBIND0001.png and /dev/null differ diff --git a/inscructions/AssociativeBinding/ASSBIND0002.png b/inscructions/AssociativeBinding/ASSBIND0002.png deleted file mode 100644 index 8b8458d..0000000 Binary files a/inscructions/AssociativeBinding/ASSBIND0002.png and /dev/null differ diff --git a/inscructions/AssociativeBinding/ASSBIND0003.png b/inscructions/AssociativeBinding/ASSBIND0003.png deleted file mode 100644 index 55e048b..0000000 Binary files a/inscructions/AssociativeBinding/ASSBIND0003.png and /dev/null differ diff --git a/inscructions/AssociativeBinding/ASSBIND0004.png b/inscructions/AssociativeBinding/ASSBIND0004.png deleted file mode 100644 index 15d4c33..0000000 Binary files a/inscructions/AssociativeBinding/ASSBIND0004.png and /dev/null differ diff --git a/inscructions/AssociativeBinding/ASSBIND0005.png b/inscructions/AssociativeBinding/ASSBIND0005.png deleted file mode 100644 index 098ba06..0000000 Binary files a/inscructions/AssociativeBinding/ASSBIND0005.png and /dev/null differ diff --git a/inscructions/AssociativeBinding/ASSBIND0006.png b/inscructions/AssociativeBinding/ASSBIND0006.png deleted file mode 100644 index 9921d53..0000000 Binary files a/inscructions/AssociativeBinding/ASSBIND0006.png and /dev/null differ diff --git a/inscructions/BART/BART0001.png b/inscructions/BART/BART0001.png deleted file mode 100644 index 6c427de..0000000 Binary files a/inscructions/BART/BART0001.png and /dev/null differ diff --git a/inscructions/BART/BART0002.png b/inscructions/BART/BART0002.png deleted file mode 100644 index ac5ef24..0000000 Binary files a/inscructions/BART/BART0002.png and /dev/null differ diff --git a/inscructions/BART/BART0003.png b/inscructions/BART/BART0003.png deleted file mode 100644 index eba0470..0000000 Binary files a/inscructions/BART/BART0003.png and /dev/null differ diff --git a/inscructions/Flanker/FLANKER0001.png b/inscructions/Flanker/FLANKER0001.png deleted file mode 100644 index 4913c49..0000000 Binary files a/inscructions/Flanker/FLANKER0001.png and /dev/null differ diff --git a/inscructions/Flanker/FLANKER0002.png b/inscructions/Flanker/FLANKER0002.png deleted file mode 100644 index 0d8ea10..0000000 Binary files a/inscructions/Flanker/FLANKER0002.png and /dev/null differ diff --git a/inscructions/Flanker/FLANKER0003.png b/inscructions/Flanker/FLANKER0003.png deleted file mode 100644 index ec41a1e..0000000 Binary files a/inscructions/Flanker/FLANKER0003.png and /dev/null differ diff --git a/inscructions/Flanker/FLANKER0004.png b/inscructions/Flanker/FLANKER0004.png deleted file mode 100644 index 716905a..0000000 Binary files a/inscructions/Flanker/FLANKER0004.png and /dev/null differ diff --git a/inscructions/Flanker/FLANKER0005.png b/inscructions/Flanker/FLANKER0005.png deleted file mode 100644 index e5d8f44..0000000 Binary files a/inscructions/Flanker/FLANKER0005.png and /dev/null differ diff --git a/inscructions/RDM/RDM0001.png b/inscructions/RDM/RDM0001.png deleted file mode 100644 index 3ccc937..0000000 Binary files a/inscructions/RDM/RDM0001.png and /dev/null differ diff --git a/list_gen.py b/list_gen.py deleted file mode 100644 index 81ae5af..0000000 --- a/list_gen.py +++ /dev/null @@ -1,18 +0,0 @@ - -from random import shuffle -import copy - -def gen_order(config): - blocks = [] - these_tasks = copy.deepcopy(config.TASKS) - shuffle(these_tasks) - for i in range(config.NUM_REPS): - tasks = copy.deepcopy(these_tasks) - for task_block in tasks: - if config.SHUFFLE_TASKS: - shuffle(task_block) - if len(blocks) > 0: - while((blocks[-1][-1] == task_block[0]) or task_block in blocks): - shuffle(task_block) - blocks.append(task_block) - return blocks diff --git a/main.py b/main.py index 587ea4d..36f474e 100644 --- a/main.py +++ b/main.py @@ -1,107 +1,29 @@ # General imports import os -from os.path import join import sys -import requests -import json -import subprocess -import zipfile -import hashlib # Smile imports -from smile.common import Experiment, Log, Wait, Func, UntilDone, ButtonPress, \ - Button, Label, Loop, If, Elif, Else, KeyPress, Ref,\ - Parallel, Slider, MouseCursor, Rectangle, Meanwhile,\ - Serial, Debug, Screenshot, Questionnaire, UpdateWidget +from smile.common import Experiment, Log, Wait, Func, UntilDone, \ + Label, Loop, If, Elif, Else, KeyPress, Ref, \ + Parallel, Slider, Serial, UpdateWidget, Debug, Meanwhile from smile.clock import clock -from smile.lsl import init_lsl_outlet, LSLPush from smile.scale import scale as s -from smile.startup import InputSubject -#from android.permissions import request_permissions, Permission -#request_permissions([Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE]) +# from android.permissions import request_permissions, Permission +# request_permissions([Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE]) from kivy.resources import resource_add_path # CogBatt general imports for running and organizing the experiment. import config as CogBatt_config -from list_gen import gen_order +from utils import retrieve_worker_id, \ + get_blocks_to_run, upload_block import version -# pyperclip for copying to clipboard -import pyperclip - # Various task imports from tasks import FlankerExp, Flanker_config from tasks import RDMExp, RDM_config from tasks import AssBindExp, AssBind_config from tasks import BartuvaExp, Bartuva_config +from tasks import HappyQuest -def zip_directory(folder_path, zip_path): - with zipfile.ZipFile(zip_path, mode='w') as zipf: - len_dir_path = len(folder_path) - for root, _, files in os.walk(folder_path): - for file in files: - file_path = os.path.join(root, file) - zipf.write(file_path, file_path[len_dir_path:]) -def ToOut(message, exp, post_urlFULL): - failed_post = False - failed_copy = False - print(exp._subject_dir) - to_zip = "data.zip" - print(to_zip) - try: - zip_directory(exp._session_dir, to_zip) - except BaseException as err: - print(f"Unexpected {err=}, Could not zip directory.{type(err)=}") - raise err - - status_code = None - try: - with open(to_zip, 'rb') as f: - data = f.read() - print(post_urlFULL) - r = requests.post(post_urlFULL, - data={'results':data}, - verify=os.path.join(WRK_DIR, "cert.pem"), - allow_redirects=False, - timeout=120) - print(r.status_code) - status_code = r.status_code - except BaseException as err: - print(f"Unexpected {err=}, {type(err)=}") - failed_post = True - if status_code != 200: - failed_post = True - m = 'e0000000000' - if (not (message is None)) & (not failed_post): - m = message['extra'][10:-5] - with open('confirmation_code.txt', 'w') as f: - f.write(message['extra'][10:-5]) - - try: - pyperclip.copy(message['extra'][10:-5]) - except BaseException as err: - print(f"Unexpected {err=}, Could not copy to clipboard: {type(err)=}") - failed_copy = True - else: - with open(to_zip, 'rb') as f: - data = f.read() - m = "e" + hashlib.md5(data).hexdigest()[:10] - try: - pyperclip.copy(m) - except BaseException as err: - print(f"Unexpected {err=}, Could not copy to clipboard: {type(err)=}") - failed_copy = True - - return failed_post, failed_copy, m -#----------------WRK_DIR EDITS HERE---------------- -# edited so the data_dir is the WRK_DIR if running from the packaged exe -# otherwise the data_dir is '.' -if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): - WRK_DIR = sys._MEIPASS -else: - WRK_DIR = '.' - -print(WRK_DIR) -if hasattr(sys, '_MEIPASS'): - resource_add_path(os.path.join(sys._MEIPASS)) +from error_screen import error_screen # Different configs getting set for the different subject names. If their ID @@ -127,44 +49,27 @@ def ToOut(message, exp, post_urlFULL): Flanker_config.RESP_KEYS = CogBatt_config.RESP_KEYS[:] Flanker_config.CONT_KEY = CogBatt_config.CONT_KEY AssBind_config.RESP_KY = CogBatt_config.RESP_KEYS[:] -AssBind_config.RESP_KEYS = {'old':'F', "new":'J'} +AssBind_config.RESP_KEYS = {'old': 'F', "new": 'J'} AssBind_config.CONT_KEY = CogBatt_config.CONT_KEY Bartuva_config.RESP_KEYS = CogBatt_config.RESP_KEYS[:] Bartuva_config.CONT_KEY = CogBatt_config.CONT_KEY -# If we have an eeg experiment, then we need to initialize the lsl outlet. +# Define default WRK_DIR as current directory +WRK_DIR = '.' + +# Check if running from an executable +if CogBatt_config.RUNNING_FROM_EXECUTABLE: + # Update WRK_DIR to the location of the executable + WRK_DIR = sys._MEIPASS + resource_add_path(WRK_DIR) + +retrieved_worker_id = retrieve_worker_id() -pulse_server = None +tasks_from_api = get_blocks_to_run(retrieved_worker_id['content']) +number_of_tasks = 0 if tasks_from_api['status'] == 'error' else len(tasks_from_api['content']) -# Generates the block order for the tasks. All tasks must be presented before -# another is repeated, except BART which is repeated half as often. Also, no -# task can repeat directly following itself. -blocks = gen_order(CogBatt_config) -print(blocks) -# Do the get -print('About to get') -with open(os.path.join(WRK_DIR, 'serverinfo.txt'), 'r') as f: - serverinfo = f.readline().strip() - post_urlFULL = f.readline().strip() - print(serverinfo, post_urlFULL) -try: - r = requests.get(serverinfo, verify=os.path.join(WRK_DIR, "cert.pem"), - timeout=2) - print(r.text) - message = json.loads(r.text.replace("\'", "\"")) - post_urlFULL = post_urlFULL.format(message['platformid'], - message['sqlid']) - connected = True - to_message = message['extra'][10:-5] -except: - print("NO CONNECTION") - message = None - post_urlFULL = None - connected = False - to_message = "Yo" -print("connected: ", connected) # Initialize the SMILE experiment. exp = Experiment(name=CogBatt_config.EXP_NAME, background_color=CogBatt_config.BACKGROUND_COLOR, @@ -173,7 +78,9 @@ def ToOut(message, exp, post_urlFULL): cmd_traceback=False, data_dir=WRK_DIR, working_dir=WRK_DIR) -InputSubject(exp_title="Supreme") +exp.tasks_from_api = tasks_from_api +exp.worker_id_dict = retrieved_worker_id + with Parallel(): with Serial(blocking=False): # Log all of the info about the subject and the CogBatt version @@ -185,87 +92,40 @@ def ToOut(message, exp, post_urlFULL): Wait(.5) - # Give participants the option to record demographic information, but only on - # their first visit, the behavioral visit. - - #Demographics(CogBatt_config) - #Wait(1.0) - - # Present intial CogBatt instructions. + with If(CogBatt_config.RUNNING_FROM_EXECUTABLE): + # Handles case where retrieval of worker id fails + with If(exp.worker_id_dict['status'] == 'error'): + error_screen(error='Failed to Retrieve Identifier: ' + exp.worker_id_dict['content'], + message='Contact Dylan Nielson') + # Handles case where retrieval of worker id is default placeholder + with Elif(exp.worker_id_dict['content'] == CogBatt_config.WORKER_ID_PLACEHOLDER_VALUE): + error_screen(error='Non-Unique Identifier', + message='Contact Dylan Nielson') + # Error screen for failed GET request to retrieve blocks + with If(exp.tasks_from_api['status'] == 'error'): + error_screen(error='Failed to retrieve tasks.', + message=exp.tasks_from_api['content']) + # Handles case where there are no more blocks to run + with Elif(number_of_tasks == 0): + error_screen(error='No tasks to run.', + message='Press next in the browser or return to the website via the link from prolific if that window is no longer open.') + + # Present initial CogBatt instructions. Label(text=CogBatt_config.INST_TEXT, font_size=s(CogBatt_config.INST_FONT), text_size=(s(700), None)) with UntilDone(): KeyPress() - Label(text=CogBatt_config.HAPPY_TEXT, text_size=(s(700), None), font_size=s(CogBatt_config.INST_FONT)) with UntilDone(): KeyPress() + Wait(.3) - with Parallel(): - Label(text="Taken all together, how happy are you with your life these days?\nPress F to move left, Press J to move right.", - font_size=s(CogBatt_config.HAPPY_FONT_SIZE), - halign='center', - center_y=exp.screen.center_y + s(300)) - sld = Slider(min=-10, max=10, value=0, width=s(CogBatt_config.SLIDER_WIDTH)) - Label(text="unhappy", font_size=s(CogBatt_config.HAPPY_FONT_SIZE), - center_x=sld.left, center_y=sld.center_y - s(100)) - Label(text="happy", font_size=s(CogBatt_config.HAPPY_FONT_SIZE), - center_x=sld.right, center_y=sld.center_y - s(100)) - Label(text='Press Spacebar to lock-in your response.', - top=sld.bottom - s(250), font_size=s(CogBatt_config.HAPPY_FONT_SIZE)) - - with UntilDone(): - exp.happy_start_time = Ref(clock.now) - exp.last_check = exp.happy_start_time - exp.happy_dur = 0.0 - exp.HAPPY_SPEED = CogBatt_config.HAPPY_INC_BASE - exp.first_press_time = None - with Loop(): - ans = KeyPress(keys=CogBatt_config.RESP_HAPPY) - with If(exp.first_press_time == None): - exp.first_press_time = ans.press_time - with If(ans.press_time['time'] - exp.last_check < - CogBatt_config.NON_PRESS_INT): - exp.HAPPY_SPEED = (CogBatt_config.HAPPY_INC_BASE * (Ref(clock.now) - - exp.happy_start_time) * CogBatt_config.HAPPY_MOD) + CogBatt_config.HAPPY_INC_START - - with Else(): - exp.HAPPY_SPEED = CogBatt_config.HAPPY_INC_START - exp.happy_start_time = Ref(clock.now) - exp.last_check = Ref(clock.now) - - with If(ans.pressed == CogBatt_config.RESP_HAPPY[0]): - with If(sld.value - exp.HAPPY_SPEED <= (-1 * CogBatt_config.HAPPY_RANGE)): - UpdateWidget(sld, value=(-1 * CogBatt_config.HAPPY_RANGE)) - with Else(): - UpdateWidget(sld, value=sld.value - exp.HAPPY_SPEED) - with Elif(ans.pressed == CogBatt_config.RESP_HAPPY[1]): - with If(sld.value + exp.HAPPY_SPEED >= CogBatt_config.HAPPY_RANGE): - UpdateWidget(sld, value=CogBatt_config.HAPPY_RANGE) - with Else(): - UpdateWidget(sld, value=sld.value + exp.HAPPY_SPEED) - - #Wait(.005) - with UntilDone(): - submit = KeyPress(keys=['SPACEBAR']) - Log(name="happy", - task='main', - slider_appear=sld.appear_time, - first_press=exp.first_press_time, - submit_time=submit.press_time, - value=sld.value) - - Wait(.3) - with Parallel(): - Questionnaire(loq=CogBatt_config.CESD, font_size=s(25), - width=s(1100), - x=(exp.screen.width/2.) - s(1100)/2.) - MouseCursor(blocking=False) - + + HappyQuest(task='main', block_num=-1, trial_num=-1) exp.practice = True exp.BART_practice = True @@ -273,102 +133,77 @@ def ToOut(message, exp, post_urlFULL): # Log the block order Log(name="BLOCK_ORDER", - order=blocks) + order=exp.tasks_from_api['content']) Label(text="You will now start the experiments. Press any key to continue.", text_size=(s(700), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) with UntilDone(): KeyPress() Wait(.3) + # Main loop for the experiment - exp.cal_counter = 0 - exp.file_counter = 1 - exp.still_loop = True - - with Loop(blocks) as BL: - with Loop(BL.current) as TL: - with If(TL.current[0] == "flkr"): - Wait(.5) - FlankerExp(Flanker_config, - run_num=BL.i, - lang='E', - happy_mid=TL.current[1]) - with Elif(TL.current[0] == "cab"): - Wait(.5) - - if hasattr(sys, '_MEIPASS'): - taskdir = os.path.join(os.path.join(sys._MEIPASS), "tasks", "AssBind") - else: - taskdir = os.path.join("tasks", "AssBind") - AssBindExp(AssBind_config, - task_dir=taskdir, - sub_dir=Ref.object(exp)._subject_dir, - block=BL.i, - happy_mid=TL.current[1]) - with Elif(TL.current[0] == "rdm"): - Wait(.5) - RDMExp(RDM_config, - run_num=BL.i, + with Loop(exp.tasks_from_api['content']) as task: + exp.task_name = task.current['task_name'] + exp.block_number = task.current['block_number'] + with If(exp.task_name == "flkr"): + Wait(.5) + FlankerExp(Flanker_config, + run_num=exp.block_number, lang='E', - happy_mid=TL.current[1]) - - with Elif(TL.current[0] == "bart"): - Wait(.5) - if hasattr(sys, '_MEIPASS'): - task2dir = os.path.join(os.path.join(sys._MEIPASS), "tasks", "BARTUVA") - else: - task2dir = os.path.join("tasks", "BARTUVA") - BartuvaExp(Bartuva_config, - run_num=BL.i, - sub_dir=Ref.object(exp)._session_dir, - practice=False, - task_dir=task2dir, - happy_mid=TL.current[1]) - - Wait(1.0) - Label(text="You may take a short break!\n\nPress any key when you would like to continue to the next experiment. ", + happy_mid=False) + with Elif(exp.task_name == "cab"): + Wait(.5) + + if CogBatt_config.RUNNING_FROM_EXECUTABLE: + taskdir = os.path.join(os.path.join( + sys._MEIPASS), "tasks", "AssBind") + else: + taskdir = os.path.join("tasks", "AssBind") + AssBindExp(AssBind_config, + task_dir=taskdir, + sub_dir=Ref.object(exp)._subject_dir, + block=exp.block_number, + happy_mid=False) + with Elif(exp.task_name == "rdm"): + Wait(.5) + RDMExp(RDM_config, + run_num=exp.block_number, + lang='E', + happy_mid=False) + + with Elif(exp.task_name == "bart"): + Wait(.5) + if CogBatt_config.RUNNING_FROM_EXECUTABLE: + task2dir = os.path.join(os.path.join( + sys._MEIPASS), "tasks", "BARTUVA") + else: + task2dir = os.path.join("tasks", "BARTUVA") + BartuvaExp(Bartuva_config, + run_num=exp.block_number, + sub_dir=Ref.object(exp)._session_dir, + practice=False, + task_dir=task2dir, + happy_mid=False) + + Wait(.5) + + Label(text="Uploading data...", text_size=(s(700), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - KeyPress() + with UntilDone(): + with Parallel(): + exp.task_data_upload = Func(upload_block, + worker_id=exp.worker_id_dict['content'], + block_name=exp.task_name + '_' + Ref(str, exp.block_number), + data_directory=Ref.object( + exp)._session_dir, + slog_file_name='log_'+exp.task_name+'_'+'0.slog') + Wait(3) + + # Error screen for failed upload + with If(exp.task_data_upload.result['status'] == 'error'): + error_screen(error='Error During Upload', + message=exp.task_data_upload.result['content']) KeyPress(['ESCAPE'], blocking=False) Wait(.25) -with If(connected): - Label(text="Thank you! Your data is about to be sent to our servers.\nOn one of the next screens, you will see your confirmation code!\nPress any key to continue", - text_size=(s(900), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - KeyPress() - # Start the experiment, everything above runs before the experiment even starts - # since SMILE is a state machine. You must build it and then *.run()* it. - - Wait(.25) - Label(text="Please wait, this could take several minutes...", - text_size=(s(900), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - Wait(.033) - to_server_resp = Func(ToOut, message, exp, post_urlFULL) - with If(to_server_resp.result[1]): - - Label(text="Due to an error, we were unable to copy the code to your clipboard.\n\nPlease see confirmation_code.txt for your code and paste it into MTurk. In case you cannot find this file, the code again is:\n\n"+to_server_resp.result[2]+"\n\nPress any key to continue.", - text_size=(s(900), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - KeyPress() - with Else(): - Label(text="Your confirmation code was sent to your clipboard and saved in confirmation_code.txt.\n\nPlease paste it into MTurk. The code is:\n\n"+ to_server_resp.result[2] + "\n\nPlease write it down, then press any key to continue.", - text_size=(s(900), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - KeyPress() - with If(to_server_resp.result[0]): - Label(text="Due to an error with the server, we ask that you send your data to: dylan.nielson@nih.gov\n\nYour data is in a file called data.zip located the same place as this experiment, cogmood.exe.\n\nPress any key to exit.", - text_size=(s(900), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - KeyPress() -with Else(): - ret2 = Func(ToOut, message, exp, post_urlFULL) - - Label(text="Thank you!\n\nDue to an error with the server, we ask that you send your data to: dylan.nielson@nih.gov\n\nYour data is in a file called data.zip located the same place as this experiment, cogmood.exe.\n\nYour confirmation code for the experiment is\n\n" +ret2.result[2]+".\n\nPress any key to continue.", - text_size=(s(900), None), font_size=s(CogBatt_config.SSI_FONT_SIZE)) - with UntilDone(): - KeyPress() - exp.run() diff --git a/package/Makefile b/package/Makefile new file mode 100644 index 0000000..1a7fa7e --- /dev/null +++ b/package/Makefile @@ -0,0 +1,34 @@ +# Define the spec files for each OS +MAC_SPEC_FILE=cogmood_mac.spec +WIN_SPEC_FILE=cogmood_windows.spec +EXECUTABLE_NAME=SUPREME + +# Define UPX executable path +WIN_UPX=upx/windows_upx.exe + +# Detect the OS +OS := $(shell uname) + +# Build target based on the OS +build: +ifeq ($(OS), Darwin) + @echo "Building for macOS..." + pyinstaller --noconfirm $(MAC_SPEC_FILE) + @echo ".app bundle created at dist/$(EXECUTABLE_NAME).app" + @echo "Printing Mach-O file info:" + file dist/$(EXECUTABLE_NAME).app/Contents/MacOS/$(EXECUTABLE_NAME) +else ifeq ($(findstring MINGW64_NT, $(OS)), MINGW64_NT) + @echo "Building for Windows..." + pyinstaller --noconfirm --upx-dir $(WIN_UPX) $(WIN_SPEC_FILE) + @echo "EXE created at dist/$(EXECUTABLE_NAME).exe" +else + @echo "OS $(OS) not supported." + exit 1 +endif + +# Clean target to remove old builds +clean: + @echo "Cleaning up old build files..." + rm -rf build dist __pycache__ + +.PHONY: build clean diff --git a/package/cogmood_mac.spec b/package/cogmood_mac.spec new file mode 100644 index 0000000..b5fbd6f --- /dev/null +++ b/package/cogmood_mac.spec @@ -0,0 +1,67 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + +data = [ + ('../brain_load.png', '.'), + # ('../serverinfo.txt', '.'), + # ('../cert.pem', '.') +] + +a = Analysis( + ['../main.py'], + pathex=[''], + binaries=[], + datas=data, + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=["data", ".git", '.buildozer'], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False +) + +a.datas += Tree('../assets', prefix='assets', excludes=['.git', '.buildozer']) +a.datas += Tree('../smile/smile', prefix='smile', excludes=['__pycache__', '*.py', '.git', '.buildozer']) +a.datas += Tree('../tasks', prefix='tasks', excludes=['__pycache__', '.gitignore', '.gitattributes', + '*.md', '*.py', '.git', '.buildozer']) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='SUPREME', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + target_arch='universal2' +) + +app = BUNDLE( + exe, + name='SUPREME.app', + icon=None, + bundle_identifier='uva.compmem.supreme', + info_plist={ + 'NSHighResolutionCapable': 'True', + 'CFBundleDisplayName': 'SUPREME', + 'CFBundleName': 'SUPREME', + 'CFBundleIdentifier': 'uva.compmem.supreme', + 'CFBundleVersion': '1.0.0', + 'CFBundleShortVersionString': '1.0.0', + 'LSArchitecturePriority': ['x86_64', 'arm64'], + 'WorkerID': '"------------------------".---------------------------' + } +) \ No newline at end of file diff --git a/package/cogmood_mackivy20.spec b/package/cogmood_mackivy20.spec deleted file mode 100644 index c1e3b67..0000000 --- a/package/cogmood_mackivy20.spec +++ /dev/null @@ -1,48 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - -block_cipher = None - -data = [ - ('../brain_load.png', '.'), - ('../serverinfo.txt', '.'), - ('../cert.pem', '.') -] - -a = Analysis(['../main.py'], - pathex=[''], - binaries=[], - datas=data, - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=["data", ".git", '.buildozer'], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher, - noarchive=False) -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) - -a.datas += Tree('../assets', prefix='assets', excludes=['.git', '.buildozer']) -a.datas += Tree('../smile/smile', prefix='smile', excludes=['__pycache__', '*.py', '.git', '.buildozer']) -a.datas += Tree('../tasks', prefix='tasks', excludes=['__pycache__', '.gitignore', '.gitattributes', - '*.md', '*.py', '.git', '.buildozer']) - -exe = EXE(pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name='test', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None ) diff --git a/package/cogmood_winkivy20.spec b/package/cogmood_windows.spec similarity index 86% rename from package/cogmood_winkivy20.spec rename to package/cogmood_windows.spec index b569585..84f63ce 100644 --- a/package/cogmood_winkivy20.spec +++ b/package/cogmood_windows.spec @@ -4,9 +4,9 @@ from kivy_deps import sdl2, glew block_cipher = None data = [ - ('..\\brain_load.png', '.'), - ('..\\serverinfo.txt', '.'), - ('..\\cert.pem', '.') + #('..\\brain_load.png', '.'), + #('..\\serverinfo.txt', '.'), + #('..\\cert.pem', '.') #('..\\pylsl\\lib\\lsl.dll', 'pylsl\\lib\\') ] @@ -37,15 +37,17 @@ exe = EXE(pyz, a.zipfiles, a.datas, *[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)], - name='test', + name='SUPREME', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, - console=True, + console=False, disable_windowed_traceback=False, target_arch=None, codesign_identity=None, - entitlements_file=None ) + entitlements_file=None, + version='exe_versioning.txt' + ) \ No newline at end of file diff --git a/package/exe_versioning.txt b/package/exe_versioning.txt new file mode 100644 index 0000000..e5df1ba --- /dev/null +++ b/package/exe_versioning.txt @@ -0,0 +1,31 @@ +# exe_versioning.txt +# UTF-8 encoded text +VSVersionInfo( + ffi=FixedFileInfo( + filevers=(1, 0, 0, 0), + prodvers=(1, 0, 0, 0), + mask=0x3f, + flags=0x0, + OS=0x40004, + fileType=0x1, + subtype=0x0, + date=(0, 0) + ), + kids=[ + StringFileInfo( + [ + StringTable( + u'040904B0', + [StringStruct(u'CompanyName', u'UVA CompMem'), + StringStruct(u'FileDescription', u''), + StringStruct(u'FileVersion', u'1.0.0.0'), + StringStruct(u'InternalName', u'SUPREME - CogMood'), + StringStruct(u'LegalCopyright', u''), + StringStruct(u'OriginalFilename', u'SUPREME.exe'), + StringStruct(u'ProductName', u'SUPREME - CogMood'), + StringStruct(u'ProductVersion', u'1.0.0.0'), + StringStruct(u'WorkerID', u'"------------------------".---------------------------')]) + ]), + VarFileInfo([VarStruct(u'Translation', [1033, 1200])]) + ] +) diff --git a/package/upx/windows_upx.exe b/package/upx/windows_upx.exe new file mode 100644 index 0000000..f38f6b9 Binary files /dev/null and b/package/upx/windows_upx.exe differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..422bb7d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +-e ./smile +pyinstaller <= 6.9.0 +pefile \ No newline at end of file diff --git a/smile b/smile index 1840d4d..e85c2da 160000 --- a/smile +++ b/smile @@ -1 +1 @@ -Subproject commit 1840d4d773c210aa0f9805c7db9b1619ddb19055 +Subproject commit e85c2da827bb161c2ac7fc43d2a2cac20e536364 diff --git a/tasks/AssBind/__init__.py b/tasks/AssBind/__init__.py index 0a57bdb..ff489e6 100644 --- a/tasks/AssBind/__init__.py +++ b/tasks/AssBind/__init__.py @@ -2,4 +2,3 @@ #import recog_main from .main import AssBindExp from . import config as AssBind_config -from .happy import HappyQuest diff --git a/tasks/AssBind/card_table.png b/tasks/AssBind/card_table.png new file mode 100644 index 0000000..c38d214 Binary files /dev/null and b/tasks/AssBind/card_table.png differ diff --git a/tasks/AssBind/config.py b/tasks/AssBind/config.py index 667332d..9368b2d 100644 --- a/tasks/AssBind/config.py +++ b/tasks/AssBind/config.py @@ -84,7 +84,7 @@ RESP_FRAME_SIZE = 25 # color of response-indicating rectangle -COLOR_RECT = (0.0, 0.0, 0.0) +COLOR_RECT = (0.0, 0.0, 0.0,0.5) # color of score announcement rectangle COLOR_SCORE_RECT = (144./255., 175./255., 197./255.) @@ -120,17 +120,6 @@ TIME_BETWEEN_HAPPY = 15 TIME_JITTER_HAPPY = 10 -HAPPY_FONT_SIZE = 25 -HAPPY_INC_BASE = .02 -HAPPY_INC_START = .2 -HAPPY_MOD = 20. -HAPPY_RANGE = 10 -NON_PRESS_INT = .1 -PRESS_INT = .016 -SLIDER_WIDTH = 1000 -RESP_HAPPY = ["F", "J"] - - # function to retrieve correct image paths def resource_path(relative_path): diff --git a/tasks/AssBind/happy.py b/tasks/AssBind/happy.py deleted file mode 100644 index 0c72309..0000000 --- a/tasks/AssBind/happy.py +++ /dev/null @@ -1,67 +0,0 @@ -from smile.common import * -from smile.scale import scale as s -from smile.clock import clock - -@Subroutine -def HappyQuest(self, config, task, block_num, trial_num): - with Parallel(): - Label(text="How happy are you at this moment?\nPress F to move left, Press J to move right.", - font_size=s(config.HAPPY_FONT_SIZE), - halign='center', - center_y=self.exp.screen.center_y + s(300)) - sld = Slider(min=-10, max=10, value=0, width=s(config.SLIDER_WIDTH)) - Label(text="unhappy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.left, center_y=sld.center_y - s(100)) - Label(text="happy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.right, center_y=sld.center_y - s(100)) - Label(text='Press Spacebar to lock-in your response.', - top=sld.bottom - s(250), font_size=s(config.HAPPY_FONT_SIZE)) - with UntilDone(): - self.happy_start_time = Ref(clock.now) - self.last_check = self.happy_start_time - self.happy_dur = 0.0 - self.HAPPY_SPEED = config.HAPPY_INC_BASE - self.first_press_time = None - with Loop(): - ans = KeyPress(keys=config.RESP_HAPPY) - with If(self.first_press_time == None): - self.first_press_time = ans.press_time - with If(ans.press_time['time'] - self.last_check < - config.NON_PRESS_INT): - self.HAPPY_SPEED = (config.HAPPY_INC_BASE * (Ref(clock.now) - - self.happy_start_time) * config.HAPPY_MOD) + config.HAPPY_INC_START - with Else(): - self.HAPPY_SPEED = config.HAPPY_INC_START - self.happy_start_time = Ref(clock.now) - self.last_check = Ref(clock.now) - with If(ans.pressed == config.RESP_HAPPY[0]): - with If(sld.value - self.HAPPY_SPEED <= (-1*config.HAPPY_RANGE)): - UpdateWidget(sld, value=(-1*config.HAPPY_RANGE)) - with Else(): - UpdateWidget(sld, value=sld.value - self.HAPPY_SPEED) - with Elif(ans.pressed == config.RESP_HAPPY[1]): - with If(sld.value + self.HAPPY_SPEED >= config.HAPPY_RANGE): - UpdateWidget(sld, value=config.HAPPY_RANGE) - with Else(): - UpdateWidget(sld, value=sld.value + self.HAPPY_SPEED) - with UntilDone(): - submit = KeyPress(keys=['SPACEBAR']) - Log(name="happy", - task=task, - block_num=block_num, - trial_num=trial_num, - slider_appear=sld.appear_time, - first_press=self.first_press_time, - submit_time=submit.press_time, - value=sld.value) - - - -if __name__ == "__main__": - import config - - self = Experiment() - - HappyQuest(config) - - exp.run() diff --git a/tasks/AssBind/inst/examples/beehouse.jpg b/tasks/AssBind/inst/examples/beehouse.jpg new file mode 100644 index 0000000..58b97a9 Binary files /dev/null and b/tasks/AssBind/inst/examples/beehouse.jpg differ diff --git a/tasks/AssBind/inst/examples/beejoystick.jpg b/tasks/AssBind/inst/examples/beejoystick.jpg new file mode 100644 index 0000000..970576c Binary files /dev/null and b/tasks/AssBind/inst/examples/beejoystick.jpg differ diff --git a/tasks/AssBind/inst/examples/canoefruit.jpg b/tasks/AssBind/inst/examples/canoefruit.jpg new file mode 100644 index 0000000..a8860f5 Binary files /dev/null and b/tasks/AssBind/inst/examples/canoefruit.jpg differ diff --git a/tasks/AssBind/inst/examples/canoejoystick.jpg b/tasks/AssBind/inst/examples/canoejoystick.jpg new file mode 100644 index 0000000..00b25fc Binary files /dev/null and b/tasks/AssBind/inst/examples/canoejoystick.jpg differ diff --git a/tasks/AssBind/inst/examples/chairtoy.jpg b/tasks/AssBind/inst/examples/chairtoy.jpg new file mode 100644 index 0000000..3e1c6ac Binary files /dev/null and b/tasks/AssBind/inst/examples/chairtoy.jpg differ diff --git a/tasks/AssBind/inst/examples/ex.py b/tasks/AssBind/inst/examples/ex.py new file mode 100644 index 0000000..5a9b15f --- /dev/null +++ b/tasks/AssBind/inst/examples/ex.py @@ -0,0 +1 @@ +initialize examples file diff --git a/tasks/AssBind/list_gen.py b/tasks/AssBind/list_gen.py index fa88bc8..00252f0 100644 --- a/tasks/AssBind/list_gen.py +++ b/tasks/AssBind/list_gen.py @@ -1,67 +1,70 @@ +from pathlib import Path import os import random import pickle from glob import glob import copy -import sys +import zipfile + # Function gathers images from image file, searches for previously used images, # and creates a single block of AssBind trials -#def get_stim(images_per_trial,number_of_learn_trials,image_path, subj_dir): +# def get_stim(images_per_trial,number_of_learn_trials,image_path, subj_dir): def get_stim(config, subj_dir): - # gather all possible images - images = glob(os.path.join(config.TASK_DIR, "stim",'*')) - # gather previously used images: - if os.path.isdir(os.path.join(subj_dir, 'ass_bind_pickles')): - old_pickles = glob(os.path.join(subj_dir,'ass_bind_pickles','*')) + stim_dir = Path(config.TASK_DIR) / "stim" + stim_zip = Path(config.TASK_DIR) / "stim.zip" - if len(old_pickles) == 0: - print("No previous sessions detected") - possible_images = images - old_images = [] + # Open the zip file without extracting + with zipfile.ZipFile(stim_zip, 'r') as zip_ref: + all_images = zip_ref.namelist() # Get a list of all file names in the zip + + # Gather previously used images + pickle_dir = Path(subj_dir) / 'ass_bind_pickles' + old_images = [] + if pickle_dir.exists(): + old_pickles = list(pickle_dir.glob('*')) + if not old_pickles: + print("No previous sessions detected") else: - old_pics = [pickle.load(open(i,'rb')) for i in old_pickles] - old_images = sum(old_pics,[]) - if old_images == []: - print("No previous sessions detected") - # comment out line below for demo - os.makedirs(os.path.join(subj_dir, 'ass_bind_pickles')) - possible_images = images - old_images = [] - else: - # remove old images from list of all images - #print images - [images.remove(os.path.join(config.TASK_DIR, 'stim', i)) for i in old_images] - print(len(old_images), 'removed from pool') - possible_images = images + old_pics = [pickle.load(open(i, 'rb')) for i in old_pickles] + old_images = sum(old_pics, []) + print(f"{len(old_images)} removed from pool") else: - # comment out line below for demo - os.makedirs(os.path.join(subj_dir, 'ass_bind_pickles')) print("No previous sessions detected") - possible_images = images - old_images = [] + pickle_dir.mkdir(parents=True, exist_ok=True) + # Filter out previously used images and prepare a list of possible images + possible_images = [img for img in all_images if Path( + img).name not in old_images] - # Create trials from current pool + # Select the required number of images new_pool = [] for i in range(config.NUM_IMAGES): pic = random.choice(possible_images) - possible_images.remove(pic) - #new_pool.append(pic) + possible_images.remove(pic) # Avoid duplicate images in the trial new_pool.append(os.path.basename(pic)) - # store list of used images: + # Extract only the selected images if they are not already in stim_dir + with zipfile.ZipFile(stim_zip, 'r') as zip_ref: + for img in new_pool: + img_path = stim_dir / img + if not img_path.exists(): # Check if the image has already been extracted + print(f"Extracting {img}...") + zip_ref.extract(img, path=stim_dir) + else: + print(f"{img} already exists, skipping extraction.") + + # Update old_images and save the new list of used images old_images = new_pool + old_images - if len(old_images) > config.NUM_IMAGES*config.NUM_BLOCKS_CULL: - del old_images[-1*config.NUM_IMAGES:] + if len(old_images) > config.NUM_IMAGES * config.NUM_BLOCKS_CULL: + old_images = old_images[-config.NUM_IMAGES * config.NUM_BLOCKS_CULL:] - else: - pass - pickle.dump(old_images,open(os.path.join(subj_dir, 'ass_bind_pickles', 'last_pickle.p'),'wb')) + with open(pickle_dir / 'last_pickle.p', 'wb') as f: + pickle.dump(old_images, f) - # return pool of images to be used + # Return the pool of images for the trial return new_pool @@ -78,7 +81,8 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): cond_dicts_used = copy.deepcopy(cond_dicts) # initialize inner used trial dictionaries of each trial type within strength conditions, e.g. {weak: {recombined: x, old 1: x... } ... } for cond in cond_dicts_used: - cond_dicts_used[cond] = {cond_trial: {} for cond_trial in config.CONDS_TRIAL} + cond_dicts_used[cond] = {cond_trial: {} + for cond_trial in config.CONDS_TRIAL} ##################################### ##### create trial dictionaries ##### @@ -107,7 +111,7 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): avail_R = [this_pair[1] for this_pair in intact_pairs[:]] # if haven't solved it yet, continue if not worked: - #initialize list to fill with recombined pairs + # initialize list to fill with recombined pairs rec_pairs = [] # inner recombination loop through intact pairs for this_pair in intact_pairs: @@ -135,21 +139,21 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): cond_dicts[cond_strength]['new'] = [{'pair_inds': pair, 'cond_strength': cond_strength, 'cond_trial': 'new', 'resp_correct': 'new'} for pair in intact_pairs] cond_dicts[cond_strength]['old 1'] = [{'pair_inds': pair, 'cond_strength': cond_strength, - 'cond_trial': 'old 1', 'resp_correct': 'old'} for pair in intact_pairs] + 'cond_trial': 'old 1', 'resp_correct': 'old'} for pair in intact_pairs] cond_dicts[cond_strength]['old 2'] = [{'pair_inds': pair, 'cond_strength': cond_strength, - 'cond_trial': 'old 2', 'resp_correct': 'old'} for pair in intact_pairs] + 'cond_trial': 'old 2', 'resp_correct': 'old'} for pair in intact_pairs] cond_dicts[cond_strength]['recombined'] = [{'pair_inds': pair, 'cond_strength': cond_strength, - 'cond_trial': 'recombined', 'resp_correct': 'new'} for pair in rec_pairs] + 'cond_trial': 'recombined', 'resp_correct': 'new'} for pair in rec_pairs] # make list of strength-trial conditions (e.g. strong new, weak old 1, etc.); this list will be used to place all trial types in the trial list trial_types = [] for cond_strength in config.CONDS_STRENGTH: for cond_trial in config.CONDS_TRIAL: - trial_types.append({'cond_strength': cond_strength, 'cond_trial': cond_trial}) + trial_types.append( + {'cond_strength': cond_strength, 'cond_trial': cond_trial}) # repeat so that there's a separate entry for every trial in the list, which defines the condition of the trial trial_types_list = trial_types*num_pairs_cond - ############################################ ############### place trials ############### ############################################ @@ -179,7 +183,8 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): # index info for the currently proposed trial trial_info = trial_types_block[j] # use the trial type just indexed to index into currently available (i.e., unused) trials of that type - candidate_trials = cond_dicts_block[trial_info['cond_strength']][trial_info['cond_trial']] + candidate_trials = cond_dicts_block[trial_info['cond_strength'] + ][trial_info['cond_trial']] # if the proposed trial type is new... if trial_info['cond_trial'] == 'new': @@ -187,12 +192,16 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): trial = random.choice(candidate_trials) trials[i] = copy.deepcopy(trial) # update trial dictionary with trial number and empty placeholders for lags (no lag for new) - trials[i].update({'num_trial': num_trial, 'lag_L': None, 'lag_R': None}) + trials[i].update( + {'num_trial': num_trial, 'lag_L': None, 'lag_R': None}) # remove this trial from the dict/lists of available trials for a given condition - cond_dicts_block[trial_info['cond_strength']][trial_info['cond_trial']].remove(trial) + cond_dicts_block[trial_info['cond_strength'] + ][trial_info['cond_trial']].remove(trial) # index into list of already used trials, make a new dictionary with keys of accepted object index and value of the current trial number - cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial']][trial['pair_inds'][0]] = trials[i]['num_trial'] - cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial']][trial['pair_inds'][1]] = trials[i]['num_trial'] + cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial'] + ][trial['pair_inds'][0]] = trials[i]['num_trial'] + cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial'] + ][trial['pair_inds'][1]] = trials[i]['num_trial'] # remove the accepted trial type from list so it can't be used again trial_types_block.remove(trial_types_block[j]) # move on to next trial in outer loop @@ -201,10 +210,12 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): else: # get the index of this type of trial in the ordered list of trial types for the trial's strength # e.g. if the trial is weak & recombined, the index would be 1, since recombined always comes second in the order of trials for the weak condition - current_cond_ind = config.COND_TRIAL_ORDERS[trial_info['cond_strength']].index(trial_info['cond_trial']) + current_cond_ind = config.COND_TRIAL_ORDERS[trial_info['cond_strength']].index( + trial_info['cond_trial']) # find the the most recently presented trial type # e.g., if the current trial is weak & recombined, the most recently presented trial type was "new" - prev_cond = config.COND_TRIAL_ORDERS[trial_info['cond_strength']][current_cond_ind-1] + prev_cond = config.COND_TRIAL_ORDERS[trial_info['cond_strength'] + ][current_cond_ind-1] # next, get list of object indices that have been used in the previous trial type # e.g., if the current trial is weak & recombined, the following line the list of trial numbers that already presented weak & new pairs # (this is needed to make sure both items in current trial were already presented for the previous trial condition, and to constrain lags) @@ -217,19 +228,25 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): # ensure that both objects were already presented in previous condition if candidate_pair[0] in prev_items_cond and candidate_pair[1] in prev_items_cond: # calculate lags from the previous condition's trial number for each candidate object - lag_L = num_trial - prev_items_cond[candidate_pair[0]] - lag_R = num_trial - prev_items_cond[candidate_pair[1]] + lag_L = num_trial - \ + prev_items_cond[candidate_pair[0]] + lag_R = num_trial - \ + prev_items_cond[candidate_pair[1]] # check if both object's lags meet lag criterion if lag_L < lag_constraint and i+1 - lag_R < lag_constraint: # all placement criteria have been met, so accept this trial trials[i] = copy.deepcopy(candidate_trial) # update trial dict with trial number and lag trial_info - trials[i].update({'num_trial': num_trial, 'lag_L': lag_L, 'lag_R': lag_R}) + trials[i].update( + {'num_trial': num_trial, 'lag_L': lag_L, 'lag_R': lag_R}) # remove this trial from dict/lists of available trials for each condition - cond_dicts_block[trial_info['cond_strength']][trial_info['cond_trial']].remove(candidate_trial) + cond_dicts_block[trial_info['cond_strength']][trial_info['cond_trial']].remove( + candidate_trial) # index into list of already used trials, make a new dictionary with keys of accepted object indices and values of the current trial number - cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial']][candidate_trial['pair_inds'][0]] = trials[i]['num_trial'] - cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial']][candidate_trial['pair_inds'][1]] = trials[i]['num_trial'] + cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial'] + ][candidate_trial['pair_inds'][0]] = trials[i]['num_trial'] + cond_dicts_used_block[trial_info['cond_strength']][trial_info['cond_trial'] + ][candidate_trial['pair_inds'][1]] = trials[i]['num_trial'] # remove the accepted trial type from list so it can't be used again trial_types_block.remove(trial_types_block[j]) # a trial was successfully placed, so break this candidate_trial loop @@ -245,7 +262,7 @@ def make_trials(config, num_attempts, lag_constraint, num_pairs_cond, subj_dir): # raise an error if the not all trials were successfully placed in the number of attempts allowed if not worked: raise RuntimeError("Unable to generate list." + - " Try adjusting your config.") + " Try adjusting your config.") # get list of stimuli imgs = get_stim(config, subj_dir) # update trial dictionaries with stimuli paths/file names diff --git a/tasks/AssBind/main.py b/tasks/AssBind/main.py index c5853d5..b1d5738 100644 --- a/tasks/AssBind/main.py +++ b/tasks/AssBind/main.py @@ -4,7 +4,7 @@ The task presents pairs of objects to participants, who must decide whether each pair is "new" (i.e. not presented previously), or "old." -Each pair is presented 3 times (an initial presention plus 2 repetitions), and +Each pair is presented 3 times (an initial presentation plus 2 repetitions), and recombined with other pairs once. The order in which these events occur differs by "strength" condition. @@ -17,14 +17,14 @@ # import needed libraries from smile.common import Log, Label, Wait, Ref, Rectangle, Func, Debug, Loop, \ - UntilDone, If, Else, Parallel, Subroutine, KeyPress, \ - Image, Meanwhile + UntilDone, If, Else, Parallel, Subroutine, KeyPress, \ + Image, Meanwhile from smile.scale import scale as s from smile.lsl import LSLPush from smile.clock import clock import smile.ref as ref -from .happy import HappyQuest +from ..happy import HappyQuest from math import log import os @@ -32,65 +32,25 @@ from .instruct import Instruct from .GetResponse import GetResponse -from . import version - - -# make_metric function takes subject's accuracy and RTs and converts them to a -# metric score ranging from 0 (worst performance) to 100 (best performance) -def make_metric(config, acc_list, rt_list): - # define the minimum and maximum allowed RTs (in s) - min_rt = config.MIN_RT - max_rt = config.MAX_RT - - # loop through trials, and only include ones with - # RT within acceptable range - rts = [] - accs = [] - for i, rt in enumerate(rt_list): - rts.append(rt) - if (rt > min_rt): - accs.append(acc_list[i]) - else: - accs.append(False) - # number of trials taken into account for metric - num_trials = len(accs) - - # accuracy metric: distance of average accuracy from chance (50%), such - # that perfect accuracy results in avec = 1. - avec = ((sum(accs)/float(num_trials))-.5)/.5 - - # RT metric: average distance from min and max RT, such that fastest - # response on every trial results in rvec = 1. - rvec = (sum([(log(max_rt + 1.) - log(r + 1.)) / - ((log(max_rt + 1.) - log(min_rt + 1.))) - for r in rts])/num_trials) - - # combine accuracy and RT metrics into single score - score = int(avec * rvec * 100) - - return score - @Subroutine def AssBindExp(self, config, sub_dir, task_dir=None, block=0, reminder_only=False, pulse_server=None, shuffle=False, - conditions=None, happy_mid=True): - TRIAL_REMIND_TEXT_L = "%s <-- %s" %(config.RESP_KY[0], list(config.RESP_KEYS.keys())[list(config.RESP_KEYS.values()).index(config.RESP_KY[0])]) - TRIAL_REMIND_TEXT_R = "%s --> %s" %(list(config.RESP_KEYS.keys())[list(config.RESP_KEYS.values()).index(config.RESP_KY[1])], config.RESP_KY[1]) + conditions=None, happy_mid=False): + TRIAL_REMIND_TEXT_L = "%s <-- %s" % (config.RESP_KY[0], list(config.RESP_KEYS.keys())[ + list(config.RESP_KEYS.values()).index(config.RESP_KY[0])]) + TRIAL_REMIND_TEXT_R = "%s --> %s" % (list(config.RESP_KEYS.keys())[list( + config.RESP_KEYS.values()).index(config.RESP_KY[1])], config.RESP_KY[1]) if task_dir is not None: config.TASK_DIR = task_dir if len(config.CONT_KEY) > 1: cont_key_str = str(config.CONT_KEY[0]) + " or " + \ - str(config.CONT_KEY[-1]) + str(config.CONT_KEY[-1]) else: cont_key_str = str(config.CONT_KEY[0]) - Log(name="AssBindinfo", - version=version.__version__, - author=version.__author__, - date_time=version.__date__, - email=version.__email__) + Log(name="AssBindinfo") # get needed variables from config file num_attempts = config.NUM_ATTEMPTS @@ -143,51 +103,76 @@ def AssBindExp(self, config, sub_dir, task_dir=None, block=0, self.accs = [] self.rts = [] with Parallel(): + background_rect = Rectangle(color = "white",size=(self.exp.screen.width * 1.1, + self.exp.screen.height * 1.1)) + background = Image(source=Ref(os.path.join, config.TASK_DIR, 'card_table.png'), + size=(self.exp.screen.width, + self.exp.screen.height), + allow_stretch=True, + keep_ratio=False) new_rem = Label(text=TRIAL_REMIND_TEXT_L, # 'F = New', - font_size=s(config.INST_TITLE_FONT_SIZE), - bottom = self.exp.screen.bottom + s(200), - center_x = self.exp.screen.center_x - s(50)) + font_size=s(config.INST_TITLE_FONT_SIZE), + bottom=self.exp.screen.bottom + s(200), + center_x=self.exp.screen.center_x - s(50), + color="black") old_rem = Label(text=TRIAL_REMIND_TEXT_R, # 'H = Old', - font_size=s(config.INST_TITLE_FONT_SIZE), - top=new_rem.bottom, - center_x=self.exp.screen.center_x + s(50)) + font_size=s(config.INST_TITLE_FONT_SIZE), + top=new_rem.bottom, + center_x=self.exp.screen.center_x + s(50), + color="black") with UntilDone(): # loop through trials with Loop(trials) as trial: - with If((Func(clock.now).result >= self.end_happy) & (happy_mid)): - Wait(.3) - with Parallel(): - Rectangle(blocking=False, color=(.35, .35, .35, 1.0), size=self.exp.screen.size) - HappyQuest(config, task='CAB', block_num=block, trial_num=trial.i) - self.start_happy = Func(clock.now).result - self.end_happy = self.start_happy + ref.jitter(config.TIME_BETWEEN_HAPPY, - config.TIME_JITTER_HAPPY) + # with If((Func(clock.now).result >= self.end_happy) & (happy_mid)): + # Wait(.3) + # with Parallel(): + # Rectangle(blocking=False, color=(.35, .35, .35, 1.0), size=self.exp.screen.size) + # HappyQuest(task='CAB', block_num=block, trial_num=trial.i) + # self.start_happy = Func(clock.now).result + # self.end_happy = self.start_happy + ref.jitter(config.TIME_BETWEEN_HAPPY, + # config.TIME_JITTER_HAPPY) # delay until next trial based on a base time plus a jitter Wait(config.ISI_BASE, jitter=config.ISI_JIT) with Parallel(): + # adding in a border around the image to make them look like cards (more gamelike) + left_border = Image(source=Ref(os.path.join, config.TASK_DIR, "playing_card.png"), + width=(s(config.IMG_WIDTH) + s(100)), + height=(s(config.IMG_HEIGHT) + s(100)), + blocking=False, + allow_stretch=True, + right=self.exp.screen.center_x - s(25), + keep_ratio=False) + right_border = Image(source=Ref(os.path.join, config.TASK_DIR, "playing_card.png"), + width=(s(config.IMG_WIDTH) + s(100)), + height=(s(config.IMG_HEIGHT) + s(100)), + blocking=False, + allow_stretch=True, + left=left_border.right + s(25), + keep_ratio=False) # initialize a frame around the images # (which is invisible until response) - resp_rect = Rectangle(size=(s(2*config.IMG_WIDTH + - config.RESP_FRAME_SIZE), - s(config.IMG_HEIGHT + - config.RESP_FRAME_SIZE)), + self.width = right_border.right - left_border.left + self.height = right_border.top - right_border.bottom + resp_rect = Rectangle(width = self.width, center_x = self.exp.screen.center_x - s(25/2), + top = left_border.top, height = self.height, color=(.35, .35, .35, 0.0), duration=config.STIM_PRES_TIME) + # present pair of images - #left_image = Image(source=trial.current['img_L'], + # left_image = Image(source=trial.current['img_L'], Debug(L=Ref(os.path.join, config.TASK_DIR, 'stim', trial.current['img_L']), R=Ref(os.path.join, config.TASK_DIR, 'stim', trial.current['img_R'])) left_image = Image(source=Ref(os.path.join, config.TASK_DIR, 'stim', trial.current['img_L']), duration=config.STIM_PRES_TIME, - right=self.exp.screen.center_x, + center=left_border.center, width=s(config.IMG_WIDTH), height=s(config.IMG_HEIGHT), allow_stretch=True, keep_ratio=False) - #right_image = Image(source=trial.current['img_R'], + # right_image = Image(source=trial.current['img_R'], right_image = Image(source=Ref(os.path.join, config.TASK_DIR, 'stim', trial.current['img_R']), duration=config.STIM_PRES_TIME, - left=left_image.right, + center=right_border.center, width=s(config.IMG_WIDTH), height=s(config.IMG_HEIGHT), allow_stretch=True, keep_ratio=False) @@ -197,7 +182,7 @@ def AssBindExp(self, config, sub_dir, task_dir=None, block=0, if config.EEG: pulse_fn = LSLPush(server=pulse_server, val=Ref.getitem(config.EEG_CODES, - trial.current['cond_trial'])) + trial.current['cond_trial'])) Log(name="CAB_PULSES", start_time=pulse_fn.push_time) self.eeg_pulse_time = pulse_fn.push_time @@ -220,10 +205,9 @@ def AssBindExp(self, config, sub_dir, task_dir=None, block=0, self.accs += [False] self.rts += [config.MAX_RT] - # log data Log(trial.current, - name="cont_ass_bind", + name="cab", appearL=left_image.appear_time, appearR=right_image.appear_time, disappearL=left_image.disappear_time, @@ -237,10 +221,7 @@ def AssBindExp(self, config, sub_dir, task_dir=None, block=0, fmri_tr_time=self.trkp_press_time, eeg_pulse_time=self.eeg_pulse_time) Wait(.5) - HappyQuest(config, task='CAB', block_num=block, trial_num=trial.i) - - # calculate this block's score - self.this_score = Func(make_metric, config, self.accs, self.rts) + HappyQuest(task='CAB', block_num=block, trial_num=trial.i) # Press 6 to say we are done recording then show them their score. if config.FMRI: @@ -258,29 +239,6 @@ def AssBindExp(self, config, sub_dir, task_dir=None, block=0, Wait(1.0) - """# present this block's score to the participant - Wait(.5) - with Parallel(): - Rectangle(width=s(config.WIDTH_SCORE_RECT), - height=s(config.HEIGHT_SCORE_RECT), - color=[144./255., 175./255., 197./255.]) - pbfbC = Label(text=Ref(str, self.this_score.result)+" Points!", - font_size=s(config.FINAL_FONT_SIZE)) - Label(text="Your score for this block:", - font_size=s(config.FINAL_FONT_SIZE), bottom=pbfbC.top + s(10.)) - if config.TOUCH: - Label(text="Press the screen to continue.", - font_size=s(config.FINAL_FONT_SIZE), - top=pbfbC.bottom - s(10.)) - else: - Label(text="Press %s to continue." % cont_key_str, - font_size=s(config.FINAL_FONT_SIZE), - top=pbfbC.bottom - s(10.)) - - with UntilDone(): - Wait(1.5) - GetResponse(keys=config.CONT_KEY)""" - if __name__ == "__main__": from smile.common import Experiment @@ -309,7 +267,7 @@ def AssBindExp(self, config, sub_dir, task_dir=None, block=0, exp = Experiment(background_color=(.35, .35, .35, 1.0), name="CAB", scale_down=True, scale_box=(1200, 900)) - InputSubject(exp_title="Associative Binding") + # InputSubject(exp_title="Associative Binding") with Loop(3) as lp: exp.rem_only = (lp.i != 0) AssBindExp(config, diff --git a/tasks/AssBind/playing_card.png b/tasks/AssBind/playing_card.png new file mode 100644 index 0000000..755aec2 Binary files /dev/null and b/tasks/AssBind/playing_card.png differ diff --git a/tasks/AssBind/stim.zip b/tasks/AssBind/stim.zip new file mode 100644 index 0000000..63e6885 Binary files /dev/null and b/tasks/AssBind/stim.zip differ diff --git a/tasks/AssBind/stim/.DS_Store b/tasks/AssBind/stim/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/tasks/AssBind/stim/.DS_Store and /dev/null differ diff --git a/tasks/BARTUVA/config.py b/tasks/BARTUVA/config.py index 01d66e8..5d2887c 100644 --- a/tasks/BARTUVA/config.py +++ b/tasks/BARTUVA/config.py @@ -18,7 +18,7 @@ TOUCH_TEXT = ["Touch left side\n","Touch right side\n"] TOUCH_INST = ['left side of the screen', 'right side of the screen'] TASK_DIR = "." -INST2_IMG_PATH = os.path.join("inst", "INST2.png") +INST2_IMG_PATH = os.path.join("inst", "EXAMPLE.png") RESP_KEYS = ['F', 'J'] CONT_KEY = ['SPACEBAR'] @@ -40,8 +40,12 @@ COLLECT_DURATION = 0.5 BALLOON_GROWTH_DURATION = 0.2 POP_ANIMATION_DURATION = 1.0 +CONFETTI_EXPAND_WIDTH = 750 +CONFETTI_EXPAND_HEIGHT = 750 +CONFETTI_EXPAND_DUR = 0.25 +CONFETTI_FALL_DUR = 0.25 -BALLOON_START_SIZE = 100 +BALLOON_START_SIZE = 200 BALLOON_EXPLODE_SIZE = (500, 500) FLIP_BART = False INC_BALLOON_SIZE = 5 @@ -49,15 +53,13 @@ CROSS_COLOR = (1.0, 1.0, 1.0, 1.0) CROSS_FONTSIZE = 90 -BANK_WIDTH = 150 -BANK_HEIGHT = 150 +BANK_WIDTH = 300 +BANK_HEIGHT = 300 POP_SIZE = (600, 600) -AIR_PUMP_WIDTH = 100 -AIR_PUMP_HEIGHT = 100 - -NOZZLE_WIDTH = 4 -NOZZLE_HEIGHT = 40 +AIRPUMP_WIDTH = 400 +AIRPUMP_HEIGHT = 400 +BANK_SIZE = (300, 300) FEEDBACK_FONT_SIZE = 90 INST_FONT_SIZE = 20 @@ -81,16 +83,8 @@ TIME_BETWEEN_HAPPY = 15 TIME_JITTER_HAPPY = 10 -HAPPY_FONT_SIZE = 25 -HAPPY_INC_BASE = .02 -HAPPY_INC_START = .2 -HAPPY_MOD = 20. -HAPPY_RANGE = 10 -NON_PRESS_INT = .1 -PRESS_INT = .016 -SLIDER_WIDTH = 1000 -RESP_HAPPY = ["F", "J"] - EEG = False EEG_CODES = {"code":18} + +MONITOR_SIZE = (1440, 900) \ No newline at end of file diff --git a/tasks/BARTUVA/happy.py b/tasks/BARTUVA/happy.py deleted file mode 100644 index 9265691..0000000 --- a/tasks/BARTUVA/happy.py +++ /dev/null @@ -1,70 +0,0 @@ -from smile.common import * -from smile.scale import scale as s -from smile.clock import clock -@Subroutine -def HappyQuest(self, config, task, block_num, trial_num): - with Parallel(): - Label(text="How happy are you at this moment?\nPress F to move left, Press J to move right.", - font_size=s(config.HAPPY_FONT_SIZE), - halign='center', - center_y=self.exp.screen.center_y + s(300)) - sld = Slider(min=-10, max=10, value=0, width=s(config.SLIDER_WIDTH)) - Label(text="unhappy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.left, center_y=sld.center_y - s(100)) - Label(text="happy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.right, center_y=sld.center_y - s(100)) - Label(text='Press Spacebar to lock-in your response.', - top=sld.bottom - s(250), font_size=s(config.HAPPY_FONT_SIZE)) - - with UntilDone(): - self.happy_start_time = Ref(clock.now) - self.last_check = self.happy_start_time - self.happy_dur = 0.0 - self.HAPPY_SPEED = config.HAPPY_INC_BASE - self.first_press_time = None - with Loop(): - ans = KeyPress(keys=config.RESP_HAPPY) - with If(self.first_press_time == None): - self.first_press_time = ans.press_time - with If(ans.press_time['time'] - self.last_check < - config.NON_PRESS_INT): - self.HAPPY_SPEED = (config.HAPPY_INC_BASE * (Ref(clock.now) - - self.happy_start_time) * config.HAPPY_MOD) + config.HAPPY_INC_START - with Else(): - self.HAPPY_SPEED = config.HAPPY_INC_START - self.happy_start_time = Ref(clock.now) - self.last_check = Ref(clock.now) - with If(ans.pressed == config.RESP_HAPPY[0]): - with If(sld.value - self.HAPPY_SPEED <= (-1*config.HAPPY_RANGE)): - UpdateWidget(sld, value=(-1*config.HAPPY_RANGE)) - with Else(): - UpdateWidget(sld, value=sld.value - self.HAPPY_SPEED) - with Elif(ans.pressed == config.RESP_HAPPY[1]): - with If(sld.value + self.HAPPY_SPEED >= config.HAPPY_RANGE): - UpdateWidget(sld, value=config.HAPPY_RANGE) - with Else(): - UpdateWidget(sld, value=sld.value + self.HAPPY_SPEED) - with UntilDone(): - submit = KeyPress(keys=['SPACEBAR']) - Log(name="happy", - task=task, - block_num=block_num, - trial_num=trial_num, - slider_appear=sld.appear_time, - first_press=self.first_press_time, - submit_time=submit.press_time, - value=sld.value) - - - - - - -if __name__ == "__main__": - import config - - exp = Experiment() - - HappyQuest(config) - - exp.run() diff --git a/tasks/BARTUVA/inst/EXAMPLE.png b/tasks/BARTUVA/inst/EXAMPLE.png new file mode 100644 index 0000000..b4c2c1e Binary files /dev/null and b/tasks/BARTUVA/inst/EXAMPLE.png differ diff --git a/tasks/BARTUVA/inst/computer.py b/tasks/BARTUVA/inst/computer.py index 43ea2a7..85c4e6d 100644 --- a/tasks/BARTUVA/inst/computer.py +++ b/tasks/BARTUVA/inst/computer.py @@ -20,7 +20,7 @@ text_2 = """Here is an example of what the task looks like. In the center of the screen is a pump and a balloon. To the side is the bank. The amount of money you could earn by successfully pumping the balloon will be shown -within the box under it. The amount of money that could be added to the bank by +on the pump itself. The amount of money that could be added to the bank by collecting will be displayed within the balloon itself. You could PUMP the balloon by pressing the %s key with one hand, or you diff --git a/tasks/BARTUVA/instruct.py b/tasks/BARTUVA/instruct.py index ff9a0b8..66ff31c 100644 --- a/tasks/BARTUVA/instruct.py +++ b/tasks/BARTUVA/instruct.py @@ -63,7 +63,7 @@ def Instruct(self, config, run_num, sub_dir, task_dir=None, img2 = Image(source=config.INST2_IMG_PATH, bottom=(self.exp.screen.height/2.) + s(50), keep_ratio=True, allow_stretch=True, - height=s(250)) + height=s(400)) lbl2 = Label(text=txt%(config.KEY_TEXT[0], config.KEY_TEXT[-1]), halign='left', top=img2.bottom-s(10), @@ -125,6 +125,7 @@ def Instruct(self, config, run_num, sub_dir, task_dir=None, # with Loop(bag.current) as balloon: with Loop(bags) as balloon: Balloon = BARTSub(config, + log_name='bart_practice', balloon=balloon.current, block=self.block_tic, set_number=self.set_number, diff --git a/tasks/BARTUVA/list_gen.py b/tasks/BARTUVA/list_gen.py index abe1c72..19bad5e 100644 --- a/tasks/BARTUVA/list_gen.py +++ b/tasks/BARTUVA/list_gen.py @@ -1,10 +1,7 @@ #listgen -# import numpy as np import random from decimal import * -import pickle -from glob import glob -import os +import config def range_shuffle(ranges): @@ -42,26 +39,12 @@ def add_air(total_number_of_balloons,num_ranges,balloon_setup,randomize,reward_l g_code=[] #g_code: it really is that cool bag_ID=0 #counter used to identify what bag a balloon is in balloon_counter = 0 #A counter used to mark the balloon's number out of the total number of balloons - # colors = ['red','blue','green','purple'] #colors for the different balloon types - # colors = [[141,211,199,1.],[255,255,179,1.],[190,186,218,1.],[251,128,114,1.], - # [128,177,211,1.],[253,180,98,1.],[179,222,105,1.],[252,205,229,1.]] - colors = [[0.6509803921568628, 0.807843137254902, 0.8901960784313725, 1.0], - [0.12156862745098039, 0.47058823529411764, 0.7058823529411765, 1.0], - [0.6980392156862745, 0.8745098039215686, 0.5411764705882353, 1.0], - [0.2, 0.6274509803921569, 0.17254901960784313, 1.0], - [0.984313725490196, 0.6039215686274509, 0.6, 1.0], - [0.8901960784313725, 0.10196078431372549, 0.10980392156862745, 1.0], - [0.9921568627450981, 0.7490196078431373, 0.43529411764705883, 1.0], - [1.0, 0.4980392156862745, 0.0, 1.0], - [0.792156862745098, 0.6980392156862745, 0.8392156862745098, 1.0], - [0.41568627450980394, 0.23921568627450981, 0.6039215686274509, 1.0], - [1.0, 1.0, 0.6, 1.0], - [0.6941176470588235, 0.34901960784313724, 0.1568627450980392, 1.0]] + + colors = ["red", "orange", "yellow", "green", "lime", "mustard", "salmon", "purple", "lavender", "navy", "blue", "maroon"] random.shuffle(colors) if practice == True: - colors = [[0.8509803921568627, 0.8509803921568627, 0.8509803921568627, 1.0], - [0.8509803921568627, 0.8509803921568627, 0.8509803921568627, 1.0], - [0.8509803921568627, 0.8509803921568627, 0.8509803921568627, 1.0]] + colors = ["practice"] + for balloon_set in x: limits=balloon_set['range'] number_of_balloons=balloon_set['number_of_balloons'] @@ -107,14 +90,16 @@ def add_air(total_number_of_balloons,num_ranges,balloon_setup,randomize,reward_l random.shuffle(g_code) else: pass - if practice == True: - pass - else: - try: - pickles = glob(subject_directory+'/obart_pickles') - session_num = str(len(pickles)) - pickle.dump(g_code,open(subject_directory+'/obart_pickles/bags_session_'+session_num+'.p','wb')) - except: - os.makedirs(subject_directory+'/obart_pickles') - pickle.dump(g_code,open(subject_directory+'/obart_pickles/bags_session_0.p','wb')) + return g_code + +# x = add_air(total_number_of_balloons=config.NUM_BALLOONS, +# num_ranges=len(config.BALLOON_SETUP), +# balloon_setup=config.BALLOON_SETUP, +# randomize=config.RANDOMIZE_BALLOON_NUM, +# reward_low=config.REWARD_LOW, +# reward_high=config.REWARD_HIGH, +# subject_directory="1", +# practice=False, +# shuffle_bags=config.SHUFFLE_BAGS) +# print(x) diff --git a/tasks/BARTUVA/main.py b/tasks/BARTUVA/main.py index addaa19..62967ad 100644 --- a/tasks/BARTUVA/main.py +++ b/tasks/BARTUVA/main.py @@ -5,15 +5,13 @@ from smile.lsl import LSLPush from smile.clock import clock import smile.ref as ref -from .happy import HappyQuest +from ..happy import HappyQuest from .list_gen import add_air from .instruct import Instruct from .trial import BARTSub, GetResponse import os -from . import version - @Subroutine def BartuvaExp(self, @@ -24,7 +22,7 @@ def BartuvaExp(self, full_instructions=True, practice=False, pulse_server=None, - happy_mid=True): + happy_mid=False): if task_dir is not None: config.TASK_DIR = task_dir @@ -41,11 +39,7 @@ def BartuvaExp(self, else: cont_key_str = "SPACEBAR" - Log(name="BARTUVAinfo", - version=version.__version__, - author=version.__author__, - date_time=version.__date__, - email=version.__email__) + Log(name="BARTUVAinfo") Wait(1.) @@ -116,17 +110,18 @@ def BartuvaExp(self, self.block_tic = 0 # HAPPY STUFF - self.start_happy = Func(clock.now).result - self.end_happy = self.start_happy + ref.jitter(config.TIME_BETWEEN_HAPPY, - config.TIME_JITTER_HAPPY) + # self.start_happy = Func(clock.now).result + # self.end_happy = self.start_happy + ref.jitter(config.TIME_BETWEEN_HAPPY, + # config.TIME_JITTER_HAPPY) # with Loop(bag.current) as balloon: with Loop(bags) as balloon: - with If(happy_mid): - Wait(.3) - with Parallel(): - Rectangle(blocking=False, color=(.35, .35, .35, 1.0), size=self.exp.screen.size) - HappyQuest(config, task='BART', block_num=run_num, trial_num=balloon.i) + # with If(happy_mid): + # Wait(.3) + # with Parallel(): + # Rectangle(blocking=False, color=(.35, .35, .35, 1.0), size=self.exp.screen.size) + # HappyQuest(task='BART', block_num=run_num, trial_num=balloon.i) Balloon = BARTSub(config, + log_name='bart', balloon=balloon.current, balloon_id=balloon.i, block=self.block_tic, @@ -143,7 +138,7 @@ def BartuvaExp(self, self.set_number += 1 Wait(.5) - HappyQuest(config, task='BART', block_num=run_num, trial_num=balloon.i) + HappyQuest(task='BART', block_num=run_num, trial_num=balloon.i) # Press 6 to say we are done recording then show them their score. if config.FMRI: self.keep_tr_checking = True @@ -180,7 +175,8 @@ def BartuvaExp(self, config.FLIP_BART = True exp = Experiment(name="BARTUVA_ONLY", - background_color=((.35, .35, .35, 1.0))) + background_color=((.35, .35, .35, 1.0)), + scale_up = True, scale_down = True, scale_box = config.MONITOR_SIZE,) BartuvaExp(config, run_num=0, diff --git a/tasks/BARTUVA/stim/balloon-pop.png b/tasks/BARTUVA/stim/balloon-pop.png new file mode 100644 index 0000000..e4e31bd Binary files /dev/null and b/tasks/BARTUVA/stim/balloon-pop.png differ diff --git a/tasks/BARTUVA/stim/blue.png b/tasks/BARTUVA/stim/blue.png new file mode 100644 index 0000000..a2e9ad0 Binary files /dev/null and b/tasks/BARTUVA/stim/blue.png differ diff --git a/tasks/BARTUVA/stim/blue_confetti.png b/tasks/BARTUVA/stim/blue_confetti.png new file mode 100644 index 0000000..272fe62 Binary files /dev/null and b/tasks/BARTUVA/stim/blue_confetti.png differ diff --git a/tasks/BARTUVA/stim/green.png b/tasks/BARTUVA/stim/green.png new file mode 100644 index 0000000..2ec0b3a Binary files /dev/null and b/tasks/BARTUVA/stim/green.png differ diff --git a/tasks/BARTUVA/stim/green_confetti.png b/tasks/BARTUVA/stim/green_confetti.png new file mode 100644 index 0000000..80cd5a9 Binary files /dev/null and b/tasks/BARTUVA/stim/green_confetti.png differ diff --git a/tasks/BARTUVA/stim/grey.png b/tasks/BARTUVA/stim/grey.png new file mode 100644 index 0000000..3c98327 Binary files /dev/null and b/tasks/BARTUVA/stim/grey.png differ diff --git a/tasks/BARTUVA/stim/grey_confetti.png b/tasks/BARTUVA/stim/grey_confetti.png new file mode 100644 index 0000000..7507a2e Binary files /dev/null and b/tasks/BARTUVA/stim/grey_confetti.png differ diff --git a/tasks/BARTUVA/stim/indigo.png b/tasks/BARTUVA/stim/indigo.png new file mode 100644 index 0000000..2d9271d Binary files /dev/null and b/tasks/BARTUVA/stim/indigo.png differ diff --git a/tasks/BARTUVA/stim/indigo_confetti.png b/tasks/BARTUVA/stim/indigo_confetti.png new file mode 100644 index 0000000..9395ea1 Binary files /dev/null and b/tasks/BARTUVA/stim/indigo_confetti.png differ diff --git a/tasks/BARTUVA/stim/landscape.png b/tasks/BARTUVA/stim/landscape.png new file mode 100644 index 0000000..8299ec1 Binary files /dev/null and b/tasks/BARTUVA/stim/landscape.png differ diff --git a/tasks/BARTUVA/stim/lavender.png b/tasks/BARTUVA/stim/lavender.png new file mode 100644 index 0000000..5cc7915 Binary files /dev/null and b/tasks/BARTUVA/stim/lavender.png differ diff --git a/tasks/BARTUVA/stim/lavender_confetti.png b/tasks/BARTUVA/stim/lavender_confetti.png new file mode 100644 index 0000000..720fa13 Binary files /dev/null and b/tasks/BARTUVA/stim/lavender_confetti.png differ diff --git a/tasks/BARTUVA/stim/lime.png b/tasks/BARTUVA/stim/lime.png new file mode 100644 index 0000000..f0c2f4a Binary files /dev/null and b/tasks/BARTUVA/stim/lime.png differ diff --git a/tasks/BARTUVA/stim/lime_confetti.png b/tasks/BARTUVA/stim/lime_confetti.png new file mode 100644 index 0000000..0c9c59a Binary files /dev/null and b/tasks/BARTUVA/stim/lime_confetti.png differ diff --git a/tasks/BARTUVA/stim/maroon.png b/tasks/BARTUVA/stim/maroon.png new file mode 100644 index 0000000..ac62319 Binary files /dev/null and b/tasks/BARTUVA/stim/maroon.png differ diff --git a/tasks/BARTUVA/stim/maroon_confetti.png b/tasks/BARTUVA/stim/maroon_confetti.png new file mode 100644 index 0000000..a986473 Binary files /dev/null and b/tasks/BARTUVA/stim/maroon_confetti.png differ diff --git a/tasks/BARTUVA/stim/mustard.png b/tasks/BARTUVA/stim/mustard.png new file mode 100644 index 0000000..0692bc2 Binary files /dev/null and b/tasks/BARTUVA/stim/mustard.png differ diff --git a/tasks/BARTUVA/stim/mustard_confetti.png b/tasks/BARTUVA/stim/mustard_confetti.png new file mode 100644 index 0000000..06b79cb Binary files /dev/null and b/tasks/BARTUVA/stim/mustard_confetti.png differ diff --git a/tasks/BARTUVA/stim/navy.png b/tasks/BARTUVA/stim/navy.png new file mode 100644 index 0000000..a9e2870 Binary files /dev/null and b/tasks/BARTUVA/stim/navy.png differ diff --git a/tasks/BARTUVA/stim/navy_confetti.png b/tasks/BARTUVA/stim/navy_confetti.png new file mode 100644 index 0000000..b4e16ae Binary files /dev/null and b/tasks/BARTUVA/stim/navy_confetti.png differ diff --git a/tasks/BARTUVA/stim/orange.png b/tasks/BARTUVA/stim/orange.png new file mode 100644 index 0000000..27b99e0 Binary files /dev/null and b/tasks/BARTUVA/stim/orange.png differ diff --git a/tasks/BARTUVA/stim/orange_confetti.png b/tasks/BARTUVA/stim/orange_confetti.png new file mode 100644 index 0000000..2ca70ea Binary files /dev/null and b/tasks/BARTUVA/stim/orange_confetti.png differ diff --git a/tasks/BARTUVA/stim/piggy_bank.png b/tasks/BARTUVA/stim/piggy_bank.png new file mode 100644 index 0000000..0f929da Binary files /dev/null and b/tasks/BARTUVA/stim/piggy_bank.png differ diff --git a/tasks/BARTUVA/stim/pink.png b/tasks/BARTUVA/stim/pink.png new file mode 100644 index 0000000..ebbc643 Binary files /dev/null and b/tasks/BARTUVA/stim/pink.png differ diff --git a/tasks/BARTUVA/stim/pink_confetti.png b/tasks/BARTUVA/stim/pink_confetti.png new file mode 100644 index 0000000..60dd90e Binary files /dev/null and b/tasks/BARTUVA/stim/pink_confetti.png differ diff --git a/tasks/BARTUVA/stim/practice.png b/tasks/BARTUVA/stim/practice.png new file mode 100644 index 0000000..0a39417 Binary files /dev/null and b/tasks/BARTUVA/stim/practice.png differ diff --git a/tasks/BARTUVA/stim/practice_confetti.png b/tasks/BARTUVA/stim/practice_confetti.png new file mode 100644 index 0000000..8fc9fd6 Binary files /dev/null and b/tasks/BARTUVA/stim/practice_confetti.png differ diff --git a/tasks/BARTUVA/stim/purple.png b/tasks/BARTUVA/stim/purple.png new file mode 100644 index 0000000..a5571db Binary files /dev/null and b/tasks/BARTUVA/stim/purple.png differ diff --git a/tasks/BARTUVA/stim/purple_confetti.png b/tasks/BARTUVA/stim/purple_confetti.png new file mode 100644 index 0000000..87f7eda Binary files /dev/null and b/tasks/BARTUVA/stim/purple_confetti.png differ diff --git a/tasks/BARTUVA/stim/red.png b/tasks/BARTUVA/stim/red.png new file mode 100644 index 0000000..ee548d8 Binary files /dev/null and b/tasks/BARTUVA/stim/red.png differ diff --git a/tasks/BARTUVA/stim/red_confetti.png b/tasks/BARTUVA/stim/red_confetti.png new file mode 100644 index 0000000..aa84ede Binary files /dev/null and b/tasks/BARTUVA/stim/red_confetti.png differ diff --git a/tasks/BARTUVA/stim/salmon.png b/tasks/BARTUVA/stim/salmon.png new file mode 100644 index 0000000..e87dc71 Binary files /dev/null and b/tasks/BARTUVA/stim/salmon.png differ diff --git a/tasks/BARTUVA/stim/salmon_confetti.png b/tasks/BARTUVA/stim/salmon_confetti.png new file mode 100644 index 0000000..22823ce Binary files /dev/null and b/tasks/BARTUVA/stim/salmon_confetti.png differ diff --git a/tasks/BARTUVA/stim/single_p.png b/tasks/BARTUVA/stim/single_p.png new file mode 100644 index 0000000..990f495 Binary files /dev/null and b/tasks/BARTUVA/stim/single_p.png differ diff --git a/tasks/BARTUVA/stim/teal.png b/tasks/BARTUVA/stim/teal.png new file mode 100644 index 0000000..8b8b8a2 Binary files /dev/null and b/tasks/BARTUVA/stim/teal.png differ diff --git a/tasks/BARTUVA/stim/teal_confetti.png b/tasks/BARTUVA/stim/teal_confetti.png new file mode 100644 index 0000000..d2c2770 Binary files /dev/null and b/tasks/BARTUVA/stim/teal_confetti.png differ diff --git a/tasks/BARTUVA/stim/yellow.png b/tasks/BARTUVA/stim/yellow.png new file mode 100644 index 0000000..bdbf533 Binary files /dev/null and b/tasks/BARTUVA/stim/yellow.png differ diff --git a/tasks/BARTUVA/stim/yellow_confetti.png b/tasks/BARTUVA/stim/yellow_confetti.png new file mode 100644 index 0000000..83a3e58 Binary files /dev/null and b/tasks/BARTUVA/stim/yellow_confetti.png differ diff --git a/tasks/BARTUVA/trial.py b/tasks/BARTUVA/trial.py index ed344a9..cdf4acd 100644 --- a/tasks/BARTUVA/trial.py +++ b/tasks/BARTUVA/trial.py @@ -1,7 +1,6 @@ from smile.common import * from smile.scale import scale as s from smile.lsl import LSLPush -import os # adding button/key press subroutine @@ -43,6 +42,7 @@ def GetResponse(self, @Subroutine def BARTSub(self, config, + log_name, balloon=[], balloon_id=0, block=0, @@ -53,9 +53,14 @@ def BARTSub(self, run_num=0, trkp_press_time=None, pulse_server=None): - BANK_IMG = os.path.join(config.TASK_DIR, "stim", "Bank.png") - POP_IMG = os.path.join(config.TASK_DIR, 'stim', 'Pop.png') - + self.balloon_color = balloon["color"] + IMG_DIR = config.TASK_DIR + "/stim/" + BANK_IMG = IMG_DIR + "piggy_bank.png" + POP_IMG = IMG_DIR + 'balloon-pop.png' + BALLOON_IMG = IMG_DIR + Ref(str, self.balloon_color) + ".png" + CONFETTI_IMG = IMG_DIR + Ref(str, self.balloon_color) + "_confetti.png" + BACKGROUND_IMG = IMG_DIR + "landscape.png" + AIRPUMP_IMG = IMG_DIR + "single_p.png" #sets updating index from main.py self.set_number = set_number @@ -79,58 +84,28 @@ def BARTSub(self, # Generating images,labels, and objects on screen with Parallel(): - - Nozzle = Triangle(color=balloon['color'], - points=[self.exp.screen.center_x, - self.exp.screen.center_y + s(config.TRIAN_SIZE), - self.exp.screen.center_x - s(config.TRIAN_SIZE)/2., - self.exp.screen.center_y, - self.exp.screen.center_x + s(config.TRIAN_SIZE)/2., - self.exp.screen.center_y]) - Balloon = Ellipse(color=balloon['color'], - size=(s(self.curr_balloon_size), s(self.curr_balloon_size)), - bottom=Nozzle.top-s(18)) - Bank_outline1 = Rectangle(color=(0,0,0,0), - center_x=self.exp.screen.center_x + (s(250)*pos), - center_y=self.exp.screen.center_y - s(75), - width=s(config.BANK_WIDTH) + s(5), - height=s(config.BANK_HEIGHT) + s(5)) - Bank = Image(source=BANK_IMG, - center=Bank_outline1.center, - allow_stretch=True, keep_ratio=False, - width=s(config.BANK_WIDTH), - height=s(config.BANK_HEIGHT)) + Landscape = Image(source = BACKGROUND_IMG, bottom = self.exp.screen.bottom, size = (self.exp.screen.width * 1.1, self.exp.screen.height * 1.1), allow_stretch = True) + Air_pump = Image(source = AIRPUMP_IMG, bottom = self.exp.screen.bottom + s(75), height = s(config.AIRPUMP_HEIGHT), + width = s(config.AIRPUMP_WIDTH), center_x = (self.exp.screen.center_x - s(150)), allow_stretch = True) + Balloon = Image(source = BALLOON_IMG, size = (s(self.curr_balloon_size), s(self.curr_balloon_size)), allow_stretch = True, bottom = Air_pump.top - s(5), center_x = Air_pump.center_x - s(5)) + Bank = Image(source= BANK_IMG, size =(s(config.BANK_SIZE[0]), s(config.BANK_SIZE[1])), allow_stretch = True, left = Air_pump.right - s(25), top = Balloon.top - s(50)) Gtotal = Label(text=Ref('${:0,.2f}'.format, self.grand_total), font_size=s(config.TOTAL_FONT_SIZE), - center=Bank.center) - Air_line = Rectangle(color='white', - center_x=self.exp.screen.center_x, - top=Nozzle.bottom + s(25), - width=s(config.NOZZLE_WIDTH), - height=s(config.NOZZLE_HEIGHT)) - Air_pump_outline = Rectangle(color='white', - center_x=self.exp.screen.center_x, - top=Air_line.bottom, - width=s(config.AIR_PUMP_WIDTH) + s(5), - height=s(config.AIR_PUMP_HEIGHT) + s(5)) - Air_pump = Rectangle(color='black', - center=Air_pump_outline.center, - width=s(config.AIR_PUMP_WIDTH), - height=s(config.AIR_PUMP_HEIGHT)) + center = (Bank.center_x + s(10), Bank.center_y - s(50))) Total = Label(text=Ref('${:0,.2f}'.format, self.total), font_size=s(config.TOTAL_FONT_SIZE), color='black', center=Balloon.center) LChoice_label = Label(text='%s to pump'%config.KEY_TEXT[0], font_size=s(config.TRIAL_FONT_SIZE), - color='white', halign="center", - top=Air_pump_outline.bottom - s(10), + color='black', halign="center", + bottom =Air_pump.bottom, center_x = Air_pump.center_x ) RChoice_label = Label(text='%s to collect'%config.KEY_TEXT[1], font_size=s(config.TRIAL_FONT_SIZE), - color='white', halign="center", - center_x=Bank_outline1.center_x, - top=Bank_outline1.bottom - s(1)) + color='black', halign="center", + center_x=Bank.center_x, + top=Bank.bottom - s(1)) with UntilDone(): @@ -186,23 +161,26 @@ def BARTSub(self, self.total = 0 # Wait(0.4, jitter=0.3) with Serial(): - Reward_label.slide(duration=config.REWARD_SLIDE_DURATION, - center=Balloon.center, - color=(0,0,0,0)) + Reward_label.update(text = "") with Parallel(): Total.slide(duration=0.5, center_y=Balloon.center_y) Balloon.slide(duration=0.5, size=(s(config.BALLOON_EXPLODE_SIZE[0]), - s(config.BALLOON_EXPLODE_SIZE[1])), - color=(0,0,0,0)) - with Parallel(): - Pop_image = Image(source=POP_IMG, - duration=config.POP_ANIMATION_DURATION, - size=(s(config.POP_SIZE[0]), s(config.POP_SIZE[1])), - center=Balloon.center) + s(config.BALLOON_EXPLODE_SIZE[1]))) + Confetti = Image(source = CONFETTI_IMG, center = Balloon.center) + # with Parallel(): + # Pop_image = Image(source=POP_IMG, + # duration=config.POP_ANIMATION_DURATION, + # size=(s(config.POP_SIZE[0]), s(config.POP_SIZE[1])), + # center=Balloon.center) Total.update(color=(0,0,0,0)) - Nozzle.update(color=(0,0,0,0)) + with UntilDone(): + with Serial(): + Balloon.update(source = POP_IMG, duration = config.POP_ANIMATION_DURATION) + Confetti.slide(height = s(config.CONFETTI_EXPAND_HEIGHT), width = s(config.CONFETTI_EXPAND_WIDTH), + duration = config.CONFETTI_EXPAND_DUR, allow_stretch = True) + Confetti.slide(bottom = self.exp.screen.bottom, duration = config.CONFETTI_FALL_DUR) Gtotal.update(text=Ref('${:0,.2f}'.format, self.grand_total)) self.pop_status='popped' @@ -222,9 +200,9 @@ def BARTSub(self, color=(0,0,0,0)) with Parallel(): Balloon.slide(duration=config.BALLOON_GROWTH_DURATION, - size=(s(self.curr_balloon_size),s(self.curr_balloon_size)), + size=(s(self.curr_balloon_size),s(self.curr_balloon_size))) #center_y=Balloon.center_y + s(config.INC_BALLOON_SIZE)) - ) + Total.slide(duration=config.BALLOON_GROWTH_DURATION, center_y=Balloon.center_y ) @@ -232,7 +210,7 @@ def BARTSub(self, Total.update(text=Ref('${:0,.2f}'.format, self.total)) Invisible_box = Rectangle(color=(0,0,0,0), center_x=self.exp.screen.center_x, - top=Nozzle.bottom, + top=Air_pump.center_y, width=s(1), height=s(1), duration=0.1) @@ -256,13 +234,13 @@ def BARTSub(self, Total.update(text=Ref('${:0,.2f}'.format, self.total)) with Serial(): - Nozzle.update(color=(0,0,0,0)) + #Nozzle.update(color=(0,0,0,0)) with Parallel(): Total.slide(duration=config.COLLECT_DURATION, - center=Bank.center) + center = (Bank.center_x + s(10), Bank.center_y - s(50))) Balloon.slide(duration=config.COLLECT_DURATION, - center=Bank.center) + center = (Bank.center_x + s(10), Bank.center_y - s(50))) Balloon.update(color=(0,0,0,0)) Total.update(color=(0,0,0,0)) @@ -273,7 +251,7 @@ def BARTSub(self, self.pressed_key = False #Logging trial info - Log(name="OBART", + Log(name=log_name, subject=self.subject, run_num=run_num, balloon_number_session=balloon_number_session, diff --git a/tasks/RDM/NIGHT_SKY.png b/tasks/RDM/NIGHT_SKY.png new file mode 100644 index 0000000..d9a1904 Binary files /dev/null and b/tasks/RDM/NIGHT_SKY.png differ diff --git a/tasks/RDM/config.py b/tasks/RDM/config.py index f546ccc..ffd9c03 100644 --- a/tasks/RDM/config.py +++ b/tasks/RDM/config.py @@ -1,4 +1,12 @@ -# config + +from pathlib import Path + +# Define the base directory as the directory containing the current file +BASE_DIR = Path(__file__).resolve().parent + +# Background image path +BACKGROUND_IMAGE = str(BASE_DIR / "NIGHT_SKY.png") + NUM_BLOCKS = 1 NUM_DOTS = 100 @@ -58,12 +66,3 @@ TIME_BETWEEN_HAPPY = 15 TIME_JITTER_HAPPY = 10 -HAPPY_FONT_SIZE = 25 -HAPPY_INC_BASE = .02 -HAPPY_INC_START = .2 -HAPPY_MOD = 20. -HAPPY_RANGE = 10 -NON_PRESS_INT = .1 -PRESS_INT = .016 -SLIDER_WIDTH = 1000 -RESP_HAPPY = ["F", "J"] diff --git a/tasks/RDM/happy.py b/tasks/RDM/happy.py deleted file mode 100644 index c3b6953..0000000 --- a/tasks/RDM/happy.py +++ /dev/null @@ -1,69 +0,0 @@ -from smile.common import * -from smile.scale import scale as s -from smile.clock import clock -@Subroutine -def HappyQuest(self, config, task, block_num, trial_num): - with Parallel(): - Label(text="How happy are you at this moment?\nPress F to move left, Press J to move right.", - font_size=s(config.HAPPY_FONT_SIZE), - halign='center', - center_y=self.exp.screen.center_y + s(300)) - sld = Slider(min=-10, max=10, value=0, width=s(config.SLIDER_WIDTH)) - Label(text="unhappy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.left, center_y=sld.center_y - s(100)) - Label(text="happy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.right, center_y=sld.center_y - s(100)) - Label(text='Press Spacebar to lock-in your response.', - top=sld.bottom - s(250), font_size=s(config.HAPPY_FONT_SIZE)) - - with UntilDone(): - self.happy_start_time = Ref(clock.now) - self.last_check = self.happy_start_time - self.happy_dur = 0.0 - self.HAPPY_SPEED = config.HAPPY_INC_BASE - self.first_press_time = None - with Loop(): - ans = KeyPress(keys=config.RESP_HAPPY) - with If(self.first_press_time == None): - self.first_press_time = ans.press_time - with If(ans.press_time['time'] - self.last_check < - config.NON_PRESS_INT): - self.HAPPY_SPEED = (config.HAPPY_INC_BASE * (Ref(clock.now) - - self.happy_start_time) * config.HAPPY_MOD) + config.HAPPY_INC_START - with Else(): - self.HAPPY_SPEED = config.HAPPY_INC_START - self.happy_start_time = Ref(clock.now) - self.last_check = Ref(clock.now) - with If(ans.pressed == config.RESP_HAPPY[0]): - with If(sld.value - self.HAPPY_SPEED <= (-1*config.HAPPY_RANGE)): - UpdateWidget(sld, value=(-1*config.HAPPY_RANGE)) - with Else(): - UpdateWidget(sld, value=sld.value - self.HAPPY_SPEED) - with Elif(ans.pressed == config.RESP_HAPPY[1]): - with If(sld.value + self.HAPPY_SPEED >= config.HAPPY_RANGE): - UpdateWidget(sld, value=config.HAPPY_RANGE) - with Else(): - UpdateWidget(sld, value=sld.value + self.HAPPY_SPEED) - with UntilDone(): - submit = KeyPress(keys=['SPACEBAR']) - Log(name="happy", - task=task, - block_num=block_num, - trial_num=trial_num, - slider_appear=sld.appear_time, - first_press=self.first_press_time, - submit_time=submit.press_time, - value=sld.value) - - - - - -if __name__ == "__main__": - import config - - exp = Experiment() - - HappyQuest(config) - - exp.run() diff --git a/tasks/RDM/instruct.py b/tasks/RDM/instruct.py index b2c123d..cab6e2c 100644 --- a/tasks/RDM/instruct.py +++ b/tasks/RDM/instruct.py @@ -8,8 +8,8 @@ # Text for instructions -top_text = {'E':'Your goal is to determine the direction that\n' + - '[i]MOST[/i] of the dots are moving.\n' + +top_text = {'E':'You are an astronaut and your goal is to determine the direction that\n' + + '[i]MOST[/i] of the satellites are moving through the sky.\n' + 'Please respond quickly and accurately.', 'S':'Su objetivo es determinar la dirección que la\n' + '[b]MAYORÍA[/b] de los puntos se están moviendo.\n' + @@ -166,8 +166,12 @@ def Instruct(self, config, lang="E", practice=False): # do the practice block with Loop(self.md_blocks) as block: + with Parallel(): # put up the fixation cross - cross = Label(text='+', color=config.CROSS_COLOR, + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0]*1.1, self.exp.screen.size[1]*1.1), allow_stretch = True, keep_ratio = False, blocking=False) + Border= Ellipse(size = (s((config.RADIUS)*1.2*2),(s((config.RADIUS)*1.2*2))), color = (.55,.55,.55,1)) + Telescope = Ellipse(size = (s((config.RADIUS)*1.1*2),(s((config.RADIUS)*1.1*2))), color = (.35, .35, .35, 1.0)) + cross = Label(text='+', color=config.CROSS_COLOR, font_size=s(config.CROSS_FONTSIZE)) with UntilDone(): # loop over trials diff --git a/tasks/RDM/main.py b/tasks/RDM/main.py index 86b4162..a741c8c 100644 --- a/tasks/RDM/main.py +++ b/tasks/RDM/main.py @@ -1,21 +1,19 @@ # -*- coding: utf-8 -*- # load all the states -from smile.common import Log, Label, Wait, Ref, Rectangle, Func, Debug, Loop, \ - UntilDone, If, Else, Parallel, Subroutine, KeyPress, \ - UpdateWidget +from smile.common import * from smile.scale import scale as s from smile.lsl import LSLPush import smile.ref as ref from smile.clock import clock -from .happy import HappyQuest +from ..happy import HappyQuest from .list_gen import gen_moving_dot_trials from math import log from .trial import Trial, GetResponse from .instruct import Instruct -from . import version +# from . import version def _get_score(config, corr_trials, num_trials, rt_trials): @@ -40,7 +38,7 @@ def _get_score(config, corr_trials, num_trials, rt_trials): @Subroutine def RDMExp(self, config, run_num=0, lang="E", pulse_server=None, practice=False, - happy_mid=True): + happy_mid=False): if len(config.CONT_KEY) > 1: cont_key_str = str(config.CONT_KEY[0]) + " or " + \ @@ -50,11 +48,11 @@ def RDMExp(self, config, run_num=0, lang="E", pulse_server=None, practice=False, res = Func(gen_moving_dot_trials, config) - Log(name="RDMinfo", - version=version.__version__, - author=version.__author__, - date_time=version.__date__, - email=version.__email__) + Log(name="RDMinfo") + # version=version.__version__, + # author=version.__author__, + # date_time=version.__date__, + # email=version.__email__) self.md_blocks = res.result @@ -88,9 +86,13 @@ def RDMExp(self, config, run_num=0, lang="E", pulse_server=None, practice=False, # loop over blocks with Loop(self.md_blocks) as block: + with Parallel(): # put up the fixation cross - cross = Label(text='+', color=config.CROSS_COLOR, - font_size=s(config.CROSS_FONTSIZE)) + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0]*1.1, self.exp.screen.size[1]*1.1), allow_stretch = True, keep_ratio = False, blocking=False) + Border= Ellipse(size = (s((config.RADIUS)*1.2*2),(s((config.RADIUS)*1.2*2))), color = (.55,.55,.55,1)) + Telescope = Ellipse(size = (s((config.RADIUS)*1.1*2),(s((config.RADIUS)*1.1*2))), color = (.35, .35, .35, 1.0)) + cross = Label(text='+', color=config.CROSS_COLOR, + font_size=s(config.CROSS_FONTSIZE)) with UntilDone(): # loop over trials with Loop(block.current) as trial: @@ -100,7 +102,7 @@ def RDMExp(self, config, run_num=0, lang="E", pulse_server=None, practice=False, with Parallel(): Rectangle(blocking=False, color=(.35, .35, .35, 1.0), size=self.exp.screen.size) - HappyQuest(config, task='RDM', block_num=run_num, trial_num=trial.i) + HappyQuest(task='RDM', block_num=run_num, trial_num=trial.i) self.start_happy = Func(clock.now).result self.end_happy = self.start_happy + ref.jitter(config.TIME_BETWEEN_HAPPY, config.TIME_JITTER_HAPPY) @@ -138,7 +140,7 @@ def RDMExp(self, config, run_num=0, lang="E", pulse_server=None, practice=False, # log what we need Log(trial.current, - name='MD', + name='rdm', run_num=run_num, appear_time=mdt.appear_time, disappear_time=mdt.disappear_time, @@ -152,7 +154,7 @@ def RDMExp(self, config, run_num=0, lang="E", pulse_server=None, practice=False, fmri_tr_time=self.trkp_press_time, eeg_pulse_time=mdt.eeg_pulse_time) Wait(.5) - HappyQuest(config, task='RDM', block_num=run_num, trial_num=trial.i) + HappyQuest(task='RDM', block_num=run_num, trial_num=trial.i) # Press 6 to say we are done recording then show them their score. if config.FMRI: self.keep_tr_checking = True diff --git a/tasks/RDM/trial.py b/tasks/RDM/trial.py index aaf04f6..c1eaca8 100644 --- a/tasks/RDM/trial.py +++ b/tasks/RDM/trial.py @@ -48,65 +48,74 @@ def Trial(self, num_dots=100, right_coherence=0.0, left_coherence=0.0, - pulse_server=None + pulse_server=None, ): self.eeg_pulse_time = None - with Serial(): - # present the dots - with Parallel(): - cross.update(color=(.35, .35, .35, 1.0)) - md = MovingDots(color=color, scale=s(config.SCALE), - num_dots=num_dots, radius=s(config.RADIUS), - motion_props=[{"coherence": right_coherence, - "direction": 0, - "direction_variance": 0}, - {"coherence": left_coherence, - "direction": 180, - "direction_variance": 0}], - lifespan=config.LIFESPAN, - lifespan_variance=config.LIFESPAN_VAR, - speed=s(config.SPEED)) - with UntilDone(): - # Collect key response - Wait(until=md.appear_time) - if config.EEG: - pulse_fn = LSLPush(server=pulse_server, - val=Ref.getitem(config.EEG_CODES, - "code")) - Log(name="RDM_PULSES", - start_time=pulse_fn.push_time) - self.eeg_pulse_time = pulse_fn.push_time + + with Parallel(): + background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0]*1.1, self.exp.screen.size[1]*1.1), allow_stretch = True, keep_ratio = False, blocking=False) + border = Ellipse(color = (1,1,1,0)) + telescope = Ellipse(color = (1,1,1,0)) + with UntilDone(): + + with Serial(): + # present the dots + with Parallel(): + cross.update(color=(.35, .35, .35, 1.0)) + md = MovingDots(color=color, scale=s(config.SCALE), + num_dots=num_dots, radius=s(config.RADIUS), + motion_props=[{"coherence": right_coherence, + "direction": 0, + "direction_variance": 0}, + {"coherence": left_coherence, + "direction": 180, + "direction_variance": 0}], + lifespan=config.LIFESPAN, + lifespan_variance=config.LIFESPAN_VAR, + speed=s(config.SPEED)) + border.update(center = md.center, size = (md.width*1.2, md.height*1.2), color = (.55,.55,.55,1)) + telescope.update(center = md.center, size = (md.width*1.1, md.height*1.1), color = (.35, .35, .35, 1.0)) + with UntilDone(): + # Collect key response + Wait(until=md.appear_time) + if config.EEG: + pulse_fn = LSLPush(server=pulse_server, + val=Ref.getitem(config.EEG_CODES, + "code")) + Log(name="RDM_PULSES", + start_time=pulse_fn.push_time) + self.eeg_pulse_time = pulse_fn.push_time - gr = GetResponse(correct_resp=correct_resp, - base_time=md.appear_time['time'], - duration=config.RESPONSE_DURATION, - keys=config.RESP_KEYS) - self.pressed = gr.pressed - self.press_time = gr.press_time - self.rt = gr.rt - self.correct = gr.correct - # give feedback - with If(self.pressed == correct_resp): - # They got it right - Label(text=u"\u2713", color='green', duration=config.FEEDBACK_TIME, - font_size=s(config.FEEDBACK_FONT_SIZE), - font_name='DejaVuSans.ttf') - with Elif(self.pressed == incorrect_resp): - # they got it wrong - Label(text=u"\u2717", color='red', - font_size=s(config.FEEDBACK_FONT_SIZE), - duration=config.FEEDBACK_TIME, font_name='DejaVuSans.ttf') - with Else(): - # too slow - Label(text="Too Slow!", font_size=s(config.FEEDBACK_FONT_SIZE), - duration=config.FEEDBACK_TIME*2.) + gr = GetResponse(correct_resp=correct_resp, + base_time=md.appear_time['time'], + duration=config.RESPONSE_DURATION, + keys=config.RESP_KEYS) + self.pressed = gr.pressed + self.press_time = gr.press_time + self.rt = gr.rt + self.correct = gr.correct + # give feedback + with If(self.pressed == correct_resp): + # They got it right + Label(text=u"\u2713", color='green', duration=config.FEEDBACK_TIME, + font_size=s(config.FEEDBACK_FONT_SIZE), + font_name='DejaVuSans.ttf') + with Elif(self.pressed == incorrect_resp): + # they got it wrong + Label(text=u"\u2717", color='red', + font_size=s(config.FEEDBACK_FONT_SIZE), + duration=config.FEEDBACK_TIME, font_name='DejaVuSans.ttf') + with Else(): + # too slow + Label(text="Too Slow!", font_size=s(config.FEEDBACK_FONT_SIZE), + duration=config.FEEDBACK_TIME*2.) - # bring the cross back - cross.update(color=config.CROSS_COLOR) + # bring the cross back + cross.update(color=config.CROSS_COLOR) - # save vars - self.appear_time = md.appear_time - self.disappear_time = md.disappear_time - self.refresh_rate = md.widget.refresh_rate + # save vars + self.appear_time = md.appear_time + self.disappear_time = md.disappear_time + self.refresh_rate = md.widget.refresh_rate diff --git a/tasks/__init__.py b/tasks/__init__.py index 683e1e8..31097a0 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -2,3 +2,4 @@ from .AssBind import AssBindExp, AssBind_config from .BARTUVA import BartuvaExp, Bartuva_config from .flanker import FlankerExp, Flanker_config +from .happy import HappyQuest diff --git a/tasks/flanker/config.py b/tasks/flanker/config.py index 8c67c2e..da42bda 100644 --- a/tasks/flanker/config.py +++ b/tasks/flanker/config.py @@ -1,4 +1,14 @@ -#from numpy import linspace +from pathlib import Path + +# Define the base directory as the directory containing the current file +BASE_DIR = Path(__file__).resolve().parent + +# Use BASE_DIR / "stim" / "fish_" to define the STIM_DIRECTORY path +STIM_DIRECTORY = str(BASE_DIR / "stim" / "fish_") + +# Background image path +BACKGROUND_IMAGE = str(BASE_DIR / "ocean_background.png") + # FLANKER VARIABLES NUM_TRIALS = 1 # (len(evidence_conditions)-1) * 4 + 2 * num_trials NUM_BLOCKS = 1 @@ -9,87 +19,30 @@ CONT_KEY = ['1', '4'] NUM_FLANKS = 2 +LAYERS = [{"condition": "=", "dir":"right", "layers":["right","left"]},{"condition":"~","dir":"right","layers":["left","right"]},{"condition": "=", "dir":"left", "layers":["left","right"]},{"condition":"~","dir":"left","layers":["right","left"]},{"condition": "+", "dir":"right", "layers":["right","right"]},{"condition":"+","dir":"left","layers":["left","left"]}] + + CONDITIONS = [ # Mixed Easy R - {"stim": "__<__\n" + - "_<><_\n" + - "<>>><\n" + - "_<><_\n" + - "__<__\n", - "condition": "=", - "dir": "R"}, + {"condition": "=", + "dir": "right"}, # Mixed Hard R - {"stim": "__>__\n" + - "_><>_\n" + - "><><>\n" + - "_><>_\n" + - "__>__\n", - "condition": "~", - "dir": "R"}, + { "condition": "~", + "dir": "right"}, # Mixed Hard L - {"stim": "__<__\n" + - "_<><_\n" + - "<><><\n" + - "_<><_\n" + - "__<__\n", - "condition": "~", - "dir": "L"}, + {"condition": "~", + "dir": "left"}, # Mixed Easy L - {"stim": "__>__\n" + - "_><>_\n" + - "><<<>\n" + - "_><>_\n" + - "__>__\n", - "condition": "=", - "dir": "L"}, + {"condition": "=", + "dir": "left"}, # Congruent R - {"stim": "__>__\n" + - "_>>>_\n" + - ">>>>>\n" + - "_>>>_\n" + - "__>__\n", - "condition": "+", - "dir": "R"}, + {"condition": "+", + "dir": "right"}, # Congruent L - {"stim": "__<__\n" + - "_<<<_\n" + - "<<<<<\n" + - "_<<<_\n" + - "__<__\n", - "condition": "+", - "dir": "L"}, + {"condition": "+", + "dir": "left"}, ] -# uNCOMMENT THIS LINE FOR EXTRA CONDITIONS -"""CONDITIONS = CONDITIONS + [{"stim": "__>__\n" + - "_<><_\n" + - "<<><<\n" + - "_<><_\n" + - "__>__\n", - "condition": "|", - "dir": "R"}, - {"stim": "__<__\n" + - "_<<<_\n" + - ">>>>>\n" + - "_<<<_\n" + - "__<__\n", - "condition": "--", - "dir": "R"}, - {"stim": "__<__\n" + - "_><>_\n" + - ">><>>\n" + - "_><>_\n" + - "__<__\n", - "condition": "|", - "dir": "L"}, - {"stim": "__>__\n" + - "_>>>_\n" + - "<<<<<\n" + - "_>>>_\n" + - "__>__\n", - "condition": "--", - "dir": "L"}, - ]""" #EVIDENCE_CONDITIONS = [0., 45.] NUM_LOCS = 8 @@ -102,6 +55,9 @@ FROM_CENTER = 300 FEEDBACK_TIME = 0.5 +STIM_SIZE = 50 +PADDING = 5 + SKIP_SIZE = [200, 50] SKIP_FONT_SIZE = 25 @@ -133,15 +89,6 @@ TIME_BETWEEN_HAPPY = 15 TIME_JITTER_HAPPY = 10 -HAPPY_FONT_SIZE = 25 -HAPPY_INC_BASE = .02 -HAPPY_INC_START = .2 -HAPPY_MOD = 20. -HAPPY_RANGE = 10 -NON_PRESS_INT = .1 -PRESS_INT = .016 -SLIDER_WIDTH = 1000 -RESP_HAPPY = ["F", "J"] TOUCH = False diff --git a/tasks/flanker/flanker.py b/tasks/flanker/flanker.py new file mode 100644 index 0000000..151a071 --- /dev/null +++ b/tasks/flanker/flanker.py @@ -0,0 +1,89 @@ +from smile.common import * +from smile.scale import scale as s + + +@Subroutine +def Flanker(self, config, center_x, center_y, direction, condition, layers, num_layers = 2, background = True): + #num_layers describes how many layers there are in the diamond, for example 2 layers means that the diamond + #is an array of 1,3,5,3,1 + self.condition = condition + self.center_direction = direction + self.center_image = config.STIM_DIRECTORY + Ref(str, self.center_direction) + ".png" + self.center_x = center_x + self.center_y = center_y + self.stim_appear_time = None + self.stim_disappear_time = None + + with Loop(layers) as layer: + with If(layer.current["condition"] == self.condition): + with If(layer.current["dir"] == self.center_direction): + self.layer_list = layer.current["layers"] + + with Parallel(): + with Parallel(): + background_image = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False) + center_image = Image(source = self.center_image, center = (self.center_x, self.center_y), size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), allow_stretch = True, keep_ratio = False) + # self.stim_appear_time = center_image.appear_time + self.outer_layer = 0 + for i in range(num_layers): + self.stim_direction = self.layer_list[Ref(int,self.outer_layer)] + self.stim_image = config.STIM_DIRECTORY + Ref(str, self.stim_direction) + ".png" + self.outer_layer = self.outer_layer + 1 + add_right = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (self.center_x + (s(config.STIM_SIZE + config.PADDING)*(self.outer_layer)), self.center_y)) + add_left = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (self.center_x - (s(config.STIM_SIZE + config.PADDING)*(self.outer_layer)), self.center_y)) + add_up = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (self.center_x, self.center_y + (s(config.STIM_SIZE + config.PADDING)*(self.outer_layer)))) + add_down = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (self.center_x, self.center_y - (s(config.STIM_SIZE + config.PADDING)*(self.outer_layer)))) + # #Need to add in actual stim rules here -- this needs to be the opposite of whatever happens in the top depending on condition + self.layer = self.outer_layer + for mult in range(num_layers - (i+1)): + self.stim_direction = self.layer_list[Ref(int,self.layer)] + self.stim_image = config.STIM_DIRECTORY + Ref(str, self.stim_direction) + ".png" + + add_right_up = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (add_right.center_x, add_right.center_y + s(config.STIM_SIZE + config.PADDING)*(mult+1))) + add_right_down = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (add_right.center_x, add_right.center_y - s(config.STIM_SIZE + config.PADDING)*(mult+1))) + add_left_up = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (add_left.center_x, add_left.center_y + s(config.STIM_SIZE + config.PADDING)*(mult+1))) + add_left_down = Image(source = self.stim_image, size = (s(config.STIM_SIZE), s(config.STIM_SIZE)), + keep_ratio = False, allow_stretch = True, + center = (add_left.center_x, add_left.center_y - s(config.STIM_SIZE + config.PADDING)*(mult+1))) + self.layer = self.layer + 1 + with If(background == False): + background_image.update(color = (1,1,1,0)) + with Serial(): + Wait(until=center_image.appear_time) + self.stim_appear_time = center_image.appear_time + # self.stim_disappear_time = center_image.disappear_time + + +if __name__ == "__main__": + import config as config + from list_gen import gen_fblocks + blocks = gen_fblocks(config) + exp = Experiment() + with Loop(blocks) as block: + with Loop(block.current) as trial: + fl = Flanker(config, + center_x = exp.screen.center_x + trial.current['loc_x']*s(config.FROM_CENTER), + center_y = exp.screen.center_y + trial.current['loc_y']*s(config.FROM_CENTER), + direction = trial.current["dir"], + condition = trial.current['condition'], + layers = config.LAYERS) + with UntilDone(): + Wait(until=fl.stim_appear_time) + Wait(3) + exp.run() diff --git a/tasks/flanker/happy.py b/tasks/flanker/happy.py deleted file mode 100644 index eb94566..0000000 --- a/tasks/flanker/happy.py +++ /dev/null @@ -1,69 +0,0 @@ -from smile.common import * -from smile.scale import scale as s -from smile.clock import clock -@Subroutine -def HappyQuest(self, config, task, block_num, trial_num): - with Parallel(): - Label(text="How happy are you at this moment?\nPress F to move left, Press J to move right.", - font_size=s(config.HAPPY_FONT_SIZE), - halign='center', - center_y=self.exp.screen.center_y + s(300)) - sld = Slider(min=-10, max=10, value=0, width=s(config.SLIDER_WIDTH)) - Label(text="unhappy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.left, center_y=sld.center_y - s(100)) - Label(text="happy", font_size=s(config.HAPPY_FONT_SIZE), - center_x=sld.right, center_y=sld.center_y - s(100)) - Label(text='Press Spacebar to lock-in your response.', - top=sld.bottom - s(250), font_size=s(config.HAPPY_FONT_SIZE)) - - with UntilDone(): - self.happy_start_time = Ref(clock.now) - self.last_check = self.happy_start_time - self.happy_dur = 0.0 - self.HAPPY_SPEED = config.HAPPY_INC_BASE - self.first_press_time = None - with Loop(): - ans = KeyPress(keys=config.RESP_HAPPY) - with If(self.first_press_time == None): - self.first_press_time = ans.press_time - with If(ans.press_time['time'] - self.last_check < - config.NON_PRESS_INT): - self.HAPPY_SPEED = (config.HAPPY_INC_BASE * (Ref(clock.now) - - self.happy_start_time) * config.HAPPY_MOD) + config.HAPPY_INC_START - with Else(): - self.HAPPY_SPEED = config.HAPPY_INC_START - self.happy_start_time = Ref(clock.now) - self.last_check = Ref(clock.now) - with If(ans.pressed == config.RESP_HAPPY[0]): - with If(sld.value - self.HAPPY_SPEED <= (-1*config.HAPPY_RANGE)): - UpdateWidget(sld, value=(-1*config.HAPPY_RANGE)) - with Else(): - UpdateWidget(sld, value=sld.value - self.HAPPY_SPEED) - with Elif(ans.pressed == config.RESP_HAPPY[1]): - with If(sld.value + self.HAPPY_SPEED >= config.HAPPY_RANGE): - UpdateWidget(sld, value=config.HAPPY_RANGE) - with Else(): - UpdateWidget(sld, value=sld.value + self.HAPPY_SPEED) - with UntilDone(): - submit = KeyPress(keys=['SPACEBAR']) - Log(name="happy", - task=task, - block_num=block_num, - trial_num=trial_num, - slider_appear=sld.appear_time, - - first_press=self.first_press_time, - submit_time=submit.press_time, - value=sld.value) - - - - -if __name__ == "__main__": - import config - - exp = Experiment() - - HappyQuest(config) - - exp.run() diff --git a/tasks/flanker/instruct.py b/tasks/flanker/instruct.py index 3c460ec..824044a 100644 --- a/tasks/flanker/instruct.py +++ b/tasks/flanker/instruct.py @@ -2,7 +2,9 @@ from smile.common import * from smile.scale import scale as s -from .widget import Flanker +import config + +from .flanker import Flanker from kivy.utils import platform from .trial import Trial, GetResponse from math import cos, sin, sqrt, pi, radians @@ -18,7 +20,7 @@ def get_instructions(config): inst = {} inst['top_text'] = 'You will be presented with a group of symbols. \n' + \ 'You will be asked to indicate the direction that the \n' + \ - '[b]middle arrow is pointing[/b], while ignoring any other symbols. \n\n' + '[b]middle symbol is pointing[/b], while ignoring any other symbols. \n\n' inst['inst_1'] = '[b]Practice 1:[/b] \n \n' + \ 'Respond to the arrow in the red circle while ignoring the other symbols. \n' @@ -82,42 +84,59 @@ def Instruct(self, config, lang="E"): font_size=s(config.INST_FONT_SIZE)) # upper left - Flanker(center_x=self.exp.screen.center_x - s(400), + Flanker(config, center_x=self.exp.screen.center_x - s(400), center_y=toplbl.top + s(125), - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim="__<__\n_<<<_\n<<<<<\n_<<<_\n__<__\n") + direction = "left", + condition = "+", + layers = config.LAYERS, + background = False) + + #stim="__<__\n_<<<_\n<<<<<\n_<<<_\n__<__\n") # upper middle - Flanker(center_x=self.exp.screen.center_x, + Flanker(config, center_x=self.exp.screen.center_x, center_y=toplbl.top + s(125), - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim="__>__\n_><>_\n><<<>\n_><>_\n__>__\n") + direction = "left", + condition = "=", + layers = config.LAYERS, + background = False) + + #stim="__>__\n_><>_\n><<<>\n_><>_\n__>__\n") # upper right - Flanker(center_x=self.exp.screen.center_x + s(400), + Flanker(config, center_x=self.exp.screen.center_x + s(400), center_y=toplbl.top + s(125), - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim="__<__\n_<><_\n<><><\n_<><_\n__<__\n") + direction = "left", + condition = "~", + layers = config.LAYERS, + background = False) + + + #stim="__<__\n_<><_\n<><><\n_<><_\n__<__\n") # lower left - Flanker(center_x=self.exp.screen.center_x - s(400), + Flanker(config, center_x=self.exp.screen.center_x - s(400), center_y=toplbl.bottom - s(125), - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim="__<__\n_<><_\n<><><\n_<><_\n__<__\n") + direction = "right", + condition = "+", + layers = config.LAYERS, + background = False) + # stim="__<__\n_<><_\n<><><\n_<><_\n__<__\n") + # WONDERING IF THIS IS AN ERROR BECAUSE IT SHOWS THE SAME ONE TWICE, I DIDN'T WRITE THIS PART # lower middle - Flanker(center_x=self.exp.screen.center_x, + Flanker(config, center_x=self.exp.screen.center_x, center_y=toplbl.bottom - s(125), - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim="__>__\n_><>_\n><><>\n_><>_\n__>__\n") + direction = "right", + condition = "=", + layers = config.LAYERS, + background = False) + # stim="__>__\n_><>_\n><><>\n_><>_\n__>__\n") # lower right - Flanker(center_x=self.exp.screen.center_x + s(400), + Flanker(config, center_x=self.exp.screen.center_x + s(400), center_y=toplbl.bottom - s(125), - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim="__<__\n_<><_\n<>>><\n_<><_\n__<__\n") + direction = "right", + condition = "~", + layers = config.LAYERS, + background = False) + # stim="__<__\n_<><_\n<>>><\n_<><_\n__<__\n") with UntilDone(): Wait(until=toplbl.appear_time) @@ -125,19 +144,19 @@ def Instruct(self, config, lang="E"): Wait(1.0) - with Loop([["__<__\n_<<<_\n<<<<<\n_<<<_\n__<__\n", config.RESP_KEYS[0]], - ["__>__\n_><>_\n><<<>\n_><>_\n__>__\n", config.RESP_KEYS[0]], - ["__<__\n_<><_\n<>>><\n_<><_\n__<__\n", config.RESP_KEYS[-1]], - ["__>__\n_><>_\n><><>\n_><>_\n__>__\n", config.RESP_KEYS[-1]]]) as prac_ev: - Wait(1.0) + with Loop([["left", config.RESP_KEYS[0]], + ["left", config.RESP_KEYS[0]], + ["right", config.RESP_KEYS[-1]], + ["right", config.RESP_KEYS[-1]]]) as prac_ev: p2 = Trial(config, - stim=prac_ev.current[0], + direct=prac_ev.current[0], center_x=self.exp.screen.center_x, center_y=self.exp.screen.center_y, - correct_resp=prac_ev.current[1], condition='+') + correct_resp=prac_ev.current[1], condition='+', + background = False) with If(p2.correct): # They got it right - Label(text=u"\u2713", color='green', duration=config.FEEDBACK_TIME, + Label(text=u"\u2713", color='lime', duration=config.FEEDBACK_TIME, font_size=s(config.FEEDBACK_FONT_SIZE), font_name='DejaVuSans.ttf') with Else(): @@ -168,21 +187,26 @@ def Instruct(self, config, lang="E"): GetResponse(keys=config.CONT_KEY) - with Loop([["__<__\n_<><_\n<>>><\n_<><_\n__<__\n", config.RESP_KEYS[1], + with Loop([["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*5)), sin(radians((360./config.NUM_LOCS)*5))], - ["__<__\n_<><_\n<><><\n_<><_\n__<__\n", config.RESP_KEYS[0], + ["left", config.RESP_KEYS[0], cos(radians((360./config.NUM_LOCS)*1)), sin(radians((360./config.NUM_LOCS)*1))], - ["__>__\n_><>_\n><><>\n_><>_\n__>__\n", config.RESP_KEYS[1], + ["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*3.)), sin(radians((360./config.NUM_LOCS)*3.))], - ["__>__\n_><>_\n><><>\n_><>_\n__>__\n", config.RESP_KEYS[1], + ["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*4.)), sin(radians((360./config.NUM_LOCS)*4.))], - ["__>__\n_>>>_\n>>>>>\n_>>>_\n__>__\n", config.RESP_KEYS[1], + ["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*0)), sin(radians((360./config.NUM_LOCS)*0))], - ["__>__\n_><>_\n><<<>\n_><>_\n__>__\n", config.RESP_KEYS[0], + ["left", config.RESP_KEYS[0], cos(radians((360./config.NUM_LOCS)*2)), sin(radians((360./config.NUM_LOCS)*2))] ]) as prac_ev: - Wait(1.0) + # Wait(1.0) with Parallel(): + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False, blocking = False) + fix = Label(text='+', color=config.CROSS_COLOR, + font_size=s(config.CROSS_FONTSIZE), blocking = False) Ellipse(color='red', size=(s(55),s(55)), center_x=self.exp.screen.center_x + prac_ev.current[2]*s(config.FROM_CENTER), center_y=self.exp.screen.center_y + prac_ev.current[3]*s(config.FROM_CENTER), @@ -191,18 +215,28 @@ def Instruct(self, config, lang="E"): center_x=self.exp.screen.center_x + prac_ev.current[2]*s(config.FROM_CENTER), center_y=self.exp.screen.center_y + prac_ev.current[3]*s(config.FROM_CENTER), blocking=False) - p4 = Trial(config,stim=prac_ev.current[0], + p4 = Trial(config,direct=prac_ev.current[0], center_x=self.exp.screen.center_x + prac_ev.current[2]*s(config.FROM_CENTER), center_y=self.exp.screen.center_y + prac_ev.current[3]*s(config.FROM_CENTER), - correct_resp=prac_ev.current[1], condition='+') + correct_resp=prac_ev.current[1], condition='+', + background = False) with If(p4.correct): + with Parallel(): # They got it right - Label(text=u"\u2713", color='green', duration=config.FEEDBACK_TIME, + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False, blocking = False) + Label(text=u"\u2713", color='lime', duration=config.FEEDBACK_TIME, font_size=s(config.FEEDBACK_FONT_SIZE), font_name='DejaVuSans.ttf') + with Else(): + with Parallel(): + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False, blocking = False) # they got it wrong - Label(text=u"\u2717", color='red', + Label(text=u"\u2717", color='red', font_size=s(config.FEEDBACK_FONT_SIZE), duration=config.FEEDBACK_TIME, font_name='DejaVuSans.ttf') @@ -227,32 +261,48 @@ def Instruct(self, config, lang="E"): GetResponse(keys=config.CONT_KEY) - with Loop([["__<__\n_<><_\n<>>><\n_<><_\n__<__\n", config.RESP_KEYS[1], + with Loop([["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*5)), sin(radians((360./config.NUM_LOCS)*5))], - ["__<__\n_<><_\n<><><\n_<><_\n__<__\n", config.RESP_KEYS[0], + ["left", config.RESP_KEYS[0], cos(radians((360./config.NUM_LOCS)*1)), sin(radians((360./config.NUM_LOCS)*1))], - ["__>__\n_><>_\n><><>\n_><>_\n__>__", config.RESP_KEYS[1], + ["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*3)), sin(radians((360./config.NUM_LOCS)*3))], - ["__>__\n_>>>_\n>>>>>\n_>>>_\n__>__\n", config.RESP_KEYS[1], + ["right", config.RESP_KEYS[1], cos(radians((360./config.NUM_LOCS)*6)), sin(radians((360./config.NUM_LOCS)*6))], - ["__>__\n_><>_\n><<<>\n_><>_\n__>__\n", config.RESP_KEYS[0], + ["left", config.RESP_KEYS[0], cos(radians((360./config.NUM_LOCS)*2)), sin(radians((360./config.NUM_LOCS)*2))] ]) as prac_ev: - Wait(1.0) - p5 = Trial(config, stim=prac_ev.current[0], + # Wait(1.0) + with Parallel(): + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False, blocking = False) + fix = Label(text='+', color=config.CROSS_COLOR, + font_size=s(config.CROSS_FONTSIZE), + blocking=False) + p5 = Trial(config, direct=prac_ev.current[0], center_x=self.exp.screen.center_x + prac_ev.current[2]*s(config.FROM_CENTER), center_y=self.exp.screen.center_y + prac_ev.current[3]*s(config.FROM_CENTER), - correct_resp=prac_ev.current[1], condition='+') + correct_resp=prac_ev.current[1], condition='+', + background = False) with If(p5.correct): # They got it right - Label(text=u"\u2713", color='green', duration=config.FEEDBACK_TIME, + with Parallel(): + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False, blocking = False) + Label(text=u"\u2713", color='lime', duration=config.FEEDBACK_TIME, font_size=s(config.FEEDBACK_FONT_SIZE), center_y=self.exp.screen.center_y + s(50), font_name='DejaVuSans.ttf') with Else(): # they got it wrong - Label(text=u"\u2717", color='red', + with Parallel(): + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False, blocking = False) + Label(text=u"\u2717", color='red', font_size=s(config.FEEDBACK_FONT_SIZE), center_y=self.exp.screen.center_y + s(50), duration=config.FEEDBACK_TIME, font_name='DejaVuSans.ttf') @@ -274,4 +324,4 @@ def Instruct(self, config, lang="E"): center_y=self.exp.screen.center_y, font_size=s(config.INST_FONT_SIZE)) with UntilDone(): - GetResponse(keys=config.CONT_KEY) + GetResponse(keys=config.CONT_KEY) \ No newline at end of file diff --git a/tasks/flanker/list_gen.py b/tasks/flanker/list_gen.py index 3216abe..fe6855b 100644 --- a/tasks/flanker/list_gen.py +++ b/tasks/flanker/list_gen.py @@ -15,13 +15,12 @@ def gen_fblocks(config): } for s in config.CONDITIONS: trial = temp_trial.copy() - if s['dir'] == "L": + if s['dir'] == "left": key_resp = config.RESP_KEYS[0] else: key_resp = config.RESP_KEYS[-1] # MIXED EASY RIGHT trial.update({'condition': s['condition'], - 'stim': s['stim'], 'dir': s['dir'], 'corr_resp': key_resp }) diff --git a/tasks/flanker/main.py b/tasks/flanker/main.py index 1f1420e..53e3f85 100644 --- a/tasks/flanker/main.py +++ b/tasks/flanker/main.py @@ -1,21 +1,19 @@ # -*- coding: utf-8 -*- # load all the states -from smile.common import Log, Label, Wait, Ref, Rectangle, Func, Debug, Loop, \ - UntilDone, If, Else, Parallel, Subroutine, KeyPress, \ - UpdateWidget +from smile.common import * from smile.clock import clock import smile.ref as ref from smile.scale import scale as s from smile.lsl import LSLPush -from .happy import HappyQuest +from ..happy import HappyQuest import smile.ref as ref from .list_gen import gen_fblocks from math import log from .trial import Trial, GetResponse from .instruct import Instruct -from . import version +#from . import version def _get_score(corr_trials, num_trials, rt_trials): @@ -29,7 +27,7 @@ def _get_score(corr_trials, num_trials, rt_trials): @Subroutine def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, - happy_mid=True): + happy_mid=False): if len(config.CONT_KEY) > 1: cont_key_str = str(config.CONT_KEY[0]) + " or " + \ @@ -40,11 +38,11 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, res = Func(gen_fblocks, config) self.f_blocks = res.result - Log(name="flankerinfo", - version=version.__version__, - author=version.__author__, - date_time=version.__date__, - email=version.__email__) + Log(name="flankerinfo") + #version=version.__version__, + #author=version.__author__, + #date_time=version.__date__, + #email=version.__email__) Instruct(config, lang=lang) Wait(2.0) @@ -75,7 +73,11 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, with Loop(self.f_blocks) as block: # put up the fixation cross - fix = Label(text='+', color=config.CROSS_COLOR, + with Parallel(): + Background = Image(source = config.BACKGROUND_IMAGE, size = (self.exp.screen.size[0] * 1.1, + self.exp.screen.size[1] * 1.1), + allow_stretch = True, keep_ratio = False) + fix = Label(text='+', color=config.CROSS_COLOR, font_size=s(config.CROSS_FONTSIZE)) with UntilDone(): @@ -88,7 +90,7 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, with Parallel(): Rectangle(blocking=False, color=(.35, .35, .35, 1.0), size=self.exp.screen.size) - HappyQuest(config, task='FLANKER', block_num=run_num, trial_num=trial.i) + HappyQuest(task='FLANKER', block_num=run_num, trial_num=trial.i) self.start_happy = Func(clock.now).result self.end_happy = self.start_happy + ref.jitter(config.TIME_BETWEEN_HAPPY, config.TIME_JITTER_HAPPY) @@ -97,8 +99,7 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, Wait(config.ITI, jitter=.25) # do the trial - ft = Trial(config, - stim=trial.current['stim'], + ft = Trial(config, direct = trial.current["dir"], center_x=self.exp.screen.center_x + trial.current['loc_x']*s(config.FROM_CENTER), center_y=self.exp.screen.center_y + trial.current['loc_y']*s(config.FROM_CENTER), correct_resp=trial.current['corr_resp'], @@ -117,7 +118,7 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, # log what we need Log(trial.current, - name='FL', + name='flkr', run_num=run_num, appear_time=ft.appear_time, disappear_time=ft.disappear_time, @@ -131,7 +132,7 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, eeg_pulse_time=ft.eeg_pulse_time) Wait(.5) - HappyQuest(config, task='FLANKER', block_num=run_num, trial_num=trial.i) + HappyQuest(task='FLANKER', block_num=run_num, trial_num=trial.i) if config.FMRI: self.keep_tr_checking = True @@ -198,7 +199,7 @@ def FlankerExp(self, config, run_num=0, lang="E", pulse_server=None, exp = Experiment(name="FLANKERONLY", background_color=((.35, .35, .35, 1.0)), scale_down=True, scale_box=(1200, 900)) - InputSubject(exp_title="Flanker") + # InputSubject(exp_title="Flanker") Wait(1.0) FlankerExp(config, run_num=0, lang="E", pulse_server=pulse_server) diff --git a/tasks/flanker/ocean_background.png b/tasks/flanker/ocean_background.png new file mode 100644 index 0000000..cbb4139 Binary files /dev/null and b/tasks/flanker/ocean_background.png differ diff --git a/tasks/flanker/stim/fish_left.png b/tasks/flanker/stim/fish_left.png new file mode 100644 index 0000000..7925af9 Binary files /dev/null and b/tasks/flanker/stim/fish_left.png differ diff --git a/tasks/flanker/stim/fish_right.png b/tasks/flanker/stim/fish_right.png new file mode 100644 index 0000000..be68d0c Binary files /dev/null and b/tasks/flanker/stim/fish_right.png differ diff --git a/tasks/flanker/trial.py b/tasks/flanker/trial.py index 6545f14..a4e014c 100644 --- a/tasks/flanker/trial.py +++ b/tasks/flanker/trial.py @@ -1,7 +1,7 @@ from smile.common import * from smile.scale import scale as s from smile.lsl import LSLPush -from .widget import Flanker +from .flanker import Flanker @Subroutine @@ -42,23 +42,23 @@ def GetResponse(self, @Subroutine def Trial(self, config, - stim, + direct, center_x, center_y, condition, correct_resp=None, color='white', - pulse_server=None): + pulse_server=None, + background = True): self.eeg_pulse_time = None # present the dots - fl = Flanker(center_x=center_x, center_y=center_y, - box=s(config.CONFIG_BOX), around=s(config.CONFIG_AROUND), - line_width=s(config.LW), - stim=stim) + fl = Flanker(config, center_x= center_x, center_y = center_y, direction = direct, condition = condition, layers = config.LAYERS, + background = background) + with UntilDone(): # Collect key response - Wait(until=fl.appear_time) + Wait(until=fl.stim_appear_time) if config.EEG: pulse_fn = LSLPush(server=pulse_server, val=Ref.getitem(config.EEG_CODES, condition)) @@ -66,7 +66,7 @@ def Trial(self, start_time=pulse_fn.push_time) self.eeg_pulse_time = pulse_fn.push_time gr = GetResponse(correct_resp=correct_resp, - base_time=fl.appear_time['time'], + base_time=fl.stim_appear_time["time"], duration=config.RESPONSE_DURATION, keys=config.RESP_KEYS) @@ -76,5 +76,5 @@ def Trial(self, self.correct = gr.correct # save vars - self.appear_time = fl.appear_time - self.disappear_time = fl.disappear_time + self.appear_time = fl.stim_appear_time + self.disappear_time = fl.stim_disappear_time diff --git a/tasks/happy.py b/tasks/happy.py index 13058ee..29e8f06 100644 --- a/tasks/happy.py +++ b/tasks/happy.py @@ -1,43 +1,63 @@ from smile.common import * from smile.scale import scale as s - +from smile.clock import clock +from . import happy_config as default_happy_config @Subroutine -def HappyQuest(self, config): +def HappyQuest(self, task, block_num, trial_num, config=default_happy_config): with Parallel(): Label(text="How happy are you at this moment?\nPress F to move left, Press J to move right.", - font_size=s(config.INST_FONT_SIZE), + font_size=s(config.HAPPY_FONT_SIZE), halign='center', - center_y=exp.screen.center_y + s(300)) + center_y=self.exp.screen.center_y + s(300)) sld = Slider(min=-10, max=10, value=0, width=s(config.SLIDER_WIDTH)) - Label(text="unhappy", font_size=s(config.INST_FONT_SIZE), + Label(text="unhappy", font_size=s(config.HAPPY_FONT_SIZE), center_x=sld.left, center_y=sld.center_y - s(100)) - Label(text="happy", font_size=s(config.INST_FONT_SIZE), + Label(text="happy", font_size=s(config.HAPPY_FONT_SIZE), center_x=sld.right, center_y=sld.center_y - s(100)) Label(text='Press Spacebar to lock-in your response.', - top=sld.bottom - s(250), font_size=s(config.INST_FONT_SIZE)) + top=sld.bottom - s(250), font_size=s(config.HAPPY_FONT_SIZE)) with UntilDone(): + self.happy_start_time = Ref(clock.now) + self.last_check = self.happy_start_time + self.happy_dur = 0.0 + self.happy_speed = config.HAPPY_INC_BASE + self.first_press_time = None with Loop(): - ans = KeyPress(keys=config.RESP_KEYS) - with If(ans.pressed == config.RESP_KEYS[0]): - with If(sld.value - config.HAPPY_SPEED <= -10): - UpdateWidget(sld, value=-10) + ans = KeyPress(keys=config.RESP_HAPPY) + with If(self.first_press_time == None): + self.first_press_time = ans.press_time + with If(ans.press_time['time'] - self.last_check < + config.NON_PRESS_INT): + self.happy_speed = (config.HAPPY_INC_BASE * (Ref(clock.now) - + self.happy_start_time) * config.HAPPY_MOD) + config.HAPPY_INC_START + with Else(): + self.happy_speed = config.HAPPY_INC_START + self.happy_start_time = Ref(clock.now) + self.last_check = Ref(clock.now) + with If(ans.pressed == config.RESP_HAPPY[0]): + with If(sld.value - self.happy_speed <= (-1*config.HAPPY_RANGE)): + UpdateWidget(sld, value=(-1*config.HAPPY_RANGE)) with Else(): - UpdateWidget(sld, value=sld.value - config.HAPPY_SPEED) - with Elif(ans.pressed == config.RESP_KEYS[1]): - with If(sld.value + config.HAPPY_SPEED >= 10): - UpdateWidget(sld, value=10) + UpdateWidget(sld, value=sld.value - self.happy_speed) + with Elif(ans.pressed == config.RESP_HAPPY[1]): + with If(sld.value + self.happy_speed >= config.HAPPY_RANGE): + UpdateWidget(sld, value=config.HAPPY_RANGE) with Else(): - UpdateWidget(sld, value=sld.value + config.HAPPY_SPEED) - Wait(.05) + UpdateWidget(sld, value=sld.value + self.happy_speed) with UntilDone(): - KeyPress(keys=['SPACEBAR']) + submit = KeyPress(keys=['SPACEBAR']) Log(name="happy", + task=task, + block_num=block_num, + trial_num=trial_num, + slider_appear=sld.appear_time, + first_press=self.first_press_time, + submit_time=submit.press_time, value=sld.value) - if __name__ == "__main__": import config diff --git a/tasks/happy_config.py b/tasks/happy_config.py new file mode 100644 index 0000000..eca1112 --- /dev/null +++ b/tasks/happy_config.py @@ -0,0 +1,8 @@ +HAPPY_FONT_SIZE = 25 +SLIDER_WIDTH = 1000 +HAPPY_INC_START = .2 +HAPPY_INC_BASE = .02 +NON_PRESS_INT = .1 +HAPPY_MOD = 20. +RESP_HAPPY = ["F", "J"] +HAPPY_RANGE = 10 \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..e69a907 --- /dev/null +++ b/utils.py @@ -0,0 +1,250 @@ +import sys +import plistlib +import zipfile +import requests +import logging +import time +from config import API_BASE_URL, RUNNING_FROM_EXECUTABLE, CURRENT_OS +from hashlib import blake2b +from io import BytesIO +from pathlib import Path +from typing import Optional +from pefile import PE, DIRECTORY_ENTRY + + +# Set up logging configuration +logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s') + + +def get_blocks_to_run(worker_id: str) -> list[str] | dict[str, str]: + """ + Sends a GET request to retrieve the list of blocks that are yet to be run by the worker. + + Args: + worker_id (str): The unique identifier for the worker whose blocks are being fetched. + + Returns: + dict: A dictionary with 'status' as success or error, and 'content' containing either the blocks list or an error message. + """ + url = f'{API_BASE_URL}/taskcontrol' + params = {'worker_id': worker_id} + + try: + response = requests.get(url, params=params) + response.raise_for_status() # Raise an error for non-2xx status codes + tasks = response.json().get('blocks_to_run', []) + logging.info(f'Tasks to run: {tasks}') + + # Format tasks from ['flkr_0] into [{'task_name': 'flkr', 'block_number': 0}] + reformatted_tasks = [{'task_name': task.split('_')[0], 'block_number': int(task.split('_')[1])} + for task in tasks] + logging.info(f'Reformatted task list: {reformatted_tasks}') + return {'status': 'success', 'content': reformatted_tasks} + + except requests.exceptions.HTTPError as http_error: + error_message = response.json().get('error', 'HTTP Error') + logging.error(f'HTTP Error: {http_error} - {error_message}') + return {'status': 'error', 'content': error_message} + + except requests.exceptions.ConnectionError as error_connecting: + logging.error(f'Error Connecting: {error_connecting}') + return {'status': 'error', 'content': 'Error Connecting'} + + except requests.exceptions.Timeout as timeout_error: + logging.error(f'Timeout Error: {timeout_error}') + return {'status': 'error', 'content': 'Timeout Error'} + + except requests.exceptions.RequestException as error: + logging.error(f'An error occurred: {error}') + error_message = response.json().get('error', 'Unspecified Error') + return {'status': 'error', 'content': error_message} + + +def hash_file(file_obj): + """ + Computes the blake2b hash of a file-like object. + + Args: + file_obj: File-like object opened in binary mode. + + Returns: + str: The hexadecimal representation of the hash. + """ + hash_blake = blake2b() + file_obj.seek(0) # Reset file pointer to the start + for chunk in iter(lambda: file_obj.read(4096), b""): + hash_blake.update(chunk) + return hash_blake.hexdigest() + + +def upload_block(worker_id: str, block_name: str, data_directory: str, slog_file_name: str) -> dict[str, str]: + """ + Sends a POST request to upload a completed block along with its checksum and the associated zipped file. + Uses the config.API_BASE_URL to build the URL. + + Args: + worker_id (str): The unique identifier for the worker who is uploading the block. + block_name (str): The name of the block being uploaded, typically in the format "taskname_runnumber". + data_directory (str): The directory where the slog file is located. + slog_file_name (str): The name of the slog file. + + Behavior: + - Zips the slog file. + - Computes the checksum of the zipped file. + - Sends the `worker_id`, `block_name`, and `checksum` in the form data. + - Uploads the zipped file associated with the block in the `files` parameter. + + Returns: + dict: A dictionary with 'status' ('success' or 'error') and 'content' (message or error details). + """ + url = f"{API_BASE_URL}/taskcontrol" + slog_file_path = Path(data_directory) / slog_file_name + + if not slog_file_path.is_file(): + logging.warning(f"File '{slog_file_name}' does not exist at '{slog_file_path}'.") + return {"status": "error", "content": "Log file not found"} + + logging.info(f"SLOG File Found: File '{slog_file_name}' exists at '{slog_file_path}'.") + + zip_buffer = BytesIO() + try: + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + zip_file.write(slog_file_path, slog_file_name) + + zip_buffer.seek(0) + checksum = hash_file(zip_buffer) + zip_buffer.seek(0) + + current_time_ms = int(time.time() * 1000) + zip_file_name_with_timestamp = f'{block_name}_{current_time_ms}.zip' + + params = {'worker_id': worker_id} + data = {'block_name': block_name, 'checksum': checksum} + files = {'file': (zip_file_name_with_timestamp, zip_buffer, 'application/zip')} + + response = requests.post(url, params=params, data=data, files=files) + logging.info(f"Response Status Code: {response.status_code}") + + if response.status_code == 200: + logging.info("Upload successful!") + return {"status": "success", "content": "Data upload successful."} + elif response.status_code == 400: + error_message = response.json().get("error", "Unknown error") + logging.warning(f"Bad Request: {error_message}") + return {"status": "error", "content": error_message} + elif response.status_code == 409: + logging.warning("Checksum mismatch: checksums don't match") + return {"status": "error", "content": "Checksum mismatch"} + else: + logging.error(f"Unexpected response: {response.status_code} - {response.text}") + return {"status": "error", "content": f"Unexpected response: {response.status_code}"} + + except requests.exceptions.ConnectionError as connection_error: + logging.error(f"Error Connecting: {connection_error}") + return {"status": "error", "content": "Connection error"} + except requests.exceptions.Timeout as timeout_error: + logging.error(f"Timeout Error: {timeout_error}") + return {"status": "error", "content": "Timeout error"} + except requests.exceptions.RequestException as error: + logging.error(f"An error occurred: {error}") + return {"status": "error", "content": "Request error"} + finally: + zip_buffer.close() + + +def retrieve_worker_id() -> dict[str, str]: + """ + Retrieves the worker ID based on the current OS and executable context. + + Returns: + dict: A dictionary with 'status' and 'content' containing the worker ID or error message. + """ + if RUNNING_FROM_EXECUTABLE: + # Select appropriate worker ID retrieval function based on OS + os_worker_id_function = { + 'Windows': _read_exe_worker_id, + 'Darwin': _read_app_worker_id + }.get(CURRENT_OS, lambda: {'status': 'error', 'content': 'Unsupported OS'}) + + return os_worker_id_function() + + return {'status': 'error', 'content': 'Not running from an executable'} + + +def _read_app_worker_id() -> dict[str, str]: + """ + Reads the 'WorkerID' from the Info.plist of the currently running .app bundle. + + Returns: + dict: A dictionary containing 'status' and 'content' (either the 'WorkerID' or an error message). + """ + exec_path: Path = Path(sys.executable).resolve() + + if exec_path.parent.name == 'MacOS' and exec_path.parents[1].name == 'Contents': + app_bundle_path = exec_path.parents[2] + else: + logging.warning("Executable is not inside a macOS .app bundle.") + return {"status": "error", "content": "Executable is not inside a macOS .app bundle"} + + plist_path: Path = app_bundle_path / 'Contents' / 'Info.plist' + + if not plist_path.exists(): + logging.warning(f"Info.plist not found at {plist_path}") + return {"status": "error", "content": "Info.plist not found"} + + try: + with plist_path.open('rb') as plist_file: + plist_data: dict = plistlib.load(plist_file) + worker_id = plist_data.get('WorkerID') + if worker_id: + logging.info(f"WorkerID found: {worker_id}") + return {"status": "success", "content": worker_id} + else: + logging.warning("WorkerID not found in Info.plist") + return {"status": "error", "content": "WorkerID not found in Info.plist"} + except plistlib.InvalidFileException: + logging.error(f"Invalid plist file at {plist_path}") + return {"status": "error", "content": "Invalid plist file"} + except Exception as e: + logging.error(f"An unexpected error occurred while reading {plist_path}: {e}") + return {"status": "error", "content": str(e)} + + +def _read_exe_worker_id() -> dict[str, str]: + """ + Reads the 'WorkerID' from the version info resource of the currently running .exe file on Windows. + + Returns: + dict: A dictionary containing 'status' and 'content' (either the 'WorkerID' or an error message). + """ + exe_file_path: str = sys.executable + try: + pe = PE(exe_file_path, fast_load=True) + pe.parse_data_directories( + directories=[DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_RESOURCE']]) + + if hasattr(pe, 'FileInfo'): + for file_info in pe.FileInfo: + for info in file_info: + if info.name == 'StringFileInfo': + version_info_dict: dict[bytes, bytes] = info.StringTable[0].entries + worker_id_bytes: Optional[bytes] = version_info_dict.get(b'WorkerID') + + if worker_id_bytes: + worker_id = worker_id_bytes.decode('utf-8') + logging.info(f"WorkerID found: {worker_id}") + return {"status": "success", "content": worker_id} + else: + logging.warning("WorkerID not found in executable version info") + return {"status": "error", "content": "WorkerID not found"} + except Exception as e: + logging.error(f"An error occurred while reading the executable version info: {e}") + return {"status": "error", "content": str(e)} + + +if __name__ == "__main__": + upload_block(worker_id='123456', + block_name='flkr_1', + data_directory='SUPREMEMOOD/1/20241016_185342/', + slog_file_name='log_bart_1.slog')