diff --git a/cometrics.py b/cometrics.py index 9facb6b..3a35ba3 100644 --- a/cometrics.py +++ b/cometrics.py @@ -1,7 +1,11 @@ import datetime import os import sys +import gc # Custom library imports +import traceback +from tkinter import messagebox + import imageio_ffmpeg from config_utils import ConfigUtils @@ -37,16 +41,23 @@ def main(config_file, first_time_user): config = ConfigUtils() config.set_cwd(cwd) config.set_logs_dir(os.path.join(cometrics_ver_root, 'logs')) + if not os.path.exists(f"{cometrics_ver_root}/Projects"): + os.mkdir(f"{cometrics_ver_root}/Projects") while True: first_time = config.get_first_time() setup = ProjectSetupWindow(config, first_time) if setup.setup_complete: while True: - manager = SessionManagerWindow(config, setup) - if manager.setup_again: - break - elif manager.close_program: - break + try: + manager = SessionManagerWindow(config, setup) + if manager.setup_again: + break + elif manager.close_program: + break + del manager + gc.collect() + except Exception as e: + messagebox.showerror("Error", f"Exception encountered:\n{str(e)}\n{traceback.print_exc()}") if manager.close_program: break else: diff --git a/config.yml b/config.yml index b35055e..a2f0e62 100644 --- a/config.yml +++ b/config.yml @@ -16,3 +16,5 @@ phases: - Treatment recent-projects: [] window-size: [] +woodway-a-sn: FTHCUWVAA +woodway-b-sn: FTHCUQ9IA diff --git a/config_utils.py b/config_utils.py index 46f1f50..9b2e47a 100644 --- a/config_utils.py +++ b/config_utils.py @@ -113,3 +113,21 @@ def set_ble(self, set_ble): if self.config: self.config['enable-ble'] = set_ble self.save_config() + + def get_woodway_a(self): + if self.config: + return str(self.config['woodway-a-sn']) + + def set_woodway_a(self, woodway_sn): + if self.config: + self.config['woodway-a-sn'] = woodway_sn + self.save_config() + + def get_woodway_b(self): + if self.config: + return str(self.config['woodway-b-sn']) + + def set_woodway_b(self, woodway_sn): + if self.config: + self.config['woodway-b-sn'] = woodway_sn + self.save_config() diff --git a/gui_images/bugsnag.png b/gui_images/bugsnag.png deleted file mode 100644 index fd6a9a3..0000000 Binary files a/gui_images/bugsnag.png and /dev/null differ diff --git a/gui_images/condition_dropdown.jpg b/gui_images/condition_dropdown.jpg index b8a7008..247bda4 100644 Binary files a/gui_images/condition_dropdown.jpg and b/gui_images/condition_dropdown.jpg differ diff --git a/gui_images/config_settings.jpg b/gui_images/config_settings.jpg new file mode 100644 index 0000000..90d083a Binary files /dev/null and b/gui_images/config_settings.jpg differ diff --git a/gui_images/e4_data.jpg b/gui_images/e4_data.jpg index 01b0a6d..ef0fa8b 100644 Binary files a/gui_images/e4_data.jpg and b/gui_images/e4_data.jpg differ diff --git a/gui_images/event_viewer.png b/gui_images/event_viewer.png new file mode 100644 index 0000000..c12ffb3 Binary files /dev/null and b/gui_images/event_viewer.png differ diff --git a/gui_images/loaded_video.jpg b/gui_images/loaded_video.jpg deleted file mode 100644 index f292392..0000000 Binary files a/gui_images/loaded_video.jpg and /dev/null differ diff --git a/gui_images/pdf_page_2.jpg b/gui_images/pdf_page_2.jpg index 019b114..bb3014a 100644 Binary files a/gui_images/pdf_page_2.jpg and b/gui_images/pdf_page_2.jpg differ diff --git a/gui_images/pdf_panel_1.jpg b/gui_images/pdf_panel_1.jpg new file mode 100644 index 0000000..668df6c Binary files /dev/null and b/gui_images/pdf_panel_1.jpg differ diff --git a/gui_images/pdf_panel_2.jpg b/gui_images/pdf_panel_2.jpg new file mode 100644 index 0000000..402ad3d Binary files /dev/null and b/gui_images/pdf_panel_2.jpg differ diff --git a/gui_images/pdf_panel_3.jpg b/gui_images/pdf_panel_3.jpg new file mode 100644 index 0000000..2e72356 Binary files /dev/null and b/gui_images/pdf_panel_3.jpg differ diff --git a/gui_images/pdf_panels.jpg b/gui_images/pdf_panels.jpg new file mode 100644 index 0000000..eac5561 Binary files /dev/null and b/gui_images/pdf_panels.jpg differ diff --git a/gui_images/pdf_populated.jpg b/gui_images/pdf_populated.jpg deleted file mode 100644 index 91e59e2..0000000 Binary files a/gui_images/pdf_populated.jpg and /dev/null differ diff --git a/gui_images/project_name.jpg b/gui_images/project_name.jpg deleted file mode 100644 index 994a1e1..0000000 Binary files a/gui_images/project_name.jpg and /dev/null differ diff --git a/gui_images/session_events.jpg b/gui_images/session_events.jpg deleted file mode 100644 index 883c378..0000000 Binary files a/gui_images/session_events.jpg and /dev/null differ diff --git a/gui_images/session_started.jpg b/gui_images/session_started.jpg deleted file mode 100644 index 8f24f23..0000000 Binary files a/gui_images/session_started.jpg and /dev/null differ diff --git a/gui_images/session_window.jpg b/gui_images/session_window.jpg new file mode 100644 index 0000000..a188120 Binary files /dev/null and b/gui_images/session_window.jpg differ diff --git a/images/folder.png b/images/folder.png new file mode 100644 index 0000000..48733c8 Binary files /dev/null and b/images/folder.png differ diff --git a/ksf_utils.py b/ksf_utils.py index bf1cf15..b36d022 100644 --- a/ksf_utils.py +++ b/ksf_utils.py @@ -103,7 +103,7 @@ def cal_acc(prim_filename, reli_filename, window_size, output_dir): dur_bindings = [] for dur in dur_b: dur_bindings.append(dur[1]) - prim_num, reli_num = int(prim_session["Session Number"]), int(reli_session["Session Number"]) + prim_num, reli_num = prim_session["Session Number"], reli_session["Session Number"] warning = "" # Perform error checking before causing errors if prim_num != reli_num: diff --git a/output_view_ui.py b/output_view_ui.py index 4f743e0..3c86310 100644 --- a/output_view_ui.py +++ b/output_view_ui.py @@ -1,13 +1,14 @@ +import glob import json +import os import pathlib import pickle import threading import time import traceback from tkinter import * -from tkinter import filedialog, messagebox, ttk +from tkinter import filedialog, messagebox from tkinter.ttk import Combobox - import matplotlib.animation as animation import matplotlib.pyplot as plt import numpy as np @@ -15,17 +16,20 @@ from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg) # Implement the default Matplotlib key bindings. from matplotlib.figure import Figure -from pyempatica.empaticae4 import EmpaticaE4, EmpaticaDataStreams, EmpaticaClient, EmpaticaServerConnectError # Custom library imports from ttkwidgets import TickScale - -from tkinter_utils import build_treeview, clear_treeview +from pywoodway.treadmill import SplitBelt, find_treadmills +from tkinter_utils import build_treeview, clear_treeview, AddWoodwayProtocolStep, AddBleProtocolStep, \ + CalibrateVibrotactors, CalibrateWoodway from ui_params import treeview_bind_tag_dict, treeview_tags, treeview_bind_tags, crossmark, checkmark +from pytactor import VibrotactorArray, VibrotactorArraySide +from pyempatica.empaticae4 import EmpaticaE4, EmpaticaDataStreams, EmpaticaClient, EmpaticaServerConnectError class OutputViewPanel: def __init__(self, parent, x, y, height, width, button_size, ksf, - field_font, header_font, video_import_cb, slider_change_cb, config): + field_font, header_font, video_import_cb, slider_change_cb, config, session_dir, + thresholds): self.KEY_VIEW, self.E4_VIEW, self.VIDEO_VIEW, self.WOODWAY_VIEW, self.BLE_VIEW = 0, 1, 2, 3, 4 self.config = config self.height, self.width = height, width @@ -33,6 +37,7 @@ def __init__(self, parent, x, y, height, width, button_size, ksf, self.current_button = 0 self.view_buttons = [] self.view_frames = [] + self.time_change_sources = False self.frame = Frame(parent, width=width, height=height) self.frame.place(x=x, y=y) @@ -76,26 +81,34 @@ def __init__(self, parent, x, y, height, width, button_size, ksf, self.e4_view = None if self.config.get_ble(): + self.time_change_sources = False ble_output_button = Button(self.frame, text="BLE Input", command=self.switch_ble_frame, width=12, font=field_font) self.view_buttons.append(ble_output_button) self.BLE_VIEW = len(self.view_buttons) - 1 self.view_buttons[self.BLE_VIEW].place(x=(len(self.view_buttons) - 1) * button_size[0], y=0, width=button_size[0], height=button_size[1]) - self.ble_view = ViewBLE() + 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]) ble_frame = Frame(parent, width=width, height=height) self.view_frames.append(ble_frame) else: self.ble_view = None if self.config.get_woodway(): + self.time_change_sources = False woodway_output_button = Button(self.frame, text="Woodway", command=self.switch_woodway_frame, width=12, font=field_font) self.view_buttons.append(woodway_output_button) self.WOODWAY_VIEW = len(self.view_buttons) - 1 self.view_buttons[self.WOODWAY_VIEW].place(x=(len(self.view_buttons) - 1) * button_size[0], y=0, width=button_size[0], height=button_size[1]) - self.woodway_view = ViewWoodway(self.view_frames[self.WOODWAY_VIEW]) + 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]) woodway_frame = Frame(parent, width=width, height=height) self.view_frames.append(woodway_frame) else: @@ -156,6 +169,10 @@ def start_session(self, recording_path=None): self.e4_view.session_started = True if self.video_view.recorder: self.video_view.recorder.start_recording(output_path=recording_path) + if self.ble_view: + self.ble_view.start_session() + if self.woodway_view: + self.woodway_view.start_session() def enable_video_slider(self): if self.video_view.player: @@ -172,6 +189,10 @@ def stop_session(self): if self.video_view.recorder: self.video_view.recorder.stop_recording() self.video_view.recorder.stop_playback() + if self.ble_view: + self.ble_view.stop_session() + if self.woodway_view: + self.woodway_view.stop_session() def check_event(self, key_char, start_time): # Make sure it is not None @@ -238,47 +259,815 @@ def save_session(self, filename, keystrokes): class ViewWoodway: - def __init__(self, parent): + def __init__(self, parent, height, width, field_font, header_font, button_size, config, session_dir, + woodway_thresh=None): + self.woodway = None + self.session_dir = session_dir + self.config = config + self.root = parent + self.protocol_steps = [] + self.selected_step = None + self.load_protocol_thread = None + self.prot_file = None + self.step_time = 0 + self.step_duration = 0 + self.woodway_speed_r, self.woodway_speed_l = 0, 0 + self.woodway_incline = 0 + self.session_started = False + self.changed_protocol = True + if woodway_thresh: + self.calibrated = True + self.woodway_thresh = woodway_thresh + else: + self.calibrated = False + self.woodway_thresh = None + # region EXPERIMENTAL PROTOCOL + element_height_adj = 100 + self.exp_prot_label = Label(parent, text="Experimental Protocol", font=header_font, anchor=CENTER) + self.exp_prot_label.place(x=int(width * 0.23) + 18, y=10, anchor=N) + self.prot_treeview_parents = [] + prot_heading_dict = {"#0": ["Duration", 'w', 20, YES, 'w']} + prot_column_dict = {"1": ["LS", 'c', 1, YES, 'c'], + "2": ["RS", 'c', 1, YES, 'c'], + "3": ["Incline", 'c', 1, YES, 'c']} + treeview_offset = int(width * 0.03) + 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) + 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), + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.woodway_connect_button = Button(parent, text="Connect", font=field_font, + command=self.__connect_to_woodway, bg='#4abb5f') + self.woodway_connect_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.25)), + y=(height - element_height_adj) + button_size[1] * 2, + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.prot_load_button = Button(parent, text="Load File", font=field_font, + command=self.__load_protocol_from_file) + self.prot_load_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.25)), + y=(height - element_height_adj) + button_size[1], + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.prot_save_button = Button(parent, text="Save To File", font=field_font, + command=self.__save_protocol_to_file) + self.prot_save_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.75)), + y=(height - element_height_adj) + button_size[1], + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + self.prot_save_button['state'] = 'disabled' + + self.prot_del_button = Button(parent, text="Delete", font=field_font, + command=self.__delete_protocol_step) + self.prot_del_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.75)), + y=(height - element_height_adj), + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.woodway_disconnect_button = Button(parent, text="Disconnect", font=field_font, + command=self.disconnect_woodway, bg='red') + self.woodway_disconnect_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.75)), + y=(height - element_height_adj) + button_size[1] * 2, + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + # endregion + + # region BELT CONTROL # Vertical sliders speed and inclination for each treadmill - self.belt_speed_l_var = IntVar(parent) - self.belt_speed_l = Scale(parent, orient="vertical", variable=self.belt_speed_l_var, - command=self.__write_l_speed) - self.belt_incline_l_var = IntVar(parent) - self.belt_incline_l = Scale(parent, orient="vertical", variable=self.belt_incline_l_var, - command=self.__write_l_incline) - self.belt_speed_r_var = IntVar(parent) - self.belt_speed_r = Scale(parent, orient="vertical", variable=self.belt_speed_r_var, - command=self.__write_r_speed) - self.belt_incline_r_var = IntVar(parent) - self.belt_incline_r = Scale(parent, orient="vertical", variable=self.belt_incline_r_var, - command=self.__write_r_incline) - # Treeview for the changes to treadmill operation with timing delays between each step, import export json - # Connect button and assignment of COM ports to each treadmill - # Start treadmill button? - pass + slider_height_adj = element_height_adj + self.belt_speed_label = Label(parent, text="Belt Speeds", font=header_font, anchor=CENTER) + self.belt_speed_label.place(x=int(width * 0.625), y=10, anchor=N) + + self.belt_speed_l_label = Label(parent, text='Left', font=field_font, anchor=CENTER) + self.belt_speed_l_label.place(x=int(width * 0.575), y=40, anchor=N) + + self.belt_speed_r_label = Label(parent, text='Right', font=field_font, anchor=CENTER) + self.belt_speed_r_label.place(x=int(width * 0.675), y=40, anchor=N) + + self.belt_speed_l_var = StringVar(parent) + self.belt_speed_l = Scale(parent, orient="vertical", variable=self.belt_speed_l_var, showvalue=False, + command=self.__write_l_speed, length=int(height * 0.7), from_=29.9, to=-29.9, + digits=3, resolution=0.1) + self.belt_speed_l.place(x=int(width * 0.575), y=70, anchor=N) + + self.belt_speed_l_value = Label(parent, text="0 MPH", anchor=CENTER, font=field_font) + self.belt_speed_l_value.place(x=int(width * 0.575), y=80 + int(height * 0.7), anchor=N) + + self.belt_speed_r_var = StringVar(parent) + self.belt_speed_r = Scale(parent, orient="vertical", variable=self.belt_speed_r_var, showvalue=False, + command=self.__write_r_speed, length=int(height * 0.7), from_=29.9, to=-29.9, + digits=3, resolution=0.1) + self.belt_speed_r.place(x=int(width * 0.675), y=70, anchor=N) + self.belt_speed_r_value = Label(parent, text="0 MPH", anchor=CENTER, font=field_font) + self.belt_speed_r_value.place(x=int(width * 0.675), y=80 + int(height * 0.7), anchor=N) + + # This section gets 25% of the panel + self.belt_incline_label = Label(parent, text="Belt Incline", font=header_font, anchor=CENTER) + self.belt_incline_label.place(x=int(width * 0.875), y=10, anchor=N) + + self.belt_incline_l_var = StringVar(parent) + self.belt_incline_l = Scale(parent, orient="vertical", variable=self.belt_incline_l_var, showvalue=False, + command=self.__write_incline, length=int(height * 0.7), from_=29.9, to=0, + digits=3, resolution=0.1) + self.belt_incline_l.place(x=int(width * 0.875), y=70, anchor=N) + + self.belt_incline_l_value = Label(parent, text="0\u00b0", anchor=CENTER, font=field_font) + self.belt_incline_l_value.place(x=int(width * 0.875), y=80 + int(height * 0.7), anchor=N) + + self.calibrate_button = Button(parent, text='Calibrate Woodway Threshold', font=field_font, + command=self.__calibrate_woodway) + self.calibrate_button.place(x=int(width * 0.75), y=(height - element_height_adj) + button_size[1] * 2, + anchor=N, + width=int(width * 0.45), height=button_size[1]) + + self.__disable_ui_elements() + # endregion + + self.woodway_dir = os.path.join(self.session_dir, "Woodway") + if os.path.exists(self.woodway_dir): + latest_protocol = max(pathlib.Path(self.woodway_dir).glob("*.json"), key=lambda f: f.stat().st_ctime) + self.__load_protocol_from_file(latest_protocol) + + 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.calibrate_button.config(state='disabled') + + def __disable_ui_elements(self): + self.belt_incline_l.config(state='disabled') + self.belt_speed_l.config(state='disabled') + self.belt_speed_r.config(state='disabled') + self.woodway_disconnect_button.config(state='disabled') + + def __enable_ui_elements(self): + self.belt_incline_l.config(state='active') + self.belt_speed_l.config(state='active') + self.belt_speed_r.config(state='active') + self.woodway_disconnect_button.config(state='active') + self.woodway_connect_button.config(state='disabled') + + def get_calibration_thresholds(self): + if not self.is_calibrated(): + raise ValueError("Woodway are not calibrated!") + else: + self.woodway_speed_l, self.woodway_speed_r = self.woodway_thresh, self.woodway_thresh + return self.woodway_thresh - def __write_speed(self, side, speed): - pass + def start_session(self): + self.session_started = True + self.woodway.belt_a.set_speed(self.woodway_speed_l) + self.woodway.belt_b.set_speed(self.woodway_speed_r) + self.__save_protocol_to_file() + + def stop_session(self): + self.session_started = False + self.woodway.belt_a.set_speed(0.0) + self.woodway.belt_b.set_speed(0.0) + self.woodway.set_elevations(0.0) + + def next_protocol_step(self, current_time): + 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() + + def __update_woodway_protocol(self): + if self.selected_step == len(self.protocol_steps): + return + self.selected_command = self.protocol_steps[self.selected_step] + self.step_duration = self.selected_command[0] + self.step_time += self.step_duration + self.woodway_speed_l = self.woodway_thresh + self.selected_command[1] + self.woodway_speed_r = self.woodway_thresh + self.selected_command[2] + self.woodway_incline += self.selected_command[3] + self.__update_woodway() + + def __update_woodway(self): + self.__write_incline(self.woodway_incline) + self.__write_speed() + + def is_calibrated(self): + return self.calibrated + + def calibrate_return(self, woodway_threshold): + self.calibrated = True + self.woodway_thresh = woodway_threshold + + def __calibrate_woodway(self): + if self.woodway: + if self.woodway.is_connected(): + CalibrateWoodway(self, self.root, self.woodway) + else: + messagebox.showerror("Error", + "Something went wrong connecting to the Woodway!\nCannot be calibrated!") + else: + messagebox.showerror("Error", "Connect to Woodway first!\nCannot be calibrated!") + + def select_protocol_step(self, event): + selection = self.prot_treeview.identify_row(event.y) + if selection: + self.selected_step = int(selection) + + def populate_protocol_steps(self): + if self.protocol_steps: + for i in range(0, len(self.protocol_steps)): + self.prot_treeview_parents.append( + self.prot_treeview.insert("", 'end', str(i + 1), text=str(self.protocol_steps[i][0]), + values=(self.protocol_steps[i][1], self.protocol_steps[i][2], + self.protocol_steps[i][3]), + tags=(treeview_tags[(i + 1) % 2]))) + + def __load_protocol_from_file(self, selected_file=None): + try: + if selected_file: + self.prot_file = selected_file + with open(self.prot_file, 'r') as f: + self.protocol_steps = json.load(f)['Steps'] + self.repopulate_treeview() + else: + selected_file = filedialog.askopenfilename(filetypes=(("JSON Files", "*.json"),)) + if selected_file: + self.prot_file = selected_file + with open(self.prot_file, 'r') as f: + self.protocol_steps = json.load(f)['Steps'] + self.repopulate_treeview() + else: + 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)}") + + def __save_protocol_to_file(self): + try: + if self.changed_protocol: + if self.prot_file: + file_dir = os.path.join(self.session_dir, "Woodway") + file_count = len(glob.glob1(file_dir, "*.json")) + if file_count > 1: + new_file = os.path.join(pathlib.Path(self.prot_file).parent, + pathlib.Path(self.prot_file).stem[:-3] + f"_V{file_count}.json") + else: + new_file = os.path.join(pathlib.Path(self.prot_file).parent, + pathlib.Path(self.prot_file).stem + f"_V{file_count}.json") + with open(new_file, 'w') as f: + x = {"Steps": self.protocol_steps} + json.dump(x, f) + self.__load_protocol_from_file(selected_file=new_file) + if not self.changed_protocol: + messagebox.showinfo("Success", "Protocol file saved!") + else: + file_dir = os.path.join(self.session_dir, "Woodway") + if not os.path.exists(file_dir): + os.mkdir(file_dir) + new_file = os.path.join(file_dir, "woodway_protocol.json") + if new_file: + self.prot_file = new_file + with open(self.prot_file, 'w') as f: + x = {"Steps": self.protocol_steps} + json.dump(x, f) + if not self.changed_protocol: + messagebox.showinfo("Success", "Protocol file saved!") + 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)}") + + def popup_return(self, new_step, edit=False): + if edit: + if self.selected_step: + self.protocol_steps[int(self.selected_step) - 1] = new_step + self.repopulate_treeview() + else: + self.protocol_steps.append(new_step) + self.repopulate_treeview() + self.changed_protocol = True + self.prot_save_button['state'] = 'active' + + def repopulate_treeview(self): + clear_treeview(self.prot_treeview) + self.prot_treeview_parents = [] + self.populate_protocol_steps() + + def __edit_protocol_step(self, event): + if self.selected_step: + step = self.protocol_steps[int(self.selected_step) - 1] + AddWoodwayProtocolStep(self, self.root, edit=True, dur=step[0], ls=step[1], rs=step[2], incl=step[3]) + + def __add_protocol_step(self): + AddWoodwayProtocolStep(self, self.root) + + def __delete_protocol_step(self): + if self.selected_step: + self.protocol_steps.pop(self.selected_step - 1) + self.repopulate_treeview() + + def __connect_to_woodway(self): + try: + a_port, b_port = find_treadmills(a_sn=self.config.get_woodway_a(), b_sn=self.config.get_woodway_b()) + if a_port and b_port: + self.woodway = SplitBelt(b_port.name, a_port.name) + self.woodway.start_belts(True, False, True, False) + self.__enable_ui_elements() + messagebox.showinfo("Success!", "Woodway Split Belt treadmill connected!") + else: + messagebox.showerror("Error", "No treadmills found! Check serial numbers and connections!") + except Exception as ex: + messagebox.showerror("Exception Encountered", + f"Encountered exception when connecting to Woodway!\n{str(ex)}") + + def disconnect_woodway(self): + if self.woodway: + self.woodway.stop_belts() + self.woodway.set_elevations(0) + self.woodway.close() + self.woodway = None + self.__disable_ui_elements() + else: + messagebox.showwarning("Warning", "Connect to Woodway first!") + + def __write_speed(self): + if self.session_started: + self.belt_speed_l.set(self.woodway_speed_l) + self.belt_speed_r.set(self.woodway_speed_r) + if self.woodway: + self.belt_speed_l_value.config(text=f"{int(self.woodway_speed_l)} MPH") + self.belt_speed_r_value.config(text=f"{int(self.woodway_speed_r)} MPH") + self.woodway.set_speed(self.woodway_speed_l, self.woodway_speed_r) def __write_l_speed(self, speed): - pass + if self.session_started: + self.belt_speed_l.set(speed) + if self.woodway: + self.belt_speed_l_value.config(text=f"{float(speed):.1f} MPH") + self.woodway.belt_a.set_speed(float(speed)) def __write_r_speed(self, speed): - pass + if self.session_started: + self.belt_speed_r.set(speed) + if self.woodway: + self.belt_speed_r_value.config(text=f"{float(speed):.1f} MPH") + self.woodway.belt_b.set_speed(float(speed)) - def __write_incline(self, side, incline): - pass + def __write_incline(self, incline): + if self.session_started: + self.belt_incline_l.set(incline) + if self.woodway: + self.belt_incline_l_value.config(text=f"{float(incline):.1f}\u00b0") + self.woodway.set_elevations(float(incline)) - def __write_l_incline(self, incline): - pass - def __write_r_incline(self, incline): - pass +class ViewBLE: + def __init__(self, parent, height, width, field_font, header_font, button_size, session_dir, ble_thresh=None): + self.root = parent + 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.prot_file = None + self.step_duration = 0 + self.step_time = 0 + self.session_started = False + self.changed_protocol = True + 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] + else: + self.calibrated = False + self.right_ble_thresh = None + self.left_ble_thresh = None + # region EXPERIMENTAL PROTOCOL + element_height_adj = 100 + self.exp_prot_label = Label(parent, text="Experimental Protocol", font=header_font, anchor=CENTER) + self.exp_prot_label.place(x=int(width * 0.23) + 18, y=10, anchor=N) + self.prot_treeview_parents = [] + prot_heading_dict = {"#0": ["Duration", 'w', 20, YES, 'w']} + prot_column_dict = {"1": ["Left", 'c', 1, YES, 'c'], + "2": ["Right", 'c', 1, YES, 'c']} + treeview_offset = int(width * 0.03) + 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) + + 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), + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.ble_connect_button = Button(parent, text="Connect", font=field_font, + command=self.__connect_to_ble, bg='#4abb5f') + self.ble_connect_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.25)), + y=(height - element_height_adj) + button_size[1] * 2, + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.prot_load_button = Button(parent, text="Load File", font=field_font, + command=self.__load_protocol_from_file) + self.prot_load_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.25)), + y=(height - element_height_adj) + button_size[1], + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.prot_save_button = Button(parent, text="Save To File", font=field_font, + command=self.__save_protocol_to_file) + self.prot_save_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.75)), + y=(height - element_height_adj) + button_size[1], + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + self.prot_save_button['state'] = 'disabled' + + self.prot_del_button = Button(parent, text="Delete", font=field_font, + command=self.__delete_protocol_step) + self.prot_del_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.75)), + y=(height - element_height_adj), + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + + self.ble_disconnect_button = Button(parent, text="Disconnect", font=field_font, + command=self.disconnect_ble, bg='red') + self.ble_disconnect_button.place(x=(treeview_offset + ((int(width * 0.5) - int(width * 0.05)) * 0.75)), + y=(height - element_height_adj) + button_size[1] * 2, + anchor=N, + width=(int(width * 0.5) - int(width * 0.05)) / 2, height=button_size[1]) + # endregion + + # region VIBROTACTOR SLIDERS + slider_vars = [ + (self.update_ble_1, IntVar(parent)), + (self.update_ble_2, IntVar(parent)), + (self.update_ble_3, IntVar(parent)), + (self.update_ble_4, IntVar(parent)), + (self.update_ble_5, IntVar(parent)), + (self.update_ble_6, IntVar(parent)), + (self.update_ble_7, IntVar(parent)), + (self.update_ble_8, IntVar(parent)), + (self.update_ble_9, IntVar(parent)), + (self.update_ble_10, IntVar(parent)), + (self.update_ble_11, IntVar(parent)), + (self.update_ble_12, IntVar(parent)), + (self.update_frequency, IntVar(parent)) + ] + self.slider_objects = [] + + slider_separation = int((width * 0.4) / 6) + slider_separation_h = 40 + slider_count = 0 + + label = Label(parent, text="Vibrotactor Control", font=header_font, anchor=N) + label.place(x=int(width * 0.60) - slider_separation + int(slider_separation * 3), y=10, anchor=N) + for i in range(0, 12): + if i == 6: + slider_count = 0 + slider_separation_h += int(height * 0.4) + label = Label(parent, text=f"{i + 1}", font=field_font, anchor=N, width=4) + label.place(x=int(width * 0.6) + int(slider_count * slider_separation), y=slider_separation_h, anchor=N) + temp_slider = Scale(parent, orient="vertical", variable=slider_vars[i][1], showvalue=False, + command=slider_vars[i][0], length=int(height * 0.35), from_=255, to=0) + temp_slider.place(x=int(width * 0.6) + int(slider_count * slider_separation), y=slider_separation_h + 20, anchor=N) + self.slider_objects.append(temp_slider) + slider_count += 1 + slider_separation_h = 40 + label = Label(parent, text="Freq", font=field_font, anchor=CENTER, width=6) + label.place(x=int(width * 0.60) - slider_separation, y=slider_separation_h, anchor=N) + self.freq_slider = Scale(parent, orient="vertical", variable=slider_vars[12][1], showvalue=False, + command=slider_vars[12][0], length=int(height * 0.75), from_=7, to=0) + self.freq_slider.place(x=int(width * 0.60) - slider_separation, y=slider_separation_h + 20, anchor=N) + + self.calibrate_button = Button(parent, text='Calibrate Vibrotactor Threshold', font=field_font, + command=self.__calibrate_ble) + self.calibrate_button.place(x=int(width * 0.75), y=(height - element_height_adj) + button_size[1] * 2, + anchor=N, + width=int(width * 0.45), height=button_size[1]) + + self.__disable_ui_elements() + self.ble_dir = os.path.join(self.session_dir, "BLE") + if os.path.exists(self.ble_dir): + latest_protocol = max(pathlib.Path(self.ble_dir).glob("*.json"), key=lambda f: f.stat().st_ctime) + self.__load_protocol_from_file(latest_protocol) + # endregion + + def __enable_ui_elements(self): + self.ble_connect_button.config(state='disabled') + self.ble_disconnect_button.config(state='active') + self.freq_slider.config(state='active') + for slider in self.slider_objects: + slider.config(state='active') + + 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.calibrate_button.config(state='disabled') + + def __disable_ui_elements(self): + self.ble_connect_button.config(state='active') + self.ble_disconnect_button.config(state='disabled') + self.freq_slider.config(state='disabled') + for slider in self.slider_objects: + slider.config(state='disabled') + + def get_calibration_thresholds(self): + if not self.is_calibrated(): + raise ValueError("Vibrotactors are not calibrated!") + else: + self.r_ble_1_3_value = self.right_ble_thresh + self.r_ble_4_6_value = self.right_ble_thresh + self.r_ble_7_9_value = self.right_ble_thresh + self.r_ble_10_12_value = self.right_ble_thresh + self.l_ble_1_3_value = self.left_ble_thresh + self.l_ble_4_6_value = self.left_ble_thresh + self.l_ble_7_9_value = self.left_ble_thresh + self.l_ble_10_12_value = self.left_ble_thresh + return self.right_ble_thresh, self.left_ble_thresh + + def start_session(self): + self.session_started = True + self.right_vta.write_all_motors(self.right_ble_thresh) + self.left_vta.write_all_motors(self.left_ble_thresh) + self.right_vta.start_imu() + self.left_vta.start_imu() + self.__save_protocol_to_file() + def stop_session(self): + self.session_started = False + self.right_vta.stop_imu() + self.left_vta.stop_imu() -class ViewBLE: - def __init__(self): - pass + def is_calibrated(self): + return self.calibrated + + def calibrate_return(self, left_threshold, right_threshold): + self.calibrated = True + self.left_ble_thresh = left_threshold + self.right_ble_thresh = right_threshold + + def __calibrate_ble(self): + if self.right_vta and self.left_vta: + if self.right_vta.is_connected() and self.left_vta.is_connected(): + CalibrateVibrotactors(self, self.root, self.left_vta, self.right_vta) + else: + messagebox.showerror("Error", "Something went wrong connecting to the vibrotactors!\nCannot be calibrated!") + else: + messagebox.showerror("Error", "Connect to vibrotactors first!\nCannot be calibrated!") + + def __edit_protocol_step(self, event): + if self.selected_step: + step = self.protocol_steps[int(self.selected_step) - 1] + AddBleProtocolStep(self, self.root, edit=True, dur=step[0], + motor_1=step[1], motor_2=step[2]) + + def next_protocol_step(self, current_time): + 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() + + def __update_ble_protocol(self): + if self.selected_step + 1 == len(self.protocol_steps): + 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() + + 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)) + + def select_protocol_step(self, event): + selection = self.prot_treeview.identify_row(event.y) + if selection: + self.selected_step = int(selection) + + def populate_protocol_steps(self): + if self.protocol_steps: + for i in range(0, len(self.protocol_steps)): + self.prot_treeview_parents.append( + self.prot_treeview.insert("", 'end', str(i + 1), text=str(self.protocol_steps[i][0]), + values=(self.protocol_steps[i][1], self.protocol_steps[i][2]), + tags=(treeview_tags[(i + 1) % 2]))) + + def __load_protocol_from_file(self, selected_file=None): + try: + if selected_file: + self.prot_file = selected_file + with open(self.prot_file, 'r') as f: + self.protocol_steps = json.load(f)['Steps'] + self.repopulate_treeview() + else: + selected_file = filedialog.askopenfilename(filetypes=(("JSON Files", "*.json"),)) + if selected_file: + self.prot_file = selected_file + with open(self.prot_file, 'r') as f: + self.protocol_steps = json.load(f)['Steps'] + self.repopulate_treeview() + else: + 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)}") + + def __load_protocol(self, file): + self.prot_file = file + self.protocol_steps = json.loads(self.prot_file) + + def popup_return(self, new_step, edit=False): + if edit: + if self.selected_step: + self.protocol_steps[int(self.selected_step) - 1] = new_step + self.repopulate_treeview() + else: + self.protocol_steps.append(new_step) + self.repopulate_treeview() + self.changed_protocol = True + self.prot_save_button['state'] = 'active' + + def repopulate_treeview(self): + clear_treeview(self.prot_treeview) + self.prot_treeview_parents = [] + self.populate_protocol_steps() + + def __add_protocol_step(self): + AddBleProtocolStep(self, self.root) + + def __delete_protocol_step(self): + if self.selected_step: + self.protocol_steps.pop(self.selected_step - 1) + self.repopulate_treeview() + + def __save_protocol_to_file(self): + try: + if self.changed_protocol: + if self.prot_file: + file_dir = os.path.join(self.session_dir, "BLE") + file_count = len(glob.glob1(file_dir, "*.json")) + if file_count > 1: + new_file = os.path.join(pathlib.Path(self.prot_file).parent, + pathlib.Path(self.prot_file).stem[:-3] + f"_V{file_count}.json") + else: + new_file = os.path.join(pathlib.Path(self.prot_file).parent, + pathlib.Path(self.prot_file).stem + f"_V{file_count}.json") + with open(new_file, 'w') as f: + x = {"Steps": self.protocol_steps} + json.dump(x, f) + self.__load_protocol_from_file(selected_file=new_file) + messagebox.showinfo("Success", "Protocol file saved!") + else: + file_dir = os.path.join(self.session_dir, "BLE") + if not os.path.exists(file_dir): + os.mkdir(file_dir) + new_file = os.path.join(file_dir, "ble_protocol.json") + if new_file: + self.prot_file = new_file + with open(self.prot_file, 'w') as f: + x = {"Steps": self.protocol_steps} + json.dump(x, f) + messagebox.showinfo("Success", "Protocol file saved!") + 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)}") + + def disconnect_ble(self): + VibrotactorArray.disconnect_ble_devices(self.ble_instance) + self.__disable_ui_elements() + + def __connect_to_ble(self): + self.ble_connect_thread = threading.Thread(target=self.__connect_ble_thread) + self.ble_connect_thread.daemon = 1 + self.ble_connect_thread.start() + + 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 + else: + vta = self.right_vta + self.right_vta = self.left_vta + self.left_vta = vta + self.__enable_ui_elements() + messagebox.showinfo("Success!", "Vibrotactor arrays are connected!") + break + else: + response = messagebox.askyesno("Error", "Could not connect to both vibrotactor arrays!\nTry again?") + if not response: + break + except Exception as ex: + messagebox.showerror("Error", f"Exception encountered:\n{str(ex)}") + + def update_frequency(self, value): + if self.right_vta and self.left_vta: + self.right_vta.set_motor_frequency(int(value)) + self.left_vta.set_motor_frequency(int(value)) + + def update_ble_1(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(0, int(value)) + self.left_vta.write_motor_level(0, int(value)) + + def update_ble_2(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(1, int(value)) + self.left_vta.write_motor_level(1, int(value)) + + def update_ble_3(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(2, int(value)) + self.left_vta.write_motor_level(2, int(value)) + + def update_ble_4(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(3, int(value)) + self.left_vta.write_motor_level(3, int(value)) + + def update_ble_5(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(4, int(value)) + self.left_vta.write_motor_level(4, int(value)) + + def update_ble_6(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(5, int(value)) + self.left_vta.write_motor_level(5, int(value)) + + def update_ble_7(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(6, int(value)) + self.left_vta.write_motor_level(6, int(value)) + + def update_ble_8(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(7, int(value)) + self.left_vta.write_motor_level(7, int(value)) + + def update_ble_9(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(8, int(value)) + self.left_vta.write_motor_level(8, int(value)) + + def update_ble_10(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(9, int(value)) + self.left_vta.write_motor_level(9, int(value)) + + def update_ble_11(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(10, int(value)) + self.left_vta.write_motor_level(10, int(value)) + + def update_ble_12(self, value): + if self.right_vta and self.left_vta: + self.right_vta.write_motor_level(11, int(value)) + self.left_vta.write_motor_level(11, int(value)) class ViewVideo: diff --git a/patient_data_fields.py b/patient_data_fields.py index 5a15cb7..87dcc5a 100644 --- a/patient_data_fields.py +++ b/patient_data_fields.py @@ -85,6 +85,20 @@ def __init__(self, parent, x, y, height, width, patient_file, prim_session_numbe self.patient_name = self.patient.name if self.patient.medical_record_number: self.patient_vars[PatientDataVar.MRN].set(self.patient.medical_record_number) + if self.patient.data_recorder: + self.patient_vars[PatientDataVar.DATA_REC].set(self.patient.data_recorder) + if self.patient.session_therapist: + self.patient_vars[PatientDataVar.SESS_THER].set(self.patient.session_therapist) + if self.patient.case_manager: + self.patient_vars[PatientDataVar.CASE_MGR].set(self.patient.case_manager) + if self.patient.primary_therapist: + self.patient_vars[PatientDataVar.PRIM_THER].set(self.patient.primary_therapist) + # if self.patient.condition_name: + # self.patient_vars[PatientDataVar.COND_NAME].set(self.patient.condition_name) + if self.patient.assessment_name: + self.patient_vars[PatientDataVar.ASSESS_NAME].set(self.patient.assessment_name) + if self.patient.session_location: + self.patient_vars[PatientDataVar.SESS_LOC].set(self.patient.session_location) self.session_number = prim_session_number self.patient_entries, self.patient_labels = [], [] @@ -245,9 +259,17 @@ def check_radio(self): print(f"ERROR: Something went wrong assigning the session type " f"{self.patient_vars[PatientDataVar.PRIM_DATA].get()}") - def save_patient_fields(self): + def save_patient_fields(self, ble_thresh_r, ble_thresh_l, woodway_thresh): self.patient.save_patient(self.patient_vars[PatientDataVar.PATIENT_NAME].get(), - self.patient_vars[PatientDataVar.MRN].get()) + self.patient_vars[PatientDataVar.MRN].get(), + self.patient_vars[PatientDataVar.SESS_LOC].get(), + self.patient_vars[PatientDataVar.ASSESS_NAME].get(), + self.patient_vars[PatientDataVar.COND_NAME].get(), + self.patient_vars[PatientDataVar.PRIM_THER].get(), + self.patient_vars[PatientDataVar.CASE_MGR].get(), + self.patient_vars[PatientDataVar.SESS_THER].get(), + self.patient_vars[PatientDataVar.DATA_REC].get(), + ble_thresh_r, ble_thresh_l, woodway_thresh) def check_session_fields(self): if self.patient_vars[PatientDataVar.SESS_LOC].get() == "": @@ -302,19 +324,84 @@ def __init__(self, patient_file): self.patient_path = None self.name = None self.medical_record_number = None + self.medical_record_number = None + self.session_location = None + self.assessment_name = None + self.condition_name = None + self.primary_therapist = None + self.case_manager = None + self.session_therapist = None + self.data_recorder = None + self.left_ble_thresh = None + self.right_ble_thresh = None + self.woodway_thresh = None if patient_file: - self.update_fields(patient_file) + try: + self.update_fields(patient_file) + except KeyError or FileNotFoundError: + self.populate_defaults() def update_fields(self, filepath): f = open(filepath) self.patient_json = json.load(f) self.name = self.patient_json["Name"] self.medical_record_number = self.patient_json["MRN"] + self.session_location = self.patient_json["Session Location"] + self.assessment_name = self.patient_json["Assessment Name"] + self.condition_name = self.patient_json["Condition Name"] + self.primary_therapist = self.patient_json["Primary Therapist"] + self.case_manager = self.patient_json["Case Manager"] + self.session_therapist = self.patient_json["Session Therapist"] + self.data_recorder = self.patient_json["Data Recorder"] + self.left_ble_thresh = self.patient_json["Left BLE Thresh"] + self.right_ble_thresh = self.patient_json["Right BLE Thresh"] + self.woodway_thresh = self.patient_json["Woodway Thresh"] + + def populate_defaults(self): + if not self.name: + self.name = "" + if not self.medical_record_number: + self.medical_record_number = "" + if not self.session_location: + self.session_location = "" + if not self.assessment_name: + self.assessment_name = "" + if not self.condition_name: + self.condition_name = "" + if not self.primary_therapist: + self.primary_therapist = "" + if not self.case_manager: + self.case_manager = "" + if not self.session_therapist: + self.session_therapist = "" + if not self.data_recorder: + self.data_recorder = "" + if not self.right_ble_thresh: + self.right_ble_thresh = "" + if not self.left_ble_thresh: + self.left_ble_thresh = "" + if not self.woodway_thresh: + self.woodway_thresh = "" + self.save_patient(self.name, self.medical_record_number, self.session_location, + self.assessment_name, self.condition_name, self.primary_therapist, + self.case_manager, self.session_therapist, self.data_recorder, + self.right_ble_thresh, self.left_ble_thresh, self.woodway_thresh) - def save_patient(self, name, mrn): + def save_patient(self, name, mrn, sess_loc, assess_name, cond_name, prim_ther, case_mgr, sess_ther, data_rec, + right_ble_thresh, left_ble_thresh, woodway_thresh): with open(self.source_file, 'w') as f: x = { "Name": name, - "MRN": mrn + "MRN": mrn, + "Session Location": sess_loc, + "Assessment Name": assess_name, + "Condition Name": cond_name, + "Primary Therapist": prim_ther, + "Case Manager": case_mgr, + "Session Therapist": sess_ther, + "Data Recorder": data_rec, + "Left BLE Thresh": left_ble_thresh, + "Right BLE Thresh": right_ble_thresh, + "Woodway Thresh": woodway_thresh } json.dump(x, f) diff --git a/project_setup_ui.py b/project_setup_ui.py index 7d6de36..6471b11 100644 --- a/project_setup_ui.py +++ b/project_setup_ui.py @@ -9,8 +9,9 @@ from tkinter.ttk import Combobox from ksf_utils import import_ksf, create_new_ksf_revision, compare_keystrokes +from patient_data_fields import PatientContainer from tkinter_utils import center, get_display_size, get_treeview_style, build_treeview, EntryPopup, select_focus, \ - NewKeyPopup, clear_treeview, get_slider_style + NewKeyPopup, clear_treeview, get_slider_style, ProjectPopup from ui_params import project_treeview_params as ptp, treeview_tags, window_ratio, large_field_font, medium_field_font, \ small_field_font, large_treeview_font, \ medium_treeview_font, small_treeview_font, large_treeview_rowheight, medium_treeview_rowheight, \ @@ -73,7 +74,8 @@ def __init__(self, config, first_time_user): project_treeview_height, treeview_width, project_heading_dict, - double_bind=self.select_project) + double_bind=self.select_project, + button_3_bind=self.delete_project) self.recent_projects = config.get_recent_projects() self.populate_recent_projects() patient_treeview_height = int(self.window_height * 0.2) @@ -192,19 +194,22 @@ def popup_return(self, data, caller): return if caller == 0: # Update path to project and create if it doesn't exist - self.project_dir = os.path.join(self.top_dir, data) + self.project_name = data[0] + self.project_dir = os.path.join(data[1], data[0]) if not os.path.exists(self.project_dir): os.mkdir(self.project_dir) + if not os.path.exists(os.path.join(self.project_dir, '.cometrics')): + open(os.path.join(self.project_dir, '.cometrics'), 'w') # Add to treeview self.project_treeview_parents.append( - self.project_treeview.insert("", 'end', str((int(self.project_treeview_parents[-1]) + 1)), text=data, + self.project_treeview.insert("", 'end', str((int(self.project_treeview_parents[-1]) + 1)), text=self.project_name, tags=treeview_tags[(int(self.project_treeview_parents[-1]) + 1) % 2])) select_focus(self.project_treeview, self.project_treeview_parents[-1]) # Save recent path to config if not self.recent_projects: self.recent_projects = [] self.recent_projects.append(self.project_dir) - self.config.set_recent_projects(self.recent_projects[-20:]) + self.config.set_recent_projects(self.recent_projects) # Load the project self.load_project(self.project_dir) elif caller == 1: @@ -224,7 +229,6 @@ def popup_return(self, data, caller): tags=treeview_tags[(int(self.concern_treeview_parents[-1]) + 1) % 2])) select_focus(self.concern_treeview, self.concern_treeview_parents[-1]) self.load_concern(len(self.concerns)) - # endregion # region Project UI Controls @@ -235,22 +239,38 @@ def select_project(self, event): self.create_new_project() else: try: - self.load_project(self.recent_projects[int(selection) - 1]) + self.selected_project = int(selection) - 1 + self.load_project(self.recent_projects[self.selected_project]) except IndexError as e: print(f"ERROR: Error encountered when selecting project:\n{str(e)}\n{traceback.print_exc()}\n" f"{self.recent_projects}\n{selection}") + def delete_project(self, event): + selection = self.project_treeview.identify_row(event.y) + if selection: + if selection == '0': + return + else: + try: + response = messagebox.askyesno("Delete Project?", + f"Delete {pathlib.Path(self.recent_projects[int(selection) - 1]).name} from Recent Projects?") + if response: + self.recent_projects.pop(int(selection) - 1) + self.config.set_recent_projects(self.recent_projects) + clear_treeview(self.project_treeview) + clear_treeview(self.patient_treeview) + clear_treeview(self.concern_treeview) + self.reset_ksf() + self.populate_recent_projects() + except IndexError as e: + print( + f"ERROR: Error encountered when deleting project: \n{str(e)}\n{traceback.print_exc()}\n{self.recent_projects}\n{selection}") + def create_new_project(self): - self.top_dir = filedialog.askdirectory(title='Select root directory to save files') - print("INFO:", self.top_dir) - if not self.top_dir: - messagebox.showwarning("Warning", "No root filepath chosen! Please try again.") - return - else: - self.top_dir = os.path.normpath(self.top_dir) - EntryPopup(self, self.main_root, "Enter New Project Name", 0) + ProjectPopup(self, self.main_root, "Create or Import New Project", 0) def populate_recent_projects(self): + self.project_treeview_parents = [] self.project_treeview_parents.append( self.project_treeview.insert("", 'end', str(0), text="Create or Import New Project", tags=treeview_tags[2])) @@ -266,18 +286,26 @@ def load_project(self, directory): try: _, self.patients, _ = next(os.walk(directory)) except StopIteration: - messagebox.showerror("Error", "Selected project cannot be found!") + response = messagebox.askyesno("Error", "Selected project cannot be found!\nDelete from Recent Projects?") + if response: + self.recent_projects.pop(self.selected_project) + self.config.set_recent_projects(self.recent_projects) + clear_treeview(self.project_treeview) + clear_treeview(self.patient_treeview) + clear_treeview(self.concern_treeview) + self.reset_ksf() + self.populate_recent_projects() return clear_treeview(self.patient_treeview) clear_treeview(self.concern_treeview) self.reset_ksf() - self.patient_treeview_parents = [] self.populate_patients() # endregion # region Patient UI Controls def populate_patients(self): + self.patient_treeview_parents = [] self.patient_treeview_parents.append(self.patient_treeview.insert("", 'end', str(0), text="Create New Patient", tags=treeview_tags[2])) if self.patients: @@ -296,6 +324,8 @@ def select_patient(self, event): self.create_new_patient() else: self.patient_dir = os.path.join(self.project_dir, self.patients[int(selection) - 1]) + if not os.path.exists(self.patient_dir): + os.mkdir(self.patient_dir) self.load_patient(self.patients[int(selection) - 1]) def patient_creation_check(self): @@ -317,19 +347,14 @@ def load_patient(self, directory): self.populate_patient_concerns() self.patient_data_file = os.path.normpath( os.path.join(self.patient_dir, pathlib.Path(self.patient_dir).stem + '.json')) - if not os.path.exists(self.patient_data_file): - with open(self.patient_data_file, 'w') as f: - x = { - "Name": pathlib.Path(self.patient_dir).stem, - "MRN": "" - } - json.dump(x, f) + self.patient_container = PatientContainer(self.patient_data_file) # endregion # region Concern UI Controls def populate_patient_concerns(self): self.read_concern_file() + self.concern_treeview_parents = [] self.concern_treeview_parents.append(self.concern_treeview.insert("", 'end', str(0), text="Create New Concern", tags=treeview_tags[2])) if self.concerns: @@ -475,6 +500,7 @@ def import_concern_ksf(self): def populate_frequency_treeview(self): self.clear_frequency_treeview() + self.frequency_treeview_parents = [] self.frequency_treeview_parents.append( self.frequency_key_treeview.insert("", 'end', str(0), text="Create New Frequency Key", tags=treeview_tags[2])) @@ -495,6 +521,7 @@ def clear_duration_treeview(self): def populate_duration_treeview(self): self.clear_duration_treeview() + self.duration_treeview_parents = [] self.duration_treeview_parents.append( self.duration_key_treeview.insert("", 'end', str(0), text="Create New Duration Key", tags=treeview_tags[2])) diff --git a/reference/Cometrics User Guide.pdf b/reference/Cometrics User Guide.pdf index 455ca5f..5c57daa 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 9ea513f..6e53e34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,8 @@ PyYAML~=6.0 pyEmpatica~=0.5.7 openpyxl~=3.0.9 matplotlib==3.4.3 -logger-util~=0.2 \ No newline at end of file +logger-util~=0.2 +future~=0.18.2 +ttkwidgets~=0.12.1 +pyWoodway~=0.2.3 +pyTactor~=0.1.4 \ No newline at end of file diff --git a/session_manager_ui.py b/session_manager_ui.py index e81cb16..fff8c4a 100644 --- a/session_manager_ui.py +++ b/session_manager_ui.py @@ -1,6 +1,5 @@ import datetime import json -import math import os import pathlib from os import walk, path @@ -31,6 +30,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.keystroke_file = project_setup.ksf_file self.session_dir = project_setup.phase_dir self.tracker_file = project_setup.tracker_file @@ -106,12 +106,16 @@ def __init__(self, config, project_setup): self.menu = MenuBar(root, self) self.stf = SessionTimeFields(self, root, x=self.logo_width + 10, - y=self.logo_height + 10, + y=(self.logo_height + 10) - self.button_size[1], height=self.patient_field_height, width=self.field_width, header_font=self.header_font, field_font=self.field_font, - field_offset=self.field_offset) + field_offset=self.field_offset, + button_size=self.button_size) + thresholds = [self.patient_container.right_ble_thresh, + self.patient_container.left_ble_thresh, + self.patient_container.woodway_thresh] self.ovu = OutputViewPanel(root, x=(self.logo_width * 2) + 20, y=(self.logo_height + 10) - self.button_size[1], @@ -123,8 +127,10 @@ def __init__(self, config, project_setup): header_font=self.header_font, video_import_cb=self.start_video_control, slider_change_cb=self.change_time, - config=self.config) - self.stf.kdf = self.ovu.key_view + config=self.config, + session_dir=self.session_dir, + thresholds=thresholds) + self.stf.ovu = self.ovu self.pdf = PatientDataFields(root, x=5, y=self.logo_height + 10, @@ -316,14 +322,18 @@ def save_session(self): f"{session_fields['Condition Name'][:2]}" f"{self.session_file_date}{reli}.json") if self.ovu.video_view.video_file: - cp_rename(src=self.ovu.video_view.video_file, - dst=path.join(self.session_dir, - self.config.get_data_folders()[1], - session_fields["Primary Data"]), - name=f"{session_fields['Session Number']}" - f"{session_fields['Assessment Name'][:2]}" - f"{session_fields['Condition Name'][:2]}" - f"{self.session_file_date}{reli}") + try: + cp_rename(src=self.ovu.video_view.video_file, + dst=path.join(self.session_dir, + self.config.get_data_folders()[1], + session_fields["Primary Data"]), + name=f"{session_fields['Session Number']}" + f"{session_fields['Assessment Name'][:2]}" + f"{session_fields['Condition Name'][:2]}" + f"{self.session_file_date}{reli}") + except FileExistsError: + messagebox.showwarning("Warning", "Video copy procedure failed, double check that session video is present with session file!") + print("WARNING: Video copy procedure failed, double check that session video is present with session file!") with open(output_session_file, 'w') as f: json.dump(session_fields, f) print(f"INFO: Saved session file to: {output_session_file}") @@ -336,9 +346,38 @@ def save_session(self): def start_session(self): response = self.pdf.check_session_fields() if response is False: + ble_thresh_r, ble_thresh_l, woodway_thresh = None, None, None + if self.config.get_ble(): + if self.ovu.ble_view: + if not self.ovu.ble_view.is_calibrated(): + messagebox.showwarning("Warning", "Vibrotactors must be calibrated before starting session!") + print("WARNING: Vibrotactors must be calibrated before starting session") + return + else: + ble_thresh_r, ble_thresh_l = self.ovu.ble_view.get_calibration_thresholds() + self.ovu.ble_view.disable_ui_elements() + else: + messagebox.showwarning("Error", "Something went wrong with starting session!\n" + "Vibrotactor view is not present when it should be!") + print("ERROR: Something went wrong with starting session, vibrotactor view is not present when it should be") + return + if self.config.get_woodway(): + if self.ovu.woodway_view: + if not self.ovu.woodway_view.is_calibrated(): + messagebox.showwarning("Warning", "Woodway must be calibrated before starting session!") + print("WARNING: Woodway must be calibrated before starting session") + return + else: + woodway_thresh = self.ovu.woodway_view.get_calibration_thresholds() + self.ovu.woodway_view.disable_ui_elements() + else: + messagebox.showwarning("Error", "Something went wrong with starting session!\n" + "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.pdf.start_label['text'] = "Session Start Time: " + self.session_time - self.pdf.save_patient_fields() + self.pdf.save_patient_fields(ble_thresh_r, ble_thresh_l, woodway_thresh) self.pdf.lock_session_fields() self.stf.lock_session_fields() # Start the session diff --git a/session_time_fields.py b/session_time_fields.py index 7a7c41a..f505473 100644 --- a/session_time_fields.py +++ b/session_time_fields.py @@ -9,17 +9,37 @@ from ui_params import treeview_bind_tags +# TODO: Create a wrapper on this like the output view ui to create the 'Session' mode and 'Review' mode +# TODO: Either the panel is switched by clicking a tab or it can be automatically created when a session with existing data is loaded? +# TODO: It would seem potentially confusing to not have some kind of obvious UI change, so maybe some kind of header tab? +# TODO: No, because you need to be able to switch back and forth, if you're reviewing a session and need to change something, +# TODO: the 'Session' mode can be used to do that class SessionTimeFields: - def __init__(self, caller, parent, x, y, height, width, + def __init__(self, caller, parent, x, y, height, width, button_size, header_font=('Purisa', 14), field_font=('Purisa', 11), - field_offset=60, kdf=None): + field_offset=60, ovu=None): + self.SESSION_VIEW, self.REVIEW_VIEW = 0, 1 + self.x, self.y = x, y + self.button_size = button_size self.width, self.height = width, height self.field_offset = field_offset - self.kdf = kdf + self.ovu = ovu self.caller = caller + self.frame = Frame(parent, width=width, height=height) self.frame.place(x=x, y=y) + clean_view = Frame(self.frame, width=width, + height=button_size[1], bg='white') + clean_view.place(x=0, y=0) + + self.session_frame = Frame(parent, width=width, height=height) + self.session_frame.place(x=x, y=y + self.button_size[1]) + + self.review_frame = Frame(parent, width=width, height=height) + self.view_frames = [self.session_frame, self.review_frame] + self.view_buttons = [] + self.video_playing = False self.session_started = False self.session_paused = False @@ -29,62 +49,64 @@ def __init__(self, caller, parent, x, y, height, width, self.start_y = 15 self.session_time = 0 self.break_time = 0 - session_time_label = Label(self.frame, text="Session Time", font=(header_font[0], header_font[1], 'bold')) + + self.current_button = 0 + session_time_label = Label(self.session_frame, text="Session Time", font=(header_font[0], header_font[1], 'bold')) session_time_label.place(x=width / 2, y=self.start_y, anchor=CENTER) - self.session_time_label = Label(self.frame, text="0:00:00", + self.session_time_label = Label(self.session_frame, text="0:00:00", font=header_font) self.session_time_label.place(x=width / 2, y=self.start_y + (field_offset / 2), anchor=CENTER) - break_time_label = Label(self.frame, text='Break Time', font=(header_font[0], header_font[1], 'bold')) + break_time_label = Label(self.session_frame, text='Break Time', font=(header_font[0], header_font[1], 'bold')) break_time_label.place(x=width / 2, y=self.start_y + ((field_offset / 2) * 2), anchor=CENTER) - self.break_time_label = Label(self.frame, text="0:00:00", + self.break_time_label = Label(self.session_frame, text="0:00:00", font=header_font) self.break_time_label.place(x=width / 2, y=self.start_y + ((field_offset / 2) * 3), anchor=CENTER) - self.session_start_label = Label(self.frame, text="Session Started", fg='green', + self.session_start_label = Label(self.session_frame, text="Session Started", fg='green', font=header_font) - self.session_paused_label = Label(self.frame, text="Session Paused", fg='yellow', + self.session_paused_label = Label(self.session_frame, text="Session Paused", fg='yellow', font=header_font) - self.session_stopped_label = Label(self.frame, text="Session Stopped", fg='red', + self.session_stopped_label = Label(self.session_frame, text="Session Stopped", fg='red', font=header_font) self.session_stopped_label.place(x=width / 2, y=self.start_y + ((field_offset / 2) * 4), anchor=CENTER) self.interval_selection = BooleanVar() - self.interval_checkbutton = Checkbutton(self.frame, text="Reminder Beep (Seconds)", + self.interval_checkbutton = Checkbutton(self.session_frame, text="Reminder Beep (Seconds)", variable=self.interval_selection, font=header_font, command=self.show_beep_interval) self.interval_checkbutton.place(x=10, y=self.start_y + ((field_offset / 2) * 6), anchor=W) self.interval_input_var = StringVar() - interval_cmd = self.frame.register(self.validate_number) - self.interval_input = Entry(self.frame, validate='all', validatecommand=(interval_cmd, '%P'), + interval_cmd = self.session_frame.register(self.validate_number) + self.interval_input = Entry(self.session_frame, validate='all', validatecommand=(interval_cmd, '%P'), font=header_font, width=6) - session_cmd = self.frame.register(self.validate_number) - self.session_dur_input = Entry(self.frame, validate='all', validatecommand=(session_cmd, '%P'), + session_cmd = self.session_frame.register(self.validate_number) + self.session_dur_input = Entry(self.session_frame, validate='all', validatecommand=(session_cmd, '%P'), font=header_font, width=6) self.session_dur_selection = BooleanVar() - self.session_dur_checkbutton = Checkbutton(self.frame, text="Session Duration (Seconds)", + self.session_dur_checkbutton = Checkbutton(self.session_frame, text="Session Duration (Seconds)", variable=self.session_dur_selection, font=header_font, command=self.show_session_time) self.session_dur_checkbutton.place(x=10, y=self.start_y + ((field_offset / 2) * 7), anchor=W) - self.session_toggle_button = Button(self.frame, text="Start Session", bg='#4abb5f', + self.session_toggle_button = Button(self.session_frame, text="Start Session", bg='#4abb5f', font=field_font, width=13, command=self.caller.start_session) self.session_toggle_button.place(x=width / 2, y=self.start_y + ((field_offset / 2) * 9), anchor=CENTER) - self.key_explanation = Label(self.frame, text="Esc Key", font=field_font, + self.key_explanation = Label(self.session_frame, text="Esc Key", font=field_font, justify=LEFT) self.key_explanation.place(x=width * 0.75, y=self.start_y + ((field_offset / 2) * 9), anchor=W) - self.session_pause_button = Button(self.frame, text="Pause Session", width=13, + self.session_pause_button = Button(self.session_frame, text="Pause Session", width=13, font=field_font, command=self.caller.pause_session) self.session_pause_button.place(x=width / 2, y=self.start_y + ((field_offset / 2) * 10.5), anchor=CENTER) - self.key_explanation = Label(self.frame, text="Left Ctrl", font=field_font, + self.key_explanation = Label(self.session_frame, text="Left Ctrl", font=field_font, justify=LEFT) self.key_explanation.place(x=width * 0.75, y=self.start_y + ((field_offset / 2) * 10.5), anchor=W) @@ -93,17 +115,45 @@ def __init__(self, caller, parent, x, y, height, width, self.forward_image = PhotoImage(file='images/skip_forward.png') self.backward_image = PhotoImage(file='images/skip_backward.png') # - self.play_button = Button(self.frame, image=self.play_image, + self.play_button = Button(self.session_frame, image=self.play_image, command=self.caller.start_session) - self.forward_button = Button(self.frame, image=self.forward_image) - self.backward_button = Button(self.frame, image=self.backward_image) + self.forward_button = Button(self.session_frame, image=self.forward_image) + self.backward_button = Button(self.session_frame, image=self.backward_image) self.session_duration = None self.beep_th = None self.interval_thread = None + # session_button = Button(self.frame, text="Session", command=self.switch_session_frame, width=12, + # font=field_font) + # self.view_buttons.append(session_button) + # self.SESSION_VIEW = len(self.view_buttons) - 1 + # self.view_buttons[self.SESSION_VIEW].place(x=(len(self.view_buttons) - 1) * button_size[0], y=0, + # width=button_size[0], height=button_size[1]) + # self.view_buttons[self.SESSION_VIEW].config(relief=SUNKEN) + + # review_button = Button(self.frame, text="Review", command=self.switch_review_frame, width=12, + # font=field_font) + # self.view_buttons.append(review_button) + # self.REVIEW_VIEW = len(self.view_buttons) - 1 + # self.view_buttons[self.REVIEW_VIEW].place(x=(len(self.view_buttons) - 1) * button_size[0], y=0, + # width=button_size[0], height=button_size[1]) + self.time_thread = threading.Thread(target=self.time_update_thread) + def switch_session_frame(self): + self.switch_frame(self.SESSION_VIEW) + + def switch_frame(self, view): + self.view_buttons[self.current_button].config(relief=RAISED) + self.view_frames[self.current_button].place_forget() + self.current_button = view + self.view_buttons[view].config(relief=SUNKEN) + self.view_frames[view].place(x=self.x, y=self.y + self.button_size[1]) + + def switch_review_frame(self): + self.switch_frame(self.REVIEW_VIEW) + def video_control(self, nframes): self.play_button.place(x=self.width / 2, y=self.start_y + ((self.field_offset / 2) * 12.0), anchor=N) @@ -157,18 +207,23 @@ def show_beep_interval(self): self.interval_input.place_forget() def time_update_thread(self): + # TODO: Tie in Woodway and BLE protocol access while self.timer_running: time.sleep(1 - time.monotonic() % 1) if self.timer_running: if self.session_started and not self.session_paused: self.session_time += 1 - for i in range(0, len(self.kdf.dur_sticky)): - if self.kdf.dur_sticky[i]: - self.kdf.dur_treeview.set(str(i), column="1", - value=self.session_time - self.kdf.sticky_start[i]) + for i in range(0, len(self.ovu.key_view.dur_sticky)): + if self.ovu.key_view.dur_sticky[i]: + self.ovu.key_view.dur_treeview.set(str(i), column="1", + value=self.session_time - self.ovu.key_view.sticky_start[i]) if self.session_duration: if self.session_time >= self.session_duration: self.caller.stop_session() + if self.ovu.woodway_view: + self.ovu.woodway_view.next_protocol_step(self.session_time) + if self.ovu.ble_view: + self.ovu.ble_view.next_protocol_step(self.session_time) elif self.session_paused: if not self.caller.ovu.video_view.player: self.break_time += 1 @@ -178,14 +233,14 @@ def time_update_thread(self): def change_time(self, current_seconds): self.session_time = current_seconds self.session_time_label['text'] = str(datetime.timedelta(seconds=self.session_time)) - for i in range(0, len(self.kdf.dur_sticky)): - if self.kdf.dur_sticky[i]: - if self.session_time < self.kdf.sticky_start[i]: - self.kdf.dur_sticky[i] = False - self.kdf.dur_treeview.item(str(i), tags=treeview_bind_tags[i % 2]) + for i in range(0, len(self.ovu.key_view.dur_sticky)): + if self.ovu.key_view.dur_sticky[i]: + if self.session_time < self.ovu.key_view.sticky_start[i]: + self.ovu.key_view.dur_sticky[i] = False + self.ovu.key_view.dur_treeview.item(str(i), tags=treeview_bind_tags[i % 2]) else: - self.kdf.dur_treeview.set(str(i), column="1", - value=self.session_time - self.kdf.sticky_start[i]) + self.ovu.key_view.dur_treeview.set(str(i), column="1", + value=self.session_time - self.ovu.key_view.sticky_start[i]) def start_session(self): self.session_started = True @@ -250,3 +305,10 @@ def stop_timer(self): def beep_thread(self): winsound.PlaySound("SystemHand", winsound.SND_ALIAS) + + +class ReviewMode: + def __init__(self, caller, parent, x, y, height, width, button_size, + header_font=('Purisa', 14), field_font=('Purisa', 11), + field_offset=60, ovu=None): + pass diff --git a/setup.py b/setup.py index 0f36a92..184fb09 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,8 @@ 'matplotlib', 'pyEmpatica', 'openpyxl', - 'logger-util' + 'logger-util', + 'pywoodway' ], include_package_data=True, zip_safe=False diff --git a/tkinter_utils.py b/tkinter_utils.py index 3ac6e21..0b0bb84 100644 --- a/tkinter_utils.py +++ b/tkinter_utils.py @@ -1,9 +1,15 @@ +import os.path +import pathlib +import threading +import time import tkinter -from tkinter import TOP, W, N, NW, CENTER, messagebox, END, ttk +from tkinter import TOP, W, N, NW, CENTER, messagebox, END, ttk, LEFT, filedialog from tkinter.ttk import Style, Combobox from tkinter.ttk import Treeview, Entry -from ui_params import treeview_default_tag_dict +import numpy as np +from PIL import ImageTk as itk +from ui_params import treeview_default_tag_dict, cometrics_ver_root def center(toplevel, y_offset=-20): @@ -59,8 +65,8 @@ def get_treeview_style(name="mystyle.Treeview", font=('Purisa', 12), heading_fon def build_treeview(root, x, y, height, width, heading_dict, column_dict=None, selectmode='browse', t_height=18, - filescroll=True, button_1_bind=None, double_bind=None, style="mystyle.Treeview", anchor=NW, - tag_dict=treeview_default_tag_dict, fs_offset=18): + filescroll=True, button_1_bind=None, double_bind=None, button_3_bind=None, style="mystyle.Treeview", + anchor=NW, tag_dict=treeview_default_tag_dict, fs_offset=18): treeview = Treeview(root, style=style, height=t_height, selectmode=selectmode) treeview.place(x=x, y=y, height=height, width=width, anchor=anchor) # Define header @@ -79,6 +85,8 @@ def build_treeview(root, x, y, height, width, heading_dict, column_dict=None, se treeview.bind("", button_1_bind) if double_bind: treeview.bind("", double_bind) + if button_3_bind: + treeview.bind("", button_3_bind) if filescroll: file_scroll = tkinter.Scrollbar(root, orient="vertical", command=treeview.yview) file_scroll.place(x=(x - fs_offset), y=y, height=height, anchor=anchor) @@ -173,7 +181,7 @@ def __init__(self, root, config): self.config = config self.popup_root = popup_root = tkinter.Toplevel(root) popup_root.config(bg="white", bd=-2) - popup_root.geometry("300x250") + popup_root.geometry("300x340") popup_root.title("Configuration Settings") fps_tag = tkinter.Label(popup_root, text="FPS", bg='white', font=('Purisa', 12)) fps_tag.place(x=10, y=10) @@ -192,11 +200,26 @@ def __init__(self, root, config): ble_checkbutton = tkinter.Checkbutton(popup_root, text="BLE Enabled", bg='white', variable=self.ble_var, font=('Purisa', 12)) ble_checkbutton.place(x=10, y=130) + + a_tag = tkinter.Label(popup_root, text="A", bg='white', font=('Purisa', 12)) + a_tag.place(x=10, y=170) + + self.a_entry = tkinter.Entry(popup_root, bd=2, width=12, font=('Purisa', 12)) + self.a_entry.insert(0, str(self.config.get_woodway_a())) + self.a_entry.place(x=60, y=170) + + b_tag = tkinter.Label(popup_root, text="B", bg='white', font=('Purisa', 12)) + b_tag.place(x=10, y=210) + + self.b_entry = tkinter.Entry(popup_root, bd=2, width=12, font=('Purisa', 12)) + self.b_entry.insert(0, str(self.config.get_woodway_b())) + self.b_entry.place(x=60, y=210) + clear_projects = tkinter.Button(popup_root, text="Clear Recent Projects", font=('Purisa', 12), command=self.clear_projects) - clear_projects.place(x=10, y=170) + clear_projects.place(x=10, y=250) ok_button = tkinter.Button(popup_root, text="OK", command=self.on_closing, font=('Purisa', 12)) - ok_button.place(x=150, y=210, anchor=N) + ok_button.place(x=150, y=290, anchor=N) def on_closing(self): self.update_fps() @@ -221,6 +244,78 @@ def clear_projects(self): self.config.set_recent_projects([]) +class ProjectPopup: + def __init__(self, top, root, name, popup_call): + assert top.popup_return + self.popup_call = popup_call + self.caller = top + self.entry = None + self.popup_root = None + self.name = name + self.popup_entry(root) + + def popup_entry(self, root): + # Create a Toplevel window + self.popup_root = popup_root = tkinter.Toplevel(root) + popup_root.config(bg="white", bd=-2) + popup_root.geometry("400x100") + popup_root.title(self.name) + + # Create an Entry Widget in the Toplevel window + project_name_text = tkinter.Label(popup_root, text="Project Name", font=('Purisa', 10), bg='white') + project_name_text.place(x=10, y=10, anchor=NW) + self.project_name_var = tkinter.StringVar(popup_root) + self.entry = tkinter.Entry(popup_root, bd=2, width=35, textvariable=self.project_name_var) + self.entry.place(x=110, y=11, anchor=NW) + + project_dir_text = tkinter.Label(popup_root, text="Project Folder", font=('Purisa', 10), bg='white') + project_dir_text.place(x=10, y=40, anchor=NW) + self.dir_entry_var = tkinter.StringVar(popup_root, value=rf'{cometrics_ver_root}\Projects') + self.dir_entry = tkinter.Entry(popup_root, bd=2, width=35, textvariable=self.dir_entry_var) + self.dir_entry.place(x=110, y=41, anchor=NW) + + self.folder_img = itk.PhotoImage(file='images/folder.png') + dir_button = tkinter.Button(popup_root, image=self.folder_img, command=self.select_dir) + dir_button.place(x=330, y=40, anchor=NW) + + # Create a Button Widget in the Toplevel Window + import_button = tkinter.Button(popup_root, text="Import", command=self.import_project) + import_button.place(x=195, y=95, anchor=tkinter.SE, width=50) + button = tkinter.Button(popup_root, text="OK", command=self.close_win) + button.place(x=205, y=95, anchor=tkinter.SW, width=50) + center(popup_root) + popup_root.focus_force() + self.entry.focus() + + def import_project(self): + chosen_project = filedialog.askdirectory(title='Select project folder') + if chosen_project: + if os.path.isdir(chosen_project): + if not os.path.exists(os.path.join(chosen_project, '.cometrics')): + response = messagebox.askyesno("Invalid Project", "Project is not a valid cometrics project, " + "this could happen if importing a legacy project " + "(legacy projects will be updated in that case), " + "continue?") + if not response: + self.popup_root.focus_force() + return + self.dir_entry_var.set(pathlib.Path(chosen_project).parent) + self.project_name_var.set(pathlib.Path(chosen_project).name) + self.close_win() + else: + messagebox.showerror("Project Error", "Projects must be folders!") + + def select_dir(self): + chosen_dir = filedialog.askdirectory(title='Select root directory to save files') + if chosen_dir: + self.dir_entry_var.set(chosen_dir) + self.popup_root.focus_force() + + def close_win(self): + self.caller.popup_return((self.entry.get(), self.dir_entry.get()), self.popup_call) + self.popup_root.destroy() + + class EntryPopup: def __init__(self, top, root, name, popup_call): assert top.popup_return @@ -303,3 +398,400 @@ def close_win(self): def set_entry_text(widget: tkinter.Entry, text): widget.delete(0, END) widget.insert(0, text) + + +class AddWoodwayProtocolStep: + def __init__(self, top, root, edit=False, dur=None, rs=None, ls=None, incl=None): + assert top.popup_return + self.caller = top + self.entry = None + self.popup_root = None + self.name = "Add Woodway Step" + self.dur, self.rs, self.ls, self.incl = dur, rs, ls, incl + self.edit = edit + self.popup_entry(root) + + def popup_entry(self, root): + # Create a Toplevel window + self.popup_root = popup_root = tkinter.Toplevel(root) + popup_root.config(bg="white", bd=-2) + popup_root.geometry("300x250") + popup_root.title(self.name) + + # Create an Entry Widget in the Toplevel window + label = tkinter.Label(popup_root, text="Step Duration", font=('Purisa', 12), bg='white') + label.pack() + self.duration_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.duration_entry.pack() + if self.dur is not None: + set_entry_text(self.duration_entry, self.dur) + label = tkinter.Label(popup_root, text="Left Side Speed", font=('Purisa', 12), bg='white') + label.pack() + self.ls_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.ls_entry.pack() + if self.ls is not None: + set_entry_text(self.ls_entry, self.ls) + label = tkinter.Label(popup_root, text="Right Side Speed", font=('Purisa', 12), bg='white') + label.pack() + self.rs_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.rs_entry.pack() + if self.rs is not None: + set_entry_text(self.rs_entry, self.rs) + label = tkinter.Label(popup_root, text="Incline", font=('Purisa', 12), bg='white') + label.pack() + self.incline_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.incline_entry.pack() + if self.incl is not None: + set_entry_text(self.incline_entry, self.incl) + + # Create a Button Widget in the Toplevel Window + button = tkinter.Button(popup_root, text="OK", command=self.close_win) + button.pack(pady=5, side=TOP) + center(popup_root) + popup_root.focus_force() + self.duration_entry.focus() + + def close_win(self): + try: + new_step = [float(self.duration_entry.get()), float(self.ls_entry.get()), + float(self.rs_entry.get()), float(self.incline_entry.get())] + self.caller.popup_return(new_step, self.edit) + self.popup_root.destroy() + except ValueError: + messagebox.showerror("Error", "All values input must be numbers! Check Woodway documentation or\n" + "User Guide for valid values!") + + +class AddBleProtocolStep: + def __init__(self, top, root, edit=False, dur=None, motor_1=None, motor_2=None): + assert top.popup_return + self.caller = top + self.entry = None + self.popup_root = None + self.name = "Add BLE Step" + self.edit = edit + self.dur, self.motor_1, self.motor_2 = dur, motor_1, motor_2 + self.popup_entry(root) + + def popup_entry(self, root): + # Create a Toplevel window + self.popup_root = popup_root = tkinter.Toplevel(root) + popup_root.config(bg="white", bd=-2) + popup_root.geometry("300x175") + popup_root.title(self.name) + + # Create an Entry Widget in the Toplevel window + label = tkinter.Label(popup_root, text="Step Duration", font=('Purisa', 12), bg='white') + label.pack() + self.duration_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.duration_entry.pack() + if self.dur is not None: + set_entry_text(self.duration_entry, self.dur) + label = tkinter.Label(popup_root, text="Left Motor Level", font=('Purisa', 12), bg='white') + label.pack() + self.motor_1_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.motor_1_entry.pack() + if self.motor_1 is not None: + set_entry_text(self.motor_1_entry, self.motor_1) + label = tkinter.Label(popup_root, text="Right Motor Level", font=('Purisa', 12), bg='white') + label.pack() + self.motor_2_entry = tkinter.Entry(popup_root, bd=2, width=25) + self.motor_2_entry.pack() + if self.motor_2 is not None: + set_entry_text(self.motor_2_entry, self.motor_2) + + # Create a Button Widget in the Toplevel Window + button = tkinter.Button(popup_root, text="OK", command=self.close_win) + button.pack(pady=5, side=TOP) + center(popup_root) + popup_root.focus_force() + self.duration_entry.focus() + + def close_win(self): + try: + new_step = [float(self.duration_entry.get()), float(self.motor_1_entry.get()), + float(self.motor_2_entry.get())] + self.caller.popup_return(new_step, edit=self.edit) + self.popup_root.destroy() + except ValueError: + messagebox.showerror("Error", "All values input must be numbers! Check Woodway documentation or\n" + "User Guide for valid values!") + + +class CalibrateWoodway: + def __init__(self, top, root, woodway): + assert top.calibrate_return + self.caller = top + self.entry = None + self.popup_root = None + self.name = "Calibrate Woodway" + self.woodway = woodway + self.calibrating = False + self.calibrated_speed_increasing = None + self.calibrated_speed_decreasing = None + self.calibrated_speed = None + self.calibration_step = 0 + self.popup_entry(root) + + def popup_entry(self, root): + # Create a Toplevel window + self.popup_root = popup_root = tkinter.Toplevel(root) + popup_root.config(bg="white", bd=-2) + popup_root.geometry("800x300") + popup_root.title(self.name) + + # Create an Entry Widget in the Toplevel window + label = tkinter.Label(popup_root, text="1. Press the 'Calibrate' button,\n" + "2. The Woodway will increase speed by 0.1 MPH from zero every five seconds,\n" + "3. Prompt the subject to alert operator when walking feels comfortable,\n" + "4. When subject alerts operator, press the 'Stop' button,\n" + "5. The speed when stopped will be recorded, \n" + "6. Press 'Calibrate' button again to decrease speed from 150% of previous recorded speed,\n" + "7. Prompt the subject to alert operator when walking feel comfortable,\n" + "8. When subject alerts operator, press the 'Stop' button,\n" + "9. The speed when stopped will be recorded,\n" + "10. The two recorded speeds will be averaged and saved as the Preferred Walking Speed,\n" + "11. Double check the final speed and press 'Save' to save the speed.", + font=('Purisa', 12), bg='white', justify=tkinter.LEFT) + label.pack() + + # Create a Button Widget in the Toplevel Window + label.place(x=10, y=10) + + # Create a Button Widget in the Toplevel Window + self.cal_button = tkinter.Button(popup_root, text="Calibrate", command=self.start_calibration_step, font=('Purisa', 12)) + self.cal_button.place(x=400, y=280, anchor=tkinter.SE, width=150, height=30) + self.stop_button = tkinter.Button(popup_root, text="Stop", command=self.stop_calibration_step, + font=('Purisa', 12)) + self.stop_button.place(x=400, y=280, anchor=tkinter.SW, width=150, height=30) + self.stop_button["state"] = 'disabled' + self.calibration_text_var = tkinter.StringVar(popup_root, value=f"Calibration Value: 0.0 MPH") + text = tkinter.Label(popup_root, textvariable=self.calibration_text_var, font=('Purisa', 12), bg='white') + text.place(x=400, y=240, anchor=tkinter.S) + center(popup_root) + popup_root.focus_force() + + def start_calibration_step(self): + if self.calibration_step == 0: + self.calibrating = True + cal_thread = threading.Thread(target=self.calibration_thread, args=(0.1, True)) + cal_thread.daemon = True + cal_thread.start() + self.calibration_step += 1 + elif self.calibration_step == 1: + self.calibrating = True + cal_thread = threading.Thread(target=self.calibration_thread, + args=(self.calibrated_speed_increasing * 1.5, False)) + cal_thread.daemon = True + cal_thread.start() + self.calibration_step += 1 + self.stop_button["state"] = 'active' + self.cal_button["state"] = 'disabled' + + def calibration_thread(self, start_value, increasing): + while self.calibrating: + if increasing: + for speed in np.arange(start_value, 20.0, 0.1): + self.calibrated_speed_increasing = speed + self.woodway.set_speed(float(speed), float(speed)) + self.calibration_text_var.set(f"Calibration Value: {self.calibrated_speed_increasing:.1f} MPH") + for i in range(0, 4): + time.sleep(0.25) + if not self.calibrating: + self.woodway.set_speed(float(0.0), float(0.0)) + return + else: + for speed in np.arange(start_value, 0.0, -0.1): + self.calibrated_speed_decreasing = speed + self.woodway.set_speed(float(speed), float(speed)) + self.calibration_text_var.set(f"Calibration Value: {self.calibrated_speed_decreasing:.1f} MPH") + for i in range(0, 4): + time.sleep(0.25) + if not self.calibrating: + self.woodway.set_speed(float(0.0), float(0.0)) + if self.calibrated_speed_increasing and self.calibrated_speed_decreasing: + self.calibrated_speed = np.average([self.calibrated_speed_increasing, + self.calibrated_speed_decreasing]) + self.calibration_text_var.set(f"Calibration Value: {self.calibrated_speed:.1f} MPH") + self.stop_button.config(text="Save", command=self.close_win) + return + + def stop_calibration_step(self): + self.calibrating = False + if self.calibration_step == 2: + self.stop_button["state"] = 'active' + self.cal_button["state"] = 'disabled' + else: + self.stop_button["state"] = 'disabled' + self.cal_button["state"] = 'active' + + def close_win(self): + if self.calibrated_speed: + self.caller.calibrate_return(self.calibrated_speed) + self.calibrating = False + self.popup_root.destroy() + + +class CalibrateVibrotactors: + def __init__(self, top, root, left_vta, right_vta): + assert top.calibrate_return + self.caller = top + self.entry = None + self.popup_root = None + self.name = "Calibrate Vibrotactors" + self.left_vta, self.right_vta = left_vta, right_vta + self.calibrating = False + self.calibrated_left_increasing = None + self.calibrated_right_increasing = None + self.calibrated_left_decreasing = None + self.calibrated_right_decreasing = None + self.calibrated_left = None + self.calibrated_right = None + self.calibration_step = 0 + self.popup_entry(root) + + def popup_entry(self, root): + # Create a Toplevel window + self.popup_root = popup_root = tkinter.Toplevel(root) + popup_root.config(bg="white", bd=-2) + popup_root.geometry("800x350") + popup_root.title(self.name) + + # Create an Entry Widget in the Toplevel window + label = tkinter.Label(popup_root, text="1. Press the 'Calibrate Left' button,\n" + "2. The vibrotactors will increase intensity by one from zero each second,\n" + "3. Prompt the subject to alert operator when vibrotactors are perceptible,\n" + "4. When subject alerts operator, press the 'Stop' button,\n" + "5. The intensity level when stopped will be recorded, \n" + "6. Press 'Calibrate Left' button again to decrease intensity from 150% of previous recorded intensity level,\n" + "7. Prompt the subject to alert operator when vibrotactors are perceptible,\n" + "8. When subject alerts operator, press the 'Stop' button,\n" + "9. The intensity level when stopped will be recorded,\n" + "10. The two recorded levels will be averaged and saved as the Left Sensory Perception Threshold,\n" + "11. Notice 'Calibrate Right' button, repeat procedure for left vibrotactor,\n" + "12. Double check the final values and press 'Save' to save the thresholds.", + font=('Purisa', 12), bg='white', justify=tkinter.LEFT) + label.place(x=10, y=10) + + # Create a Button Widget in the Toplevel Window + self.cal_button = tkinter.Button(popup_root, text="Calibrate Left", command=self.start_calibration_step, + font=('Purisa', 12)) + self.cal_button.place(x=400, y=330, anchor=tkinter.SE, width=150, height=30) + self.stop_button = tkinter.Button(popup_root, text="Stop", command=self.stop_calibration_step, + font=('Purisa', 12)) + self.stop_button.place(x=400, y=330, anchor=tkinter.SW, width=150, height=30) + self.stop_button["state"] = 'disabled' + self.calibration_text_var = tkinter.StringVar(popup_root, value=f"Calibration Left: 0\tCalibration Right: 0") + text = tkinter.Label(popup_root, textvariable=self.calibration_text_var, font=('Purisa', 12), bg='white') + text.place(x=400, y=290, anchor=tkinter.S) + center(popup_root) + popup_root.focus_force() + + def start_calibration_step(self): + if self.calibration_step == 0: + self.calibrating = True + cal_thread = threading.Thread(target=self.calibration_thread, args=(self.left_vta, 0, True, True)) + cal_thread.daemon = True + cal_thread.start() + self.calibration_step += 1 + elif self.calibration_step == 1: + self.calibrating = True + cal_thread = threading.Thread(target=self.calibration_thread, + args=(self.left_vta, int(self.calibrated_left_increasing * 1.5), + False, True)) + cal_thread.daemon = True + cal_thread.start() + self.calibration_step += 1 + elif self.calibration_step == 2: + self.calibrating = True + cal_thread = threading.Thread(target=self.calibration_thread, + args=(self.right_vta, 0, True, False)) + cal_thread.daemon = True + cal_thread.start() + self.calibration_step += 1 + elif self.calibration_step == 3: + self.calibrating = True + cal_thread = threading.Thread(target=self.calibration_thread, + args=(self.right_vta, int(self.calibrated_right_increasing * 1.5), + False, False)) + cal_thread.daemon = True + cal_thread.start() + self.calibration_step += 1 + self.stop_button["state"] = 'active' + self.cal_button["state"] = 'disabled' + + def calibration_thread(self, vta, start_value, increasing, side): + while self.calibrating: + if side: + if increasing: + for speed in np.arange(start_value, 255, 1): + self.calibrated_left_increasing = speed + vta.write_all_motors(speed) + self.calibration_text_var.set(f"Calibration Left: {self.calibrated_left_increasing}" + f"\tCalibration Right: 0") + for i in range(0, 4): + time.sleep(0.25) + if not self.calibrating: + vta.write_all_motors(0) + return + else: + for speed in np.arange(start_value, 0, -1): + self.calibrated_left_decreasing = speed + vta.write_all_motors(speed) + self.calibration_text_var.set(f"Calibration Left: {self.calibrated_left_decreasing}" + f"\tCalibration Right: 0") + for i in range(0, 4): + time.sleep(0.25) + if not self.calibrating: + vta.write_all_motors(0) + if self.calibrated_left_decreasing and self.calibrated_left_increasing: + self.calibrated_left = int(np.average([self.calibrated_left_increasing, + self.calibrated_left_decreasing])) + self.calibration_text_var.set(f"Calibration Left: {self.calibrated_left}" + f"\tCalibration Right: 0") + self.cal_button.config(text="Calibrate Right") + return + else: + if increasing: + for speed in np.arange(start_value, 255, 1): + self.calibrated_right_increasing = speed + vta.write_all_motors(speed) + self.calibration_text_var.set(f"Calibration Left: {self.calibrated_left}" + f"\tCalibration Right: {self.calibrated_right_increasing}") + for i in range(0, 4): + time.sleep(0.25) + if not self.calibrating: + vta.write_all_motors(0) + return + else: + for speed in np.arange(start_value, 0, -1): + self.calibrated_right_decreasing = speed + vta.write_all_motors(speed) + self.calibration_text_var.set(f"Calibration Left: {self.calibrated_left}" + f"\tCalibration Right: {self.calibrated_right_decreasing}") + for i in range(0, 4): + time.sleep(0.25) + if not self.calibrating: + vta.write_all_motors(0) + if self.calibrated_right_decreasing and self.calibrated_right_increasing: + self.calibrated_right = int(np.average([self.calibrated_right_decreasing, + self.calibrated_right_increasing])) + self.calibration_text_var.set(f"Calibration Left: {self.calibrated_left}" + f"\tCalibration Right: {self.calibrated_right}") + self.stop_button.config(text="Save", command=self.close_win) + return + + def stop_calibration_step(self): + self.calibrating = False + if self.calibration_step == 4: + self.cal_button["state"] = 'disabled' + self.stop_button["state"] = 'active' + else: + self.stop_button["state"] = 'disabled' + self.cal_button["state"] = 'active' + + def close_win(self): + if self.calibrated_left and self.calibrated_right: + self.caller.calibrate_return(self.calibrated_left, self.calibrated_right) + self.calibrating = False + self.popup_root.destroy() diff --git a/ui_params.py b/ui_params.py index 38bf690..373fb8b 100644 --- a/ui_params.py +++ b/ui_params.py @@ -1,4 +1,4 @@ -cometrics_version = "1.0.13" +cometrics_version = "1.1.0" ui_title = f"cometrics v{cometrics_version}" cometrics_data_root = fr'C:\cometrics' diff --git a/user_guide/Cometrics User Guide_v5.docx b/user_guide/Cometrics User Guide_v5.docx new file mode 100644 index 0000000..0b60896 Binary files /dev/null and b/user_guide/Cometrics User Guide_v5.docx differ diff --git a/user_guide/Cometrics User Guide_v5.pdf b/user_guide/Cometrics User Guide_v5.pdf new file mode 100644 index 0000000..5c57daa Binary files /dev/null and b/user_guide/Cometrics User Guide_v5.pdf differ