diff --git a/cometrics.py b/cometrics.py index a323b03..8c060d6 100644 --- a/cometrics.py +++ b/cometrics.py @@ -53,18 +53,19 @@ def main(config_file, first_time_user): config = ConfigUtils() manager = SessionManagerWindow(config, setup) get_process_memory() + setup_again = manager.setup_again if manager.close_program: break LoadingWindow(objects=manager) + if setup_again: + break except Exception as e: messagebox.showerror("Error", f"Exception encountered:\n{str(e)}\n{traceback.print_exc()}") manager = None - break + sys.exit(1) if manager: if manager.close_program: - break - else: - break + sys.exit(0) else: break sys.exit(0) diff --git a/cx_exe_setup.py b/cx_exe_setup.py index 92e0097..86cc605 100644 --- a/cx_exe_setup.py +++ b/cx_exe_setup.py @@ -50,8 +50,8 @@ build_exe_options = dict( - packages=["os", "sys", "tkinter", 'logger_util'], - includes=['pynput', 'pynput.keyboard._win32', 'pynput.mouse._win32', 'logger_util'], + packages=["os", "sys", "tkinter", 'logger_util', 'scipy'], + includes=['pynput', 'pynput.keyboard._win32', 'pynput.mouse._win32', 'logger_util', 'scipy._lib.deprecation'], excludes=[], include_files=['external_bin', 'images', 'reference', 'config.yml', 'LICENSE', (r'venv\Lib\site-packages\imageio', r'lib\imageio')], diff --git a/e4_utils.py b/e4_utils.py new file mode 100644 index 0000000..f524c75 --- /dev/null +++ b/e4_utils.py @@ -0,0 +1,288 @@ +import _tkinter +import csv +import glob +import json +import os +import pathlib +import threading +import tkinter +from datetime import datetime +from enum import IntEnum +from tkinter import messagebox +import neurokit2 as nk +import numpy as np +import pandas as pd +import warnings + +from pyempatica import EmpaticaE4 +from tkvideoutils import ImageLabel + +from tkinter_utils import center + +warnings.filterwarnings('ignore') + + +class EmpaticaData(IntEnum): + ACC_3D = 0 + ACC_X = 1 + ACC_Y = 2 + ACC_Z = 3 + ACC_TIMESTAMPS = 4 + BVP = 5 + BVP_TIMESTAMPS = 6 + EDA = 7 + EDA_TIMESTAMPS = 8 + TMP = 9 + TMP_TIMESTAMPS = 10 + TAG = 11 + TAG_TIMESTAMPS = 12 + IBI = 13 + IBI_TIMESTAMPS = 14 + BAT = 15 + BAT_TIMESTAMPS = 16 + HR = 17 + HR_TIMESTAMPS = 18 + + +date_format = "%B %d, %Y" +time_format = "%H:%M:%S" +datetime_format = date_format + time_format +eda_header = ['SCR_Peaks_N', 'SCR_Peaks_Amplitude_Mean'] +ppg_header = ['PPG_Rate_Mean', 'HRV_MeanNN', 'HRV_SDNN', 'HRV_SDANN1', 'HRV_SDNNI1', + 'HRV_SDANN2', 'HRV_SDNNI2', 'HRV_SDANN5', 'HRV_SDNNI5', 'HRV_RMSSD', 'HRV_SDSD', 'HRV_CVNN', + 'HRV_CVSD', 'HRV_MedianNN', 'HRV_MadNN', 'HRV_MCVNN', 'HRV_IQRNN', 'HRV_Prc20NN', 'HRV_Prc80NN', + 'HRV_pNN50', 'HRV_pNN20', 'HRV_MinNN', 'HRV_MaxNN', 'HRV_HTI', 'HRV_TINN', 'HRV_ULF', 'HRV_VLF', + 'HRV_LF', 'HRV_HF', 'HRV_VHF', 'HRV_LFHF', 'HRV_LFn', 'HRV_HFn', 'HRV_LnHF', 'HRV_SD1', 'HRV_SD2', + 'HRV_SD1SD2', 'HRV_S', 'HRV_CSI', 'HRV_CVI', 'HRV_CSI_Modified', 'HRV_PIP', 'HRV_IALS', 'HRV_PSS', + 'HRV_PAS', 'HRV_GI', 'HRV_SI', 'HRV_AI', 'HRV_PI', 'HRV_C1d', 'HRV_C1a', 'HRV_SD1d', 'HRV_SD1a', + 'HRV_C2d', 'HRV_C2a', 'HRV_SD2d', 'HRV_SD2a', 'HRV_Cd', 'HRV_Ca', 'HRV_SDNNd', 'HRV_SDNNa', + 'HRV_DFA_alpha1', 'HRV_MFDFA_alpha1_Width', 'HRV_MFDFA_alpha1_Peak', 'HRV_MFDFA_alpha1_Mean', + 'HRV_MFDFA_alpha1_Max', 'HRV_MFDFA_alpha1_Delta', 'HRV_MFDFA_alpha1_Asymmetry', + 'HRV_MFDFA_alpha1_Fluctuation', 'HRV_MFDFA_alpha1_Increment', 'HRV_ApEn', 'HRV_SampEn', 'HRV_ShanEn', + 'HRV_FuzzyEn', 'HRV_MSEn', 'HRV_CMSEn', 'HRV_RCMSEn', 'HRV_CD', 'HRV_HFD', 'HRV_KFD', 'HRV_LZC'] + + +def find_indices(search_list, search_item): + indices = [] + for (index, item) in enumerate(search_list): + if item in search_item: + indices.append(index) + return indices + + +def convert_legacy_events_e4(legacy_events, session_start_time): + for legacy_event in legacy_events: + if type(legacy_event[1]) is list: + event_times = legacy_event[1] + legacy_event[3] = [session_start_time + event_times[0], session_start_time + event_times[1]] + else: + event_times = legacy_event[1] + legacy_event[3] = session_start_time + event_times + + +def convert_legacy_e4_data(empatica_data): + converted_data = [[] for _ in range(19)] + for window in empatica_data: + for i in range(0, 13): + converted_data[i].extend(window[i]) + return converted_data + + +def convert_timezone(old_time_object): + new_value_timestamp = old_time_object.timestamp() + return datetime.utcfromtimestamp(new_value_timestamp) + + +def convert_timestamps(empatica_data): + empatica_data[EmpaticaData.BAT_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.BAT_TIMESTAMPS]] + empatica_data[EmpaticaData.TAG_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.TAG_TIMESTAMPS]] + empatica_data[EmpaticaData.TMP_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.TMP_TIMESTAMPS]] + empatica_data[EmpaticaData.HR_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.HR_TIMESTAMPS]] + empatica_data[EmpaticaData.IBI_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.IBI_TIMESTAMPS]] + empatica_data[EmpaticaData.ACC_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.ACC_TIMESTAMPS]] + empatica_data[EmpaticaData.BVP_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.BVP_TIMESTAMPS]] + empatica_data[EmpaticaData.EDA_TIMESTAMPS] = [int(l) for l in empatica_data[EmpaticaData.EDA_TIMESTAMPS]] + + +def export_e4_metrics(root, prim_dir, reli_dir, output_dir, time_period=20): + prim_files = glob.glob(f'{prim_dir}/**/*.json', recursive=True) + reli_files = glob.glob(f'{reli_dir}/**/*.json', recursive=True) + prim_filepaths = [_ for _ in prim_files if _.split("\\")[0]] + reli_filepaths = [_ for _ in reli_files if _.split("\\")[0]] + + popup_root = tkinter.Toplevel(root) + popup_root.config(bd=-2) + popup_root.title("Processing") + popup_root.geometry("250x100") + popup_root.config(bg="white") + center(popup_root) + label_var = tkinter.StringVar(popup_root, value=f'Processing Session 0 / {len(prim_filepaths + reli_filepaths)}') + text_label = tkinter.Label(popup_root, textvariable=label_var, font=('Purisa', 11), bg="white") + text_label.place(x=125, y=50, anchor=tkinter.CENTER) + + prim_export = os.path.join(output_dir, "Primary") + reli_export = os.path.join(output_dir, "Reliability") + + # Create directories if they don't exist + if not os.path.exists(reli_export): + os.mkdir(reli_export) + if not os.path.exists(prim_export): + os.mkdir(prim_export) + + e4_metrics_thread = threading.Thread(target=__e4_metrics_thread, args=(prim_filepaths, reli_filepaths, prim_export, + reli_export, time_period, label_var, + output_dir, popup_root)) + e4_metrics_thread.daemon = True + e4_metrics_thread.start() + + +def __e4_metrics_thread(prim_filepaths, reli_filepaths, prim_export, reli_export, time_period, label_var, output_dir, + popup_root): + e4_data_found = False + file_count = 0 + if prim_filepaths and reli_filepaths: + for file in prim_filepaths: + file_count += 1 + label_var.set(f'Processing Session {file_count} / {len(prim_filepaths + reli_filepaths)}') + e4_data_found |= process_e4_data(file, prim_export, time_period) + for file in reli_filepaths: + file_count += 1 + label_var.set(f'Processing Session {file_count} / {len(prim_filepaths + reli_filepaths)}') + e4_data_found |= process_e4_data(file, reli_export, time_period) + if e4_data_found: + messagebox.showinfo("E4 Metrics Computed", "E4 sessions have been successfully analyzed!\n" + "Check in raw data folders for output CSV files.") + os.startfile(output_dir) + else: + messagebox.showwarning("Warning", "No E4 data found in sessions!") + else: + messagebox.showwarning("Warning", "No sessions found!") + popup_root.destroy() + + +def process_e4_data(file, output_dir, time_period): + with open(file, 'r') as f: + json_file = json.load(f) + e4_data = json_file['E4 Data'] + if e4_data: + freq = json_file['KSF']['Frequency'] + freq_header = [] + for f_key in freq: + freq_header.append(f_key[1]) + dur = json_file['KSF']['Duration'] + dur_header = [] + for d_key in dur: + dur_header.append(d_key[1]) + with open(os.path.join(output_dir, f"{pathlib.Path(file).stem}_HR.csv"), 'w', + newline='') as ppg_file: + ksf_ppg_header = ['Session Time', 'E4 Time'] + freq_header + dur_header + ppg_header + ppg_f = csv.writer(ppg_file) + ppg_f.writerow([pathlib.Path(file).parts[-3]]) + ppg_f.writerow([pathlib.Path(file).parts[-2]]) + ppg_f.writerow([pathlib.Path(file).stem]) + ppg_f.writerow([str(datetime.now())]) + ppg_f.writerow(ksf_ppg_header) + + with open(os.path.join(output_dir, f"{pathlib.Path(file).stem}_EDA.csv"), 'w', + newline='') as eda_file: + ksf_eda_header = ['Session Time', 'E4 Time'] + freq_header + dur_header + eda_header + eda_f = csv.writer(eda_file) + eda_f.writerow([pathlib.Path(file).parts[-3]]) + eda_f.writerow([pathlib.Path(file).parts[-2]]) + eda_f.writerow([pathlib.Path(file).stem]) + eda_f.writerow([str(datetime.now())]) + eda_f.writerow(ksf_eda_header) + + event_history = json_file['Event History'] + if len(e4_data) > 19: + start_time_datetime = convert_timezone( + datetime.strptime(json_file['Session Date'] + json_file['Session Start Time'], + datetime_format)) + start_time = int(EmpaticaE4.get_unix_timestamp(start_time_datetime)) + end_time = int(start_time + int(json_file['Session Time'])) + e4_data = convert_legacy_e4_data(e4_data) + convert_legacy_events_e4(event_history, start_time) + else: + start_time = int(json_file['Session Start Timestamp']) + end_time = int(json_file['Session End Timestamp']) + convert_timestamps(e4_data) + for i in range(start_time + int(time_period / 2), end_time, time_period): + try: + data_time = i - int(time_period / 2) + session_time = data_time - start_time + data_range = (data_time, data_time + time_period) + timestamp_list = np.arange(*data_range) + + ppg_csv_data = [session_time, data_time] + len(freq_header) * [0] + len(dur_header) * [ + 0] + eda_csv_data = [session_time, data_time] + len(freq_header) * [0] + len(dur_header) * [ + 0] + + ppg_data_range = find_indices(e4_data[EmpaticaData.BVP_TIMESTAMPS], timestamp_list) + ppg_data = e4_data[EmpaticaData.BVP][ppg_data_range[0]:ppg_data_range[-1]] + + eda_data_range = find_indices(e4_data[EmpaticaData.EDA_TIMESTAMPS], timestamp_list) + eda_data = e4_data[EmpaticaData.EDA][eda_data_range[0]:eda_data_range[-1]] + + for event in event_history: + if type(event[1]) is list: + event_duration = np.arange(int(event[3][0]), int(event[3][1])) + if int(event[3][0]) in timestamp_list or int(event[3][1]) in timestamp_list: + ppg_csv_data[dur_header.index(event[0]) + 2 + len(freq_header)] = 1 + eda_csv_data[dur_header.index(event[0]) + 2 + len(freq_header)] = 1 + if i in event_duration: + ppg_csv_data[dur_header.index(event[0]) + 2 + len(freq_header)] = 1 + eda_csv_data[dur_header.index(event[0]) + 2 + len(freq_header)] = 1 + else: + if int(event[3]) in timestamp_list: + ppg_csv_data[freq_header.index(event[0]) + 2] = 1 + eda_csv_data[freq_header.index(event[0]) + 2] = 1 + try: + ppg_signals, _ = nk.ppg_process(ppg_data, sampling_rate=64) + ppg_results = nk.ppg_analyze(ppg_signals, sampling_rate=64) + ppg_csv_data.extend(ppg_results.values.ravel().tolist()) + except Exception as e: + print(f"INFO: Could not process PPG signal in {file}") + try: + eda_signals, _ = eda_custom_process(eda_data, sampling_rate=4) + eda_results = nk.eda_analyze(eda_signals, method='interval-related') + eda_csv_data.extend(eda_results.values.ravel().tolist()) + except Exception as e: + print(f"INFO: Could not process EDA signal in {file}") + except KeyError: + print(f"No E4 data found in {file}") + except Exception as e: + print(f"Something went wrong with {file}: {str(e)}") + finally: + eda_f.writerow(eda_csv_data) + ppg_f.writerow(ppg_csv_data) + return True + else: + return False + + +def eda_custom_process(eda_signal, sampling_rate=4, method="neurokit"): + # https://github.com/neuropsychology/NeuroKit/issues/554#issuecomment-958031898 + eda_signal = nk.signal_sanitize(eda_signal) + + # Series check for non-default index + if type(eda_signal) is pd.Series and type(eda_signal.index) != pd.RangeIndex: + eda_signal = eda_signal.reset_index(drop=True) + + # Preprocess + eda_cleaned = eda_signal # Add your custom cleaning module here or skip cleaning + eda_decomposed = nk.eda_phasic(eda_cleaned, sampling_rate=sampling_rate) + + # Find peaks + peak_signal, info = nk.eda_peaks( + eda_decomposed["EDA_Phasic"].values, + sampling_rate=sampling_rate, + method=method, + amplitude_min=0.1, + ) + info['sampling_rate'] = sampling_rate # Add sampling rate in dict info + + # Store + signals = pd.DataFrame({"EDA_Raw": eda_signal, "EDA_Clean": eda_cleaned}) + signals = pd.concat([signals, eda_decomposed, peak_signal], axis=1) + return signals, info diff --git a/ksf_utils.py b/ksf_utils.py index 35d50f8..5de8ead 100644 --- a/ksf_utils.py +++ b/ksf_utils.py @@ -341,7 +341,7 @@ def convert_json_csv(json_files, existing_files, output_dir): # Load session and split it up with open(file, 'r') as f: session = json.load(f) - session_data = {k: v for k, v in session.items() if k in list(session.keys())[:15]} + session_data = {k: v for k, v in session.items() if k in list(session.keys())[:17]} event_history = session["Event History"] updated = False for i in range(0, len(event_history)): @@ -358,7 +358,7 @@ def convert_json_csv(json_files, existing_files, output_dir): json.dump(session, f) with open(file, 'r') as f: session = json.load(f) - session_data = {k: v for k, v in session.items() if k in list(session.keys())[:15]} + session_data = {k: v for k, v in session.items() if k in list(session.keys())[:17]} event_history = session["Event History"] # TODO: Export E4 data to CSV... somehow e4_data = session["E4 Data"] diff --git a/menu_bar.py b/menu_bar.py index 37a38a8..1d454be 100644 --- a/menu_bar.py +++ b/menu_bar.py @@ -5,6 +5,7 @@ from config_utils import ConfigUtils from ksf_utils import export_columnwise_csv, populate_spreadsheet from tkinter_utils import ConfigPopup, ExternalButtonPopup +from e4_utils import export_e4_metrics class MenuBar(Frame): @@ -29,6 +30,7 @@ def __init__(self, parent, caller, *args, **kwargs): edit_menu = Menu(menu, tearoff=0) edit_menu.add_command(label="Analyze Sessions", command=self.load_sessions) edit_menu.add_command(label="Calculate Session Accuracy", command=self.get_session_acc) + edit_menu.add_command(label="Calculate E4 Metrics", command=self.get_e4_metrics) menu.add_cascade(label="Analyze", menu=edit_menu) help_menu = Menu(menu, tearoff=0) @@ -37,6 +39,9 @@ def __init__(self, parent, caller, *args, **kwargs): help_menu.add_command(label="Open Current Directory", command=self.open_current_dir) menu.add_cascade(label="Help", menu=help_menu) + def get_e4_metrics(self): + export_e4_metrics(self.caller.root, self.caller.prim_dir, self.caller.reli_dir, self.caller.export_dir) + def config_popup(self): ConfigPopup(self.parent, self.caller.config) diff --git a/output_view_ui.py b/output_view_ui.py index 987c36a..0c94fab 100644 --- a/output_view_ui.py +++ b/output_view_ui.py @@ -64,7 +64,8 @@ def __init__(self, caller, parent, x, y, height, width, button_size, ksf, self.view_buttons[self.KEY_VIEW].config(relief=SUNKEN) self.key_view = KeystrokeDataFields(self.view_frames[self.KEY_VIEW], ksf, height=self.height - self.button_size[1], width=self.width, - field_font=field_font, header_font=header_font, button_size=button_size) + field_font=field_font, header_font=header_font, button_size=button_size, + caller=caller) if self.config.get_e4(): e4_output_button = Button(self.frame, text="E4 Streams", command=self.switch_e4_frame, width=12, @@ -93,7 +94,8 @@ def __init__(self, caller, parent, x, y, height, width, button_size, ksf, self.ble_view = ViewBLE(self.view_frames[self.BLE_VIEW], height=self.height - self.button_size[1], width=self.width, field_font=field_font, header_font=header_font, button_size=button_size, - session_dir=session_dir, ble_thresh=thresholds[0:2]) + session_dir=session_dir, ble_thresh=thresholds[0:2], + ble_button=ble_output_button) ble_frame = Frame(parent, width=width, height=height) self.view_frames.append(ble_frame) else: @@ -110,7 +112,8 @@ def __init__(self, caller, parent, x, y, height, width, button_size, ksf, self.woodway_view = ViewWoodway(self.view_frames[self.WOODWAY_VIEW], height=self.height - self.button_size[1], width=self.width, field_font=field_font, header_font=header_font, button_size=button_size, - config=config, session_dir=session_dir, woodway_thresh=thresholds[2]) + config=config, session_dir=session_dir, woodway_thresh=thresholds[2], + woodway_button=woodway_output_button) woodway_frame = Frame(parent, width=width, height=height) self.view_frames.append(woodway_frame) else: @@ -126,7 +129,7 @@ def __init__(self, caller, parent, x, y, height, width, button_size, ksf, height=self.height - self.button_size[1], width=self.width, field_font=field_font, header_font=header_font, button_size=button_size, video_import_cb=video_import_cb, slider_change_cb=slider_change_cb, - fps=self.config.get_fps(), kdf=self.key_view) + fps=self.config.get_fps(), kdf=self.key_view, video_button=video_button) self.event_history = [] def switch_key_frame(self): @@ -173,7 +176,7 @@ def close(self): def start_session(self, recording_path=None): if self.e4_view: - self.e4_view.session_started = True + self.e4_view.start_session() if self.video_view.recorder: self.recording_path = recording_path audio_path = os.path.join(pathlib.Path(recording_path).parent, pathlib.Path(recording_path).stem + ".wav") @@ -220,11 +223,8 @@ def check_event(self, key_char, start_time): current_window = None # Add the frame and key to the latest E4 window reading if streaming if self.e4_view: - if self.e4_view.windowed_readings: - if current_frame: - self.e4_view.windowed_readings[-1][-1].append(current_frame) - self.e4_view.windowed_readings[-1][-2].append(key_char) - current_window = len(self.e4_view.windowed_readings) - 1 + if self.e4_view.e4: + current_window = EmpaticaE4.get_unix_timestamp() # Get the appropriate key event key_events = self.key_view.check_key(key_char, start_time, current_frame, current_window, current_audio_frame) # Add to session history @@ -248,7 +248,21 @@ def get_session_data(self): video_data = self.video_view.video_file e4_data = None if self.e4_view: - e4_data = self.e4_view.windowed_readings + if self.e4_view.e4: + e4_data = [ + self.e4_view.e4.acc_3d, + self.e4_view.e4.acc_x, + self.e4_view.e4.acc_y, + self.e4_view.e4.acc_z, + self.e4_view.e4.acc_timestamps, + self.e4_view.e4.bvp, self.e4_view.e4.bvp_timestamps, + self.e4_view.e4.gsr, self.e4_view.e4.gsr_timestamps, + self.e4_view.e4.tmp, self.e4_view.e4.tmp_timestamps, + self.e4_view.e4.tag, self.e4_view.e4.tag_timestamps, + self.e4_view.e4.ibi, self.e4_view.e4.ibi_timestamps, + self.e4_view.e4.bat, self.e4_view.e4.bat_timestamps, + self.e4_view.e4.hr, self.e4_view.e4.hr_timestamps + ] return self.key_view.event_history, e4_data, video_data def save_session(self, filename, keystrokes): @@ -274,13 +288,14 @@ def save_session(self, filename, keystrokes): class ViewWoodway: def __init__(self, parent, height, width, field_font, header_font, button_size, config, session_dir, - woodway_thresh=None): + woodway_button, woodway_thresh=None): self.woodway = None + self.tab_button = woodway_button self.session_dir = session_dir self.config = config self.root = parent self.protocol_steps = [] - self.selected_step = None + self.selected_step = 0 self.load_protocol_thread = None self.prot_file = None self.step_time = 0 @@ -290,12 +305,15 @@ def __init__(self, parent, height, width, field_font, header_font, button_size, self.session_started = False self.changed_protocol = True self.__connected = False + self.paused = False if woodway_thresh: self.calibrated = True self.woodway_thresh = woodway_thresh + print(f"INFO: Woodway calibrated already - Thresh: {self.woodway_thresh} Calibrated: {self.calibrated}") else: self.calibrated = False self.woodway_thresh = None + print("INFO: Woodway is not calibrated!") # region EXPERIMENTAL PROTOCOL element_height_adj = 100 self.exp_prot_label = Label(parent, text="Experimental Protocol", font=header_font, anchor=CENTER) @@ -306,13 +324,16 @@ def __init__(self, parent, height, width, field_font, header_font, button_size, "2": ["RS", 'c', 1, YES, 'c'], "3": ["Incline", 'c', 1, YES, 'c']} treeview_offset = int(width * 0.03) + + # TODO: When the session is paused the woodway and vibrotactors should stop, equalize speeds first?? self.prot_treeview, self.prot_filescroll = build_treeview(parent, x=treeview_offset, y=40, height=height - element_height_adj - 40, heading_dict=prot_heading_dict, column_dict=prot_column_dict, width=(int(width * 0.5) - int(width * 0.05)), button_1_bind=self.select_protocol_step, - double_bind=self.__edit_protocol_step) + double_bind=self.__edit_protocol_step, + button_3_bind=self.__delete_protocol_step) self.prot_add_button = Button(parent, text="Add", font=field_font, command=self.__add_protocol_step) self.prot_add_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.25)), y=(height - element_height_adj), @@ -417,10 +438,10 @@ def __init__(self, parent, height, width, field_font, header_font, button_size, def disable_ui_elements(self): self.__disable_ui_elements() - self.prot_add_button.config(state='disabled') - self.prot_del_button.config(state='disabled') - self.prot_save_button.config(state='disabled') - self.prot_load_button.config(state='disabled') + # self.prot_add_button.config(state='disabled') + # self.prot_del_button.config(state='disabled') + # self.prot_save_button.config(state='disabled') + # self.prot_load_button.config(state='disabled') self.calibrate_button.config(state='disabled') def __enable_connect_button(self): @@ -458,17 +479,36 @@ def stop_session(self): self.disconnect_woodway() def next_protocol_step(self, current_time): + if self.selected_step >= len(self.protocol_steps): + return if current_time == 1: self.selected_step = 0 self.__update_woodway_protocol() if (self.step_time - current_time) == 0: self.selected_step += 1 self.__update_woodway_protocol() - select_focus(self.prot_treeview, self.prot_treeview_parents[self.selected_step]) - scroll_to(self.prot_treeview, self.selected_step) + + def pause_woodway(self): + if self.session_started: + self.belt_speed_l.set(0.0) + self.belt_speed_r.set(0.0) + if self.woodway: + self.belt_speed_l_value.config(text=f"{float(0.0):.1f} MPH") + self.belt_speed_r_value.config(text=f"{float(0.0):.1f} MPH") + self.woodway.set_speed(0.0, 0.0) + self.__write_incline(0.0) + self.paused = True + + def start_woodway(self): + self.paused = False + self.__update_woodway() def __update_woodway_protocol(self): - if self.selected_step == len(self.protocol_steps): + if self.selected_step >= len(self.protocol_steps): + self.woodway_speed_l = 0.0 + self.woodway_speed_r = 0.0 + self.woodway_incline = 0.0 + self.__update_woodway() return self.selected_command = self.protocol_steps[self.selected_step] self.step_duration = self.selected_command[0] @@ -477,6 +517,8 @@ def __update_woodway_protocol(self): self.woodway_speed_r = self.woodway_thresh + self.selected_command[2] self.woodway_incline += self.selected_command[3] self.__update_woodway() + select_focus(self.prot_treeview, self.prot_treeview_parents[self.selected_step]) + scroll_to(self.prot_treeview, self.selected_step) def __update_woodway(self): self.__write_incline(self.woodway_incline) @@ -499,8 +541,10 @@ def __calibrate_woodway(self): else: messagebox.showerror("Error", "Something went wrong connecting to the Woodway!\nCannot be calibrated!") + self.tab_button['text'] = 'Woodway' + crossmark else: messagebox.showerror("Error", "Connect to Woodway first!\nCannot be calibrated!") + self.tab_button['text'] = 'Woodway' + crossmark def select_protocol_step(self, event): selection = self.prot_treeview.identify_row(event.y) @@ -519,6 +563,7 @@ def populate_protocol_steps(self): def __load_protocol_from_file(self, selected_file=None): try: if selected_file: + self.selected_step = 0 self.prot_file = selected_file with open(self.prot_file, 'r') as f: self.protocol_steps = json.load(f)['Steps'] @@ -526,6 +571,7 @@ def __load_protocol_from_file(self, selected_file=None): else: selected_file = filedialog.askopenfilename(filetypes=(("JSON Files", "*.json"),)) if selected_file: + self.selected_step = 0 self.prot_file = selected_file with open(self.prot_file, 'r') as f: self.protocol_steps = json.load(f)['Steps'] @@ -534,8 +580,10 @@ def __load_protocol_from_file(self, selected_file=None): self.prot_save_button['state'] = 'active' else: messagebox.showwarning("Warning", "No file selected, please try again!") + self.tab_button['text'] = 'Woodway' + checkmark except Exception as ex: messagebox.showerror("Exception Encountered", f"Error encountered when loading protocol file!\n{str(ex)}") + self.tab_button['text'] = 'Woodway' + crossmark def __save_protocol_to_file(self): try: @@ -558,7 +606,6 @@ def __save_protocol_to_file(self): x = {"Steps": self.protocol_steps} json.dump(x, f) self.__load_protocol_from_file(selected_file=new_file) - messagebox.showinfo("Success", "Protocol file saved!") self.changed_protocol = False self.prot_save_button['state'] = 'disabled' else: @@ -571,7 +618,6 @@ def __save_protocol_to_file(self): with open(self.prot_file, 'w') as f: x = {"Steps": self.protocol_steps} json.dump(x, f) - messagebox.showinfo("Success", "Protocol file saved!") self.changed_protocol = False self.prot_save_button['state'] = 'disabled' else: @@ -603,7 +649,7 @@ def __edit_protocol_step(self, event): def __add_protocol_step(self): AddWoodwayProtocolStep(self, self.root) - def __delete_protocol_step(self): + def __delete_protocol_step(self, event=None): if self.selected_step: self.protocol_steps.pop(self.selected_step - 1) self.repopulate_treeview() @@ -619,11 +665,14 @@ def __connect_to_woodway(self): self.__enable_ui_elements() self.__connected = True messagebox.showinfo("Success!", "Woodway Split Belt treadmill connected!") + self.tab_button['text'] = 'Woodway' + checkmark else: messagebox.showerror("Error", "No treadmills found! Check serial numbers and connections!") + self.tab_button['text'] = 'Woodway' + crossmark except Exception as ex: messagebox.showerror("Exception Encountered", f"Encountered exception when connecting to Woodway!\n{str(ex)}") + self.tab_button['text'] = 'Woodway' + crossmark def disconnect_woodway(self): if self.woodway: @@ -634,6 +683,7 @@ def disconnect_woodway(self): self.__disable_ui_elements() self.__enable_connect_button() self.__connected = False + self.tab_button['text'] = 'Woodway' def __write_speed(self): if self.session_started: @@ -667,30 +717,36 @@ def __write_incline(self, incline): class ViewBLE: - def __init__(self, parent, height, width, field_font, header_font, button_size, session_dir, ble_thresh=None): + def __init__(self, parent, height, width, field_font, header_font, button_size, + session_dir, ble_button, ble_thresh=None): self.root = parent + self.tab_button = ble_button self.session_dir = session_dir self.ble_instance = VibrotactorArray.get_ble_instance() self.left_vta, self.right_vta = None, None self.ble_connect_thread = None self.protocol_steps = [] - self.selected_step = None + self.selected_step = 0 self.prot_file = None self.step_duration = 0 self.step_time = 0 self.session_started = False self.changed_protocol = True self.__connected = False + self.paused = False self.r_ble_1_3_value, self.r_ble_4_6_value, self.r_ble_7_9_value, self.r_ble_10_12_value = 0, 0, 0, 0 self.l_ble_1_3_value, self.l_ble_4_6_value, self.l_ble_7_9_value, self.l_ble_10_12_value = 0, 0, 0, 0 if ble_thresh[0] and ble_thresh[1]: self.calibrated = True self.right_ble_thresh = ble_thresh[0] self.left_ble_thresh = ble_thresh[1] + print(f"INFO: Vibrotactors already calibrated - Right: {self.right_ble_thresh} Left: {self.left_ble_thresh} " + f"Calibrated: {self.calibrated}") else: self.calibrated = False self.right_ble_thresh = None self.left_ble_thresh = None + print("INFO: Vibrotactors not calibrated!") # region EXPERIMENTAL PROTOCOL element_height_adj = 100 self.exp_prot_label = Label(parent, text="Experimental Protocol", font=header_font, anchor=CENTER) @@ -706,7 +762,8 @@ def __init__(self, parent, height, width, field_font, header_font, button_size, column_dict=prot_column_dict, width=(int(width * 0.5) - int(width * 0.05)), button_1_bind=self.select_protocol_step, - double_bind=self.__edit_protocol_step) + double_bind=self.__edit_protocol_step, + button_3_bind=self.__delete_protocol_step) self.prot_add_button = Button(parent, text="Add", font=field_font, command=self.__add_protocol_step) self.prot_add_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.25)), @@ -821,10 +878,10 @@ def disable_ui_elements(self): for slider in self.slider_objects: slider.config(state='active') self.ble_disconnect_button.config(state='disabled') - self.prot_add_button.config(state='disabled') - self.prot_del_button.config(state='disabled') - self.prot_save_button.config(state='disabled') - self.prot_load_button.config(state='disabled') + # self.prot_add_button.config(state='disabled') + # self.prot_del_button.config(state='disabled') + # self.prot_save_button.config(state='disabled') + # self.prot_load_button.config(state='disabled') self.calibrate_button.config(state='disabled') def __enable_connect_button(self): @@ -832,6 +889,7 @@ def __enable_connect_button(self): def __disable_ui_elements(self): self.ble_disconnect_button.config(state='disabled') + self.__enable_connect_button() self.freq_slider.config(state='disabled') for slider in self.slider_objects: slider.config(state='disabled') @@ -882,6 +940,7 @@ def __calibrate_ble(self): else: messagebox.showerror("Error", "Something went wrong connecting to the vibrotactors!\nCannot be calibrated!") + self.tab_button['text'] = 'BLE Input' + crossmark else: messagebox.showerror("Error", "Connect to vibrotactors first!\nCannot be calibrated!") @@ -892,40 +951,44 @@ def __edit_protocol_step(self, event): motor_1=step[1], motor_2=step[2]) def next_protocol_step(self, current_time): + if self.selected_step >= len(self.protocol_steps): + return if current_time == 1: self.selected_step = 0 self.__update_ble_protocol() if (self.step_time - current_time) == 0: self.selected_step += 1 self.__update_ble_protocol() - select_focus(self.prot_treeview, self.prot_treeview_parents[self.selected_step]) - scroll_to(self.prot_treeview, self.selected_step) + + def pause_ble(self): + for slider in self.slider_objects: + slider.set(0.0) + self.right_vta.write_all_motors(int(0.0)) + self.left_vta.write_all_motors(int(0.0)) + self.paused = True + + def start_ble(self): + self.paused = False + self.__update_ble() def __update_ble_protocol(self): - if self.selected_step + 1 == len(self.protocol_steps): + if self.selected_step >= len(self.protocol_steps): + self.r_ble_1_3_value = 0 + self.l_ble_1_3_value = 0 + self.__update_ble() return self.selected_command = self.protocol_steps[self.selected_step] self.step_duration = self.selected_command[0] self.step_time += self.step_duration self.r_ble_1_3_value = (self.selected_command[1] / 100) * self.right_ble_thresh - # self.r_ble_4_6_value = self.selected_command[2] - # self.r_ble_7_9_value = self.selected_command[3] - # self.r_ble_10_12_value = self.selected_command[4] self.l_ble_1_3_value = (self.selected_command[2] / 100) * self.left_ble_thresh - # self.l_ble_4_6_value = self.selected_command[2] - # self.l_ble_7_9_value = self.selected_command[3] - # self.l_ble_10_12_value = self.selected_command[4] self.__update_ble() + select_focus(self.prot_treeview, self.prot_treeview_parents[self.selected_step]) + scroll_to(self.prot_treeview, self.selected_step) def __update_ble(self): for slider in self.slider_objects: slider.set(self.l_ble_1_3_value) - # for i in range(3, 6): - # self.slider_objects[i].set(self.l_ble_4_6_value) - # for i in range(6, 9): - # self.slider_objects[i].set(self.l_ble_7_9_value) - # for i in range(9, 12): - # self.slider_objects[i].set(self.l_ble_10_12_value) self.right_vta.write_all_motors(int(self.r_ble_1_3_value)) self.left_vta.write_all_motors(int(self.l_ble_1_3_value)) @@ -945,6 +1008,7 @@ def populate_protocol_steps(self): def __load_protocol_from_file(self, selected_file=None): try: if selected_file: + self.selected_step = 0 self.prot_file = selected_file with open(self.prot_file, 'r') as f: self.protocol_steps = json.load(f)['Steps'] @@ -952,6 +1016,7 @@ def __load_protocol_from_file(self, selected_file=None): else: selected_file = filedialog.askopenfilename(filetypes=(("JSON Files", "*.json"),)) if selected_file: + self.selected_step = 0 self.prot_file = selected_file with open(self.prot_file, 'r') as f: self.protocol_steps = json.load(f)['Steps'] @@ -962,6 +1027,7 @@ def __load_protocol_from_file(self, selected_file=None): messagebox.showwarning("Warning", "No file selected, please try again!") except Exception as ex: messagebox.showerror("Exception Encountered", f"Error encountered when loading protocol file!\n{str(ex)}") + self.tab_button['text'] = 'BLE Input' + crossmark def __load_protocol(self, file): self.prot_file = file @@ -986,7 +1052,7 @@ def repopulate_treeview(self): def __add_protocol_step(self): AddBleProtocolStep(self, self.root) - def __delete_protocol_step(self): + def __delete_protocol_step(self, event=None): if self.selected_step: self.protocol_steps.pop(self.selected_step - 1) self.repopulate_treeview() @@ -1014,7 +1080,6 @@ def __save_protocol_to_file(self): x = {"Steps": self.protocol_steps} json.dump(x, f) self.__load_protocol_from_file(selected_file=new_file) - messagebox.showinfo("Success", "Protocol file saved!") self.changed_protocol = False self.prot_save_button['state'] = 'disabled' else: @@ -1027,18 +1092,19 @@ def __save_protocol_to_file(self): with open(self.prot_file, 'w') as f: x = {"Steps": self.protocol_steps} json.dump(x, f) - messagebox.showinfo("Success", "Protocol file saved!") self.changed_protocol = False self.prot_save_button['state'] = 'disabled' else: messagebox.showwarning("Warning", "No filename supplied! Can't save, please try again!") except Exception as ex: messagebox.showerror("Exception Encountered", f"Error encountered when saving protocol file!\n{str(ex)}") + self.tab_button['text'] = 'BLE Input' + crossmark def disconnect_ble(self): VibrotactorArray.disconnect_ble_devices(self.ble_instance) self.__disable_ui_elements() self.__connected = False + self.tab_button['text'] = 'BLE Input' def __connect_to_ble(self): self.ble_connect_thread = threading.Thread(target=self.__connect_ble_thread) @@ -1048,20 +1114,21 @@ def __connect_to_ble(self): def __connect_ble_thread(self): while True: try: - self.left_vta = VibrotactorArray(self.ble_instance) - self.right_vta = VibrotactorArray(self.ble_instance) - if self.left_vta.is_connected() and self.left_vta.is_connected(): - if self.left_vta.get_side() != VibrotactorArraySide.LEFT: - vta = self.left_vta - self.left_vta = self.right_vta - self.right_vta = vta + left_vta = VibrotactorArray(self.ble_instance) + right_vta = VibrotactorArray(self.ble_instance) + if left_vta.is_connected() and left_vta.is_connected(): + print(f"INFO: VTA Left - {left_vta.get_side()} VTA Right - {right_vta.get_side()}") + if left_vta.get_side() != VibrotactorArraySide.LEFT: + self.left_vta = right_vta + self.right_vta = left_vta else: - vta = self.right_vta - self.right_vta = self.left_vta - self.left_vta = vta + self.left_vta = left_vta + self.right_vta = right_vta + print(f"INFO: VTA Left - {self.left_vta.get_side()} VTA Right - {self.right_vta.get_side()}") self.__enable_ui_elements() messagebox.showinfo("Success!", "Vibrotactor arrays are connected!") self.__connected = True + self.tab_button['text'] = 'BLE Input' + checkmark break else: response = messagebox.askyesno("Error", "Could not connect to both vibrotactor arrays!\nTry again?") @@ -1069,6 +1136,7 @@ def __connect_ble_thread(self): break except Exception as ex: messagebox.showerror("Error", f"Exception encountered:\n{str(ex)}") + self.tab_button['text'] = 'BLE Input' + crossmark def update_frequency(self, value): if self.right_vta and self.left_vta: @@ -1137,9 +1205,10 @@ def update_ble_12(self, value): class ViewVideo: - def __init__(self, caller, root, height, width, field_font, header_font, button_size, fps, kdf, field_offset=60, + def __init__(self, caller, root, height, width, field_font, header_font, button_size, fps, kdf, video_button, field_offset=60, video_import_cb=None, slider_change_cb=None): self.recording_fps = fps + self.tab_button = video_button self.kdf = kdf self.caller = caller self.height, self.width = height, width @@ -1371,9 +1440,11 @@ def load_camera(self): keep_ratio=True) self.video_loaded = True self.recorder.start_playback() + self.tab_button['text'] = 'Video View' + checkmark except Exception as e: messagebox.showerror("Error", f"Error loading camera:\n{str(e)}") print(f"ERROR: Error loading camera:\n{str(e)}\n" + traceback.print_exc()) + self.tab_button['text'] = 'Video View' + crossmark def set_clip(self, start_frame, end_frame): if self.player: @@ -1447,9 +1518,11 @@ def load_video(self, ask=True, video_filepath=None): print( f"INFO: ({self.video_width}, {self.video_height}) {self.player.size} {self.player.aspect_ratio}") self.video_loaded = True + self.tab_button['text'] = 'Video View' + checkmark except Exception as e: messagebox.showerror("Error", f"Error loading video:\n{str(e)}") print(f"ERROR: Error loading video:\n{str(e)}\n" + traceback.print_exc()) + self.tab_button['text'] = 'Video View' + crossmark def pause_video(self): try: @@ -1462,6 +1535,7 @@ def pause_video(self): return False except Exception as e: print(f"ERROR: Error starting video:\n{str(e)}\n{traceback.print_exc()}") + self.tab_button['text'] = 'Video View' + crossmark def play_video(self, video_output=None, audio_output=None): try: @@ -1476,6 +1550,7 @@ def play_video(self, video_output=None, audio_output=None): return False except Exception as e: print(f"ERROR: Error starting video:\n{str(e)}\n{traceback.print_exc()}") + self.tab_button['text'] = 'Video View' + crossmark def toggle_video(self): try: @@ -1486,6 +1561,7 @@ def toggle_video(self): return False except Exception as e: print(f"ERROR: Error starting video:\n{str(e)}\n{traceback.print_exc()}") + self.tab_button['text'] = 'Video View' + crossmark class ViewE4: @@ -1765,12 +1841,18 @@ def save_session(self, filename): if self.e4_client.connected: self.e4_client.save_readings(filename) + def start_session(self): + self.session_started = True + if self.e4: + self.e4.clear_readings() + def stop_plot(self): self.streaming = False self.kill = True def start_plot(self, e4): self.e4 = e4 + self.windowed_readings = self.e4.windowed_readings if not self.streaming: self.streaming = True self.update_thread.start() @@ -1819,23 +1901,6 @@ def acc_animate(self, e4): if self.streaming: if self.e4: if self.e4.connected: - if self.session_started: - if self.save_reading: - self.save_reading = False - self.windowed_readings.append( - (self.e4.acc_3d[-(32 * 3):], - self.e4.acc_x[-32:], self.e4.acc_y[-32:], self.e4.acc_z[-32:], - self.e4.acc_timestamps[-32:], - self.e4.bvp[-64:], self.e4.bvp_timestamps[-64:], - self.e4.gsr[-4:], self.e4.gsr_timestamps[-4:], - self.e4.tmp[-4:], self.e4.tmp_timestamps[-4:], - # Key tag - [], - # Frame index - []) - ) - else: - self.save_reading = True if self.root.winfo_viewable(): # Limit x and y lists to 20 items x_ys = self.e4.acc_x[-100:] @@ -1891,7 +1956,7 @@ def gsr_animate(self, e4): class KeystrokeDataFields: def __init__(self, parent, keystroke_file, height, width, - field_font, header_font, button_size): + field_font, header_font, button_size, caller): # TODO: Add editing of event history by double clicking event separation_distance = 30 fs_offset = 10 + ((width * 0.25) * 0.5) @@ -1903,6 +1968,7 @@ def __init__(self, parent, keystroke_file, height, width, self.height, self.width = height, width self.frame = parent + self.caller = caller keystroke_label = Label(self.frame, text="Frequency Bindings", font=header_font) keystroke_label.place(x=(width * 0.25) - 30, y=start_y, anchor=CENTER) @@ -2053,6 +2119,7 @@ def check_key(self, key_char, start_time, current_frame, current_window, current if self.dur_bindings[i][0] == key_char: if self.dur_sticky[i]: self.dur_treeview.item(str(i), tags=treeview_bind_tags[i % 2]) + self.caller.pdf.hide_dur_key(i) self.dur_sticky[i] = False duration = [self.sticky_start[i][0], start_time] frame = [self.sticky_start[i][1], current_frame] @@ -2065,6 +2132,7 @@ def check_key(self, key_char, start_time, current_frame, current_window, current self.dur_treeview.set(str(i), column="2", value=self.sticky_dur[i]) else: self.dur_treeview.item(str(i), tags=treeview_bind_tags[2]) + self.caller.pdf.show_dur_key(i) self.dur_sticky[i] = True self.sticky_start[i] = (start_time, current_frame, current_window, current_audio_frame) if return_bindings: diff --git a/patient_data_fields.py b/patient_data_fields.py index deb13de..de8b6dd 100644 --- a/patient_data_fields.py +++ b/patient_data_fields.py @@ -8,7 +8,7 @@ import re from ksf_utils import open_keystroke_file from tkinter_utils import build_treeview -from ui_params import treeview_bind_tags +from ui_params import treeview_bind_tags, treeview_bind_tag_dict class PatientDataVar: @@ -216,6 +216,7 @@ def __init__(self, parent, x, y, height, width, patient_file, prim_session_numbe column_dict=dur_column_dict, heading_dict=dur_heading_dict, anchor=N, + tag_dict=treeview_bind_tag_dict, fs_offset=(width / 2) - 7) for i in range(0, len(self.bindings)): bind = self.bindings[i] @@ -234,6 +235,12 @@ def __init__(self, parent, x, y, height, width, patient_file, prim_session_numbe self.patient_vars[PatientDataVar.DATA_REC].set("Debug") self.patient_vars[PatientDataVar.PRIM_THER].set("Debug") + def show_dur_key(self, i): + self.bind_treeview.item(str(len(self.freq_bindings) + i), tags=treeview_bind_tags[2]) + + def hide_dur_key(self, i): + self.bind_treeview.item(str(len(self.freq_bindings) + i), tags=treeview_bind_tags[(len(self.freq_bindings) + i) % 2]) + def check_load_session(self, *args): session_number = self.patient_vars[PatientDataVar.SESS_NUM].get() if bool(re.match('^[0-9]+$', session_number)): diff --git a/reference/Cometrics User Guide.pdf b/reference/Cometrics User Guide.pdf index adb49d7..5344a84 100644 Binary files a/reference/Cometrics User Guide.pdf and b/reference/Cometrics User Guide.pdf differ diff --git a/requirements.txt b/requirements.txt index abc2f4a..4d0d248 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,11 +6,13 @@ pillow~=9.0.1 numpy~=1.22.2 WMI~=1.5.1 PyYAML~=6.0 -pyEmpatica~=0.5.7 +pyEmpatica==0.6.0 openpyxl~=3.0.9 matplotlib==3.4.3 logger-util~=0.2 future~=0.18.2 ttkwidgets~=0.12.1 pyWoodway~=0.2.4 -pyTactor~=0.1.4 \ No newline at end of file +pyTactor~=0.1.4 +neurokit2~=0.2.0 +pandas~=1.4.3 \ No newline at end of file diff --git a/session_manager_ui.py b/session_manager_ui.py index 35988a3..c5cde7d 100644 --- a/session_manager_ui.py +++ b/session_manager_ui.py @@ -7,13 +7,14 @@ from tkinter import messagebox from PIL import Image, ImageTk +from pyempatica import EmpaticaE4 from pynput import keyboard # Custom library imports from tkvideoutils import cp_rename from menu_bar import MenuBar from output_view_ui import OutputViewPanel -from patient_data_fields import PatientDataFields, PatientDataVar +from patient_data_fields import PatientDataFields, PatientDataVar, PatientContainer from session_time_fields import SessionTimeFields from tkinter_utils import get_treeview_style, get_slider_style from ui_params import large_header_font, large_field_font, large_field_offset, medium_header_font, medium_field_font, \ @@ -30,7 +31,7 @@ def __init__(self, config, project_setup): self.button_input_handler = None self.ext_raw, self.ext_dur_val, self.ext_freq_val = None, None, None self.patient_file = project_setup.patient_data_file - self.patient_container = project_setup.patient_container + self.patient_container = PatientContainer(project_setup.patient_data_file) self.keystroke_file = project_setup.ksf_file self.session_dir = project_setup.phase_dir self.tracker_file = project_setup.tracker_file @@ -46,10 +47,10 @@ def __init__(self, config, project_setup): # Log this for debugging print("INFO:", self.patient_file, self.keystroke_file, self.session_dir, self.prim_dir, self.reli_dir) # Generate session date and time - now = datetime.datetime.today() + self.now = now = datetime.datetime.today() self.session_date = now.strftime("%B %d, %Y") self.session_file_date = now.strftime("%B")[:3] + now.strftime("%d") + now.strftime("%Y") - self.session_time = datetime.datetime.now().strftime("%H:%M:%S") + self.session_time = now.strftime("%H:%M:%S") # Get the number of primary and reliability sessions collected so far self.prim_session_number = 1 self.reli_session_number = 1 @@ -109,6 +110,7 @@ def __init__(self, config, project_setup): thresholds = [self.patient_container.right_ble_thresh, self.patient_container.left_ble_thresh, self.patient_container.woodway_thresh] + print(f"INFO: Thresholds {thresholds}") self.ovu = OutputViewPanel(self, root, x=(self.logo_width * 2) + 30, y=(self.logo_height + 10) - self.button_size[1], @@ -258,8 +260,6 @@ def on_press(self, key): # Enforce lower case for all inputs that are characters key_char = str(key_char).lower() self.handle_key_press(key_char) - else: - print("INFO: Typing outside window") except AttributeError: try: # Only process key input if the main window has focus, otherwise ignore @@ -314,6 +314,8 @@ def save_session(self): x = { "Session Date": self.session_date, "Session Start Time": self.session_time, + "Session Start Timestamp": EmpaticaE4.get_unix_timestamp(self.now), + "Session End Timestamp": EmpaticaE4.get_unix_timestamp(), "Session Time": self.stf.session_time, "Pause Time": self.stf.break_time, "Keystroke File": pathlib.Path(self.keystroke_file).stem, @@ -395,7 +397,11 @@ def start_session(self): "Woodway view is not present when it should be!") print("ERROR: Something went wrong with starting session, Woodway view is not present when it should be") return - self.session_time = datetime.datetime.now().strftime("%H:%M:%S") + # self.session_time = datetime.datetime.now().strftime("%H:%M:%S") + self.now = now = datetime.datetime.today() + self.session_date = now.strftime("%B %d, %Y") + self.session_file_date = now.strftime("%B")[:3] + now.strftime("%d") + now.strftime("%Y") + self.session_time = now.strftime("%H:%M:%S") self.pdf.start_label['text'] = "Session Start Time: " + self.session_time self.pdf.save_patient_fields(ble_thresh_r, ble_thresh_l, woodway_thresh) self.pdf.lock_session_fields() diff --git a/session_time_fields.py b/session_time_fields.py index 8f14df3..c353551 100644 --- a/session_time_fields.py +++ b/session_time_fields.py @@ -247,11 +247,21 @@ def time_update_thread(self): if self.session_time >= self.session_duration: self.caller.stop_session() if self.ovu.woodway_view: + if self.ovu.woodway_view.paused: + self.ovu.woodway_view.start_woodway() self.ovu.woodway_view.next_protocol_step(self.session_time) if self.ovu.ble_view: + if self.ovu.ble_view.paused: + self.ovu.ble_view.start_ble() self.ovu.ble_view.next_protocol_step(self.session_time) elif self.session_paused: - if not self.caller.ovu.video_view.player: + if self.ovu.ble_view: + if not self.ovu.ble_view.paused: + self.ovu.ble_view.pause_ble() + if self.ovu.woodway_view: + if not self.ovu.woodway_view.paused: + self.ovu.woodway_view.pause_woodway() + if not self.ovu.video_view.player: self.break_time += 1 self.break_time_label['text'] = str(datetime.timedelta(seconds=self.break_time)) self.session_time_label['text'] = str(datetime.timedelta(seconds=self.session_time)) diff --git a/ui_params.py b/ui_params.py index 4627dee..4efeefd 100644 --- a/ui_params.py +++ b/ui_params.py @@ -1,4 +1,4 @@ -cometrics_version = "1.2.1" +cometrics_version = "1.2.7" ui_title = f"cometrics v{cometrics_version}" cometrics_data_root = fr'C:\cometrics' diff --git a/user_guide/Cometrics User Guide_v6.docx b/user_guide/Cometrics User Guide_v7.docx similarity index 89% rename from user_guide/Cometrics User Guide_v6.docx rename to user_guide/Cometrics User Guide_v7.docx index c099243..2a6ef03 100644 Binary files a/user_guide/Cometrics User Guide_v6.docx and b/user_guide/Cometrics User Guide_v7.docx differ diff --git a/user_guide/Cometrics User Guide_v7.pdf b/user_guide/Cometrics User Guide_v7.pdf new file mode 100644 index 0000000..5344a84 Binary files /dev/null and b/user_guide/Cometrics User Guide_v7.pdf differ