valve_testing_window
This module defines the ValveTestWindow class, a Tkinter Toplevel window
providing functionality for testing and priming valves. Test and prime procedures are
facilitated via the controllers.arduino_control.ArduinoManager
.
It allows users to:
- Select individual valves for testing or priming.
- Configure test parameters like desired dispense volume and number of actuations.
- Initiate automated valve tests, calculating new opening durations based on user-provided dispensed volumes.
- Initiate valve test/priming sequences.
- Manually override and adjust valve timings via a separate window (
ManualTimeAdjustment
). - View test results and current valve timings in a table format.
- Confirm or abort automatically calculated timing changes.
This window interacts heavily with the controllers.arduino_control.ArduinoManager
controller for communication
and the modesl.arduino_data.ArduinoData
model for storing/retrieving valve duration configurations.
It uses views.gui_common.GUIUtils
for common UI elements and window management.
1""" 2This module defines the ValveTestWindow class, a Tkinter Toplevel window 3providing functionality for testing and priming valves. Test and prime procedures are 4facilitated via the `controllers.arduino_control.ArduinoManager`. 5 6It allows users to: 7- Select individual valves for testing or priming. 8- Configure test parameters like desired dispense volume and number of actuations. 9- Initiate automated valve tests, calculating new opening durations based on 10 user-provided dispensed volumes. 11- Initiate valve test/priming sequences. 12- Manually override and adjust valve timings via a separate window (`ManualTimeAdjustment`). 13- View test results and current valve timings in a table format. 14- Confirm or abort automatically calculated timing changes. 15 16This window interacts heavily with the `controllers.arduino_control.ArduinoManager` controller for communication 17and the `modesl.arduino_data.ArduinoData` model for storing/retrieving valve duration configurations. 18 19It uses `views.gui_common.GUIUtils` for common UI elements and window management. 20""" 21 22import tkinter as tk 23from tkinter import simpledialog 24from tkinter import ttk 25 26 27import numpy as np 28 29### USED FOR TYPE HINTING ### 30import numpy.typing as npt 31 32from enum import Enum 33import threading 34import logging 35import toml 36 37from controllers.arduino_control import ArduinoManager 38from models.arduino_data import ArduinoData 39from views.gui_common import GUIUtils 40from views.valve_testing.manual_time_adjustment_window import ManualTimeAdjustment 41from views.valve_testing.valve_changes_window import ValveChanges 42import system_config 43 44 45logger = logging.getLogger() 46"""Get the logger in use for the app.""" 47 48rig_config = system_config.get_rig_config() 49 50with open(rig_config, "r") as f: 51 VALVE_CONFIG = toml.load(f)["valve_config"] 52 53# pull total valves constant from toml config 54TOTAL_VALVES = VALVE_CONFIG["TOTAL_CURRENT_VALVES"] 55 56VALVES_PER_SIDE = TOTAL_VALVES // 2 57 58 59class WindowMode(Enum): 60 """ 61 Represents the two primary operational modes of the ValveTestWindow: 62 Testing mode for calibrating valve durations, and Priming mode for 63 actuating valves repeatedly without calibration. 64 """ 65 66 TESTING = "Testing" 67 PRIMING = "Priming" 68 69 70class ValveTestWindow(tk.Toplevel): 71 """ 72 *This is a bit of a god class and should likely be refactored... but for now... it works!* 73 74 Implements the main Tkinter Toplevel window for valve testing and priming operations. 75 76 This window provides an interface to interact with the Arduino 77 controller for calibrating valve open times based on dispensed volume or 78 for running priming sequences. It features modes for testing and 79 priming, manages valve selections, displays test progress and results, 80 and handles communication with the Arduino. 81 82 Attributes 83 ---------- 84 - **`arduino_controller`** (*ArduinoManager*): Reference to the main Arduino controller instance, used for sending commands and data. 85 - **`arduino_data`** (*ArduinoData*): Reference to the data model holding valve duration configurations. 86 - **`window_mode`** (*WindowMode*): Enum indicating the current operational mode (Testing or Priming). 87 - **`test_running`** (*bool*): Flag indicating if an automated test sequence is currently active. 88 - **`prime_running`** (*bool*): Flag indicating if a priming sequence is currently active. 89 - **`desired_volume`** (*tk.DoubleVar*): Tkinter variable storing the target volume (in microliters) per actuation for automatic duration calculation. 90 - **`actuations`** (*tk.IntVar*): Tkinter variable storing the number of times each valve should be actuated during a test or prime sequence. 91 - **`ml_dispensed`** (*list[tuple[int, float]]*): Stores tuples of (valve_number, dispensed_volume_ml) entered by the user during a test. 92 - **`valve_buttons`** (*list[tk.Button | None]*): List holding the Tkinter Button widgets for each valve selection. 93 - **`valve_test_button`** (*tk.Button | None*): The main button used to start/abort tests or start/stop priming. 94 - **`table_entries`** (*list[str | None]*): List holding the item IDs for each row in the ttk.Treeview table, used for modifying/hiding/showing specific rows. 95 - **`valve_selections`** (*npt.NDArray[np.int8]*): Numpy array representing the selection state of each valve (0 if not selected, valve_number if selected). 96 - **`side_one_tests`** (*npt.NDArray[np.int8] | None*): Numpy array holding the valve numbers (0-indexed) selected for testing/priming on side one. 97 - **`side_two_tests`** (*npt.NDArray[np.int8] | None*): Numpy array holding the valve numbers (0-indexed) selected for testing/priming on side two. 98 - **`stop_event`** (*threading.Event*): Event flag used to signal the Arduino listener thread (`run_test`) to stop. 99 - **`mode_button`** (*tk.Button*): Button to switch between Testing and Priming modes. 100 - **`dispensed_vol_frame`** (*tk.Frame*): Container frame for the desired volume input elements (visible in Testing mode). 101 - **`valve_table_frame`** (*tk.Frame*): Container frame for the valve test results table (visible in Testing mode). 102 - **`valve_table`** (*ttk.Treeview*): The table widget displaying valve numbers, test status, and timings. 103 - **`valve_buttons_frame`** (*tk.Frame*): Main container frame for the side-by-side valve selection buttons. 104 - **`side_one_valves_frame`** (*tk.Frame*): Frame holding valve selection buttons for the first side. 105 - **`side_two_valves_frame`** (*tk.Frame*): Frame holding valve selection buttons for the second side. 106 - **`manual_adjust_window`** (*ManualTimeAdjustment*): Instance of the separate window for manual timing adjustments. 107 108 Methods 109 ------- 110 - `setup_basic_window_attr`() 111 Configures basic window properties like title, key bindings, and icon. 112 - `show`() 113 Makes the window visible (deiconifies). 114 - `switch_window_mode`() 115 Toggles the window between Testing and Priming modes, adjusting UI elements accordingly. 116 - `create_interface`() 117 Builds the entire GUI layout, placing widgets and frames. 118 - `start_testing_toggle`() 119 Handles the press of the main test/prime button in Testing mode (starts or aborts test). 120 - `create_buttons`() 121 Creates and places the valve selection buttons and the main start/abort button. 122 - `create_valve_test_table`() 123 Creates the ttk.Treeview table for displaying valve test results and timings. 124 - `update_table_entry_test_status`(...) 125 Updates the 'Amount Dispensed' column for a specific valve in the table. 126 - `update_table_entries`(...) 127 Updates the entire valve table, showing/hiding rows based on selections and refreshing currently loaded durations. 128 - `toggle_valve_button`(...) 129 Handles clicks on valve selection buttons, updating their appearance and the `valve_selections` array. 130 - `verify_variables`(...) 131 Reads verification data back from Arduino to confirm test parameters were received correctly. 132 - `verify_schedule`(...) 133 Reads verification data back from Arduino to confirm the valve test/prime schedule was received correctly. 134 - `send_schedules`() 135 Sends the command, parameters (schedule lengths, actuations), and valve schedule to the Arduino. Handles mode-specific commands and confirmations. 136 - `stop_priming`() 137 Sends a command to the Arduino to stop an ongoing priming sequence. 138 - `take_input`(...) 139 Prompts the user (via simpledialog) to enter the dispensed volume for a completed valve test pair. Sends confirmation back to Arduino. 140 - `run_test`() 141 Runs in a separate thread to listen for messages from the Arduino during a test, triggering events for input prompts or completion. 142 - `abort_test`() 143 Sends a command to the Arduino to abort the current test and stops the listener thread. 144 - `testing_complete`(...) 145 Handles the event triggered when the Arduino signals the end of the entire test sequence. Initiates duration updates. 146 - `auto_update_durations`() 147 Calculates new valve durations based on test results (`ml_dispensed`) and opens the `ValveChanges` confirmation window. 148 - `confirm_valve_changes`(...) 149 Callback function executed if the user confirms changes in the `ValveChanges` window. Saves new durations and archives old ones via `ArduinoData`. 150 """ 151 152 def __init__(self, arduino_controller: ArduinoManager) -> None: 153 """ 154 Initializes the ValveTestWindow. 155 156 Parameters 157 ---------- 158 - **arduino_controller** (*ArduinoManager*): The application's instance of the Arduino controller. 159 160 Raises 161 ------ 162 - Propagates exceptions from `GUIUtils` methods during icon setting. 163 - Propagates exceptions from `arduino_data.load_durations()`. 164 """ 165 super().__init__() 166 self.arduino_controller: ArduinoManager = arduino_controller 167 self.arduino_data: ArduinoData = arduino_controller.arduino_data 168 169 self.setup_basic_window_attr() 170 171 self.window_mode: WindowMode = WindowMode.TESTING 172 173 self.test_running: bool = False 174 self.prime_running: bool = False 175 176 self.desired_volume: tk.DoubleVar = tk.DoubleVar(value=5) 177 178 self.actuations: tk.IntVar = tk.IntVar(value=1000) 179 180 self.ml_dispensed: list[tuple[int, float]] = [] 181 182 self.valve_buttons: list[tk.Button | None] = [None] * TOTAL_VALVES 183 184 self.valve_test_button: tk.Button | None = None 185 186 self.table_entries: list[str | None] = [None] * TOTAL_VALVES 187 188 self.valve_selections: npt.NDArray[np.int8] = np.zeros( 189 (TOTAL_VALVES,), dtype=np.int8 190 ) 191 192 self.side_one_tests: None | npt.NDArray[np.int8] = None 193 self.side_two_tests: None | npt.NDArray[np.int8] = None 194 195 self.stop_event: threading.Event = threading.Event() 196 197 self.create_interface() 198 199 # hide main window for now until we deliberately enter this window. 200 self.withdraw() 201 202 self.manual_adjust_window = ManualTimeAdjustment( 203 self.arduino_controller.arduino_data 204 ) 205 # hide the adjustment window until we need it. 206 self.manual_adjust_window.withdraw() 207 208 logging.info("Valve Test Window initialized.") 209 210 def setup_basic_window_attr(self) -> None: 211 """ 212 Sets up fundamental window properties. 213 214 Configures the window title, binds Ctrl+W and the close button ('X') 215 to hide the window, sets grid column/row weights for resizing behavior, 216 and applies the application icon using `GUIUtils`. 217 """ 218 219 self.title("Valve Testing") 220 self.bind("<Control-w>", lambda event: self.withdraw()) 221 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 222 223 self.grid_columnconfigure(0, weight=1) 224 225 for i in range(5): 226 self.grid_rowconfigure(i, weight=1) 227 self.grid_rowconfigure(0, weight=0) 228 229 window_icon_path = GUIUtils.get_window_icon_path() 230 GUIUtils.set_program_icon(self, icon_path=window_icon_path) 231 232 def show(self) -> None: 233 """ 234 Makes the ValveTestWindow visible. 235 236 Calls `deiconify()` to show the window if it was previously hidden (withdrawn). 237 """ 238 self.deiconify() 239 240 def switch_window_mode(self) -> None: 241 """ 242 Toggles the window's operational mode between Testing and Priming. 243 244 Updates the window title and mode button text/color. Hides or shows 245 mode-specific UI elements (desired volume input, test results table). 246 Changes the command associated with the main start/stop button. 247 Prevents mode switching if a test or prime sequence is currently running to avoid Arduino 248 Communications errors. Resizes and centers the window after GUI changes. 249 """ 250 251 if self.window_mode == WindowMode.TESTING: 252 if self.test_running: 253 # if the test is currently running we do not want to switch modes. Stop testing to switch modes 254 GUIUtils.display_error( 255 "ACTION PROHIBITED", 256 "You cannot switch modes while a test is running. If you'd like to switch modes, please abort the test and try again.", 257 ) 258 return 259 self.title("Valve Priming") 260 261 self.window_mode = WindowMode.PRIMING 262 263 self.mode_button.configure(text="Switch to Testing Mode", bg="DeepSkyBlue3") 264 265 # orig. row 1 266 self.dispensed_vol_frame.grid_forget() 267 # orig. row 3 268 self.valve_table_frame.grid_forget() 269 270 # start testing button -> start priming button / command 271 272 if isinstance(self.valve_test_button, tk.Button): 273 self.valve_test_button.configure( 274 text="Start Priming", 275 bg="coral", 276 command=lambda: self.send_schedules(), 277 ) 278 279 else: 280 if self.prime_running: 281 # if the test is currently running we do not want to switch modes. Stop testing to switch modes 282 GUIUtils.display_error( 283 "ACTION PROHIBITED", 284 "You cannot switch modes while a valve prime operation is running. If you'd like to switch modes, please abort the prime and try again.", 285 ) 286 return 287 288 self.title("Valve Testing") 289 290 self.window_mode = WindowMode.TESTING 291 292 self.mode_button.configure(text="Switch to Priming Mode", bg="coral") 293 294 self.valve_table_frame.grid( 295 row=3, column=0, sticky="nsew", pady=10, padx=10 296 ) 297 self.dispensed_vol_frame.grid(row=1, column=0, padx=5, sticky="nsew") 298 299 if isinstance(self.valve_test_button, tk.Button): 300 self.valve_test_button.configure( 301 text="Start Testing", 302 bg="green", 303 command=lambda: self.start_testing_toggle(), 304 ) 305 306 # resize the window to fit current content 307 self.update_idletasks() 308 GUIUtils.center_window(self) 309 310 def create_interface(self) -> None: 311 """ 312 Constructs and arranges all GUI elements within the window. 313 314 Creates frames for structure, input fields (desired volume, actuations), 315 the mode switch button, the valve selection buttons, the test results table, 316 and the main start/abort/prime button. Uses `GUIUtils` for 317 widget creation where possible. Calls helper methods `create_valve_test_table` 318 and `create_buttons`. Centers the window after creation. 319 """ 320 321 self.mode_button = GUIUtils.create_button( 322 self, 323 "Switch to Priming Mode", 324 command=lambda: self.switch_window_mode(), 325 bg="coral", 326 row=0, 327 column=0, 328 )[1] 329 330 # create a new frame to contain the desired dispensed volume label and frame 331 self.dispensed_vol_frame = tk.Frame(self) 332 self.dispensed_vol_frame.grid(row=1, column=0, padx=5, sticky="nsew") 333 334 self.dispensed_vol_frame.grid_columnconfigure(0, weight=1) 335 self.dispensed_vol_frame.grid_rowconfigure(0, weight=1) 336 self.dispensed_vol_frame.grid_rowconfigure(1, weight=1) 337 338 label = tk.Label( 339 self.dispensed_vol_frame, 340 text="Desired Dispensed Volume in Microliters (ul)", 341 bg="light blue", 342 font=("Helvetica", 20), 343 highlightthickness=1, 344 highlightbackground="dark blue", 345 height=2, 346 width=50, 347 ) 348 label.grid(row=0, pady=5) 349 350 entry = tk.Entry( 351 self.dispensed_vol_frame, 352 textvariable=self.desired_volume, 353 font=("Helvetica", 24), 354 highlightthickness=1, 355 highlightbackground="black", 356 ) 357 entry.grid(row=1, sticky="nsew", pady=5, ipady=14) 358 359 # create another new frame to contain the amount of times each 360 # valve should actuate label and frame 361 frame = tk.Frame(self) 362 frame.grid(row=2, column=0, padx=5, sticky="nsew") 363 364 frame.grid_columnconfigure(0, weight=1) 365 frame.grid_rowconfigure(0, weight=1) 366 frame.grid_rowconfigure(1, weight=1) 367 368 label = tk.Label( 369 frame, 370 text="How Many Times Should the Valve Actuate?", 371 bg="light blue", 372 font=("Helvetica", 20), 373 highlightthickness=1, 374 highlightbackground="dark blue", 375 height=2, 376 width=50, 377 ) 378 label.grid(row=0, pady=5) 379 380 entry = tk.Entry( 381 frame, 382 textvariable=self.actuations, 383 font=("Helvetica", 24), 384 highlightthickness=1, 385 highlightbackground="black", 386 ) 387 entry.grid(row=1, sticky="nsew", pady=5, ipady=12) 388 389 ### ROW 3 ### 390 self.create_valve_test_table() 391 392 # setup frames for valve buttons 393 self.valve_buttons_frame = tk.Frame( 394 self, highlightbackground="black", highlightthickness=1 395 ) 396 ### ROW 4 ### 397 self.valve_buttons_frame.grid(row=4, column=0, pady=10, padx=10, sticky="nsew") 398 self.valve_buttons_frame.grid_columnconfigure(0, weight=1) 399 self.valve_buttons_frame.grid_rowconfigure(0, weight=1) 400 401 self.side_one_valves_frame = tk.Frame( 402 self.valve_buttons_frame, highlightbackground="black", highlightthickness=1 403 ) 404 self.side_one_valves_frame.grid(row=0, column=0, pady=10, padx=10, sticky="w") 405 self.side_one_valves_frame.grid_columnconfigure(0, weight=1) 406 for i in range(4): 407 self.side_one_valves_frame.grid_rowconfigure(i, weight=1) 408 409 self.side_two_valves_frame = tk.Frame( 410 self.valve_buttons_frame, highlightbackground="black", highlightthickness=1 411 ) 412 self.side_two_valves_frame.grid(row=0, column=1, pady=10, padx=10, sticky="e") 413 self.side_two_valves_frame.grid_columnconfigure(0, weight=1) 414 for i in range(4): 415 self.side_two_valves_frame.grid_rowconfigure(i, weight=1) 416 417 ### ROW 5 ### 418 self.create_buttons() 419 420 self.update_idletasks() 421 GUIUtils.center_window(self) 422 423 def start_testing_toggle(self) -> None: 424 """ 425 Handles the action of the main button when in Testing mode. 426 427 If a test is running, it calls `abort_test()` and updates the button appearance. 428 If no test is running, it calls `send_schedules()` to initiate a new test. 429 """ 430 431 if self.test_running: 432 if isinstance(self.valve_test_button, tk.Button): 433 self.valve_test_button.configure(text="Start Testing", bg="green") 434 self.abort_test() 435 else: 436 self.send_schedules() 437 438 def create_buttons(self) -> None: 439 """ 440 Creates and places the individual valve selection buttons and the main action button. 441 442 Generates buttons for each valve (1 to TOTAL_VALVES), arranging them 443 into side-one and side-two frames using `GUIUtils.create_button`. 444 Assigns the `toggle_valve_button` command to each valve button. 445 446 Creates the main 'Start Testing' / 'Start Priming' / 'Abort' button. 447 """ 448 for i in range(VALVES_PER_SIDE): 449 # GUIUtils.create_button returns both the buttons frame and button object in a tuple. using [1] on the 450 # return item yields the button object. 451 452 # return the side one button and put it on the side one 'side' 453 # of the list (0-4) 454 self.valve_buttons[i] = GUIUtils.create_button( 455 self.side_one_valves_frame, 456 f"Valve {i + 1}", 457 lambda i=i: self.toggle_valve_button(self.valve_buttons[i], i), 458 "light blue", 459 i, 460 0, 461 )[1] 462 463 # return the side two button and put it on the side two 'side' 464 # of the list (5-8) 465 self.valve_buttons[i + (VALVES_PER_SIDE)] = GUIUtils.create_button( 466 self.side_two_valves_frame, 467 f"Valve {i + (VALVES_PER_SIDE) + 1}", 468 lambda i=i: self.toggle_valve_button( 469 self.valve_buttons[i + (VALVES_PER_SIDE)], i + (VALVES_PER_SIDE) 470 ), 471 "light blue", 472 i, 473 0, 474 )[1] 475 476 self.valve_test_button = GUIUtils.create_button( 477 self, "Start Testing", lambda: self.start_testing_toggle(), "green", 5, 0 478 )[1] 479 480 def create_valve_test_table(self) -> None: 481 """ 482 Creates, configures, and populates the initial state of the ttk.Treeview table. 483 484 Sets up the table columns ("Valve", "Amount Dispensed...", "Testing Opening Time..."). 485 Adds a button above the table to open the `ManualTimeAdjustment` window. 486 487 Loads the currently selected valve durations from `models.arduino_data.ArduinoData`. 488 Inserts initial rows for all possible valves, marking them as "Test Not Complete" 489 and displaying their current duration. Stores row IDs in `self.table_entries` for later access. 490 """ 491 492 # this function creates and labels the testing window table that displays the information recieved from the arduino reguarding the results of the tes 493 self.valve_table_frame = tk.Frame( 494 self, highlightbackground="black", highlightthickness=1 495 ) 496 self.valve_table_frame.grid(row=3, column=0, sticky="nsew", pady=10, padx=10) 497 # tell the frame to fill the available width 498 self.valve_table_frame.grid_columnconfigure(0, weight=1) 499 500 self.valve_table = ttk.Treeview( 501 self.valve_table_frame, show="headings", height=12 502 ) 503 504 headings = [ 505 "Valve", 506 "Amount Dispensed Per Lick (ul)", 507 "Testing Opening Time of (ms)", 508 ] 509 510 self.valve_table["columns"] = headings 511 512 self.valve_table.grid(row=1, sticky="ew") 513 514 button = tk.Button( 515 self.valve_table_frame, 516 text="Manual Valve Duration Override", 517 command=lambda: self.manual_adjust_window.show(), 518 bg="light blue", 519 highlightbackground="black", 520 highlightthickness=1, 521 ) 522 523 button.grid(row=0, sticky="e") 524 525 # Configure the columns 526 for col in self.valve_table["columns"]: 527 self.valve_table.heading(col, text=col) 528 529 arduino_data = self.arduino_controller.arduino_data 530 531 side_one_durations, side_two_durations, date_used = ( 532 arduino_data.load_durations() 533 ) 534 535 # insert default entries for each total valve until user selects how many 536 # they want to test 537 for i in range(VALVES_PER_SIDE): 538 self.table_entries[i] = self.valve_table.insert( 539 "", 540 i, 541 values=( 542 f"{i + 1}", 543 "Test Not Complete", 544 f"{side_one_durations[i]} ms", 545 ), 546 ) 547 self.table_entries[i + 4] = self.valve_table.insert( 548 "", 549 i + VALVES_PER_SIDE, 550 values=( 551 f"{i + VALVES_PER_SIDE + 1}", 552 "Test Not Complete", 553 f"{side_two_durations[i]} ms", 554 ), 555 ) 556 557 def update_table_entry_test_status(self, valve: int, l_per_lick: float) -> None: 558 """ 559 Updates the 'Amount Dispensed Per Lick (ul)' column for a specific valve row. 560 561 Parameters 562 ---------- 563 - **valve** (*int*): The 1-indexed valve number whose row needs updating. 564 - **l_per_lick** (*float*): The calculated volume dispensed per lick in milliliters (mL). 565 """ 566 567 self.valve_table.set( 568 self.table_entries[valve - 1], column=1, value=f"{l_per_lick * 1000} uL" 569 ) 570 571 def update_table_entries(self) -> None: 572 """ 573 Refreshes the entire valve test table based on current selections and durations. 574 575 Hides rows for unselected valves and shows rows for selected valves. 576 Reloads the latest durations from `ArduinoData` and updates the 577 'Testing Opening Time' column for all visible rows. Resets the 578 'Amount Dispensed' column to "Test Not Complete" for visible rows. 579 """ 580 side_one, side_two, _ = self.arduino_data.load_durations() 581 # np array where the entry is not zero but the valve number itself 582 selected_valves = self.valve_selections[self.valve_selections != 0] 583 584 # for all valve numbers, if that number is in selected_valves -1 (the entire array 585 # decremented by one) place it in the table and ensure its duration is up-to-date 586 for i in range(TOTAL_VALVES): 587 # if the iter number is also contained in the selected valves arr 588 if i in (selected_valves - 1): 589 # check if it's side two or side one 590 if i >= VALVES_PER_SIDE: 591 duration = side_two[i - VALVES_PER_SIDE] 592 else: 593 duration = side_one[i] 594 self.valve_table.item( 595 self.table_entries[i], 596 values=( 597 f"{i + 1}", 598 "Test Not Complete", 599 f"{duration} ms", 600 ), 601 ) 602 self.valve_table.reattach(self.table_entries[i], "", i) 603 else: 604 self.valve_table.detach(self.table_entries[i]) 605 606 logging.info("Valve test table updated.") 607 608 def toggle_valve_button(self, button: tk.Button, valve_num: int) -> None: 609 """ 610 Handles the click event for a valve selection button. 611 612 Toggles the button's visual state (raised/sunken). Updates the 613 corresponding entry in the `self.valve_selections` numpy array (0 for 614 deselected, valve_number + 1 for selected). Calls `update_table_entries` 615 to refresh the table display. Filling arrays this way allows us to tell 616 the arduino exactly which valves should be toggled (1, 4, 7) as long as 617 their value in the array is not 0. 618 619 Parameters 620 ---------- 621 - **button** (*tk.Button*): The specific valve button widget that was clicked. 622 - **valve_num** (*int*): The 0-indexed number of the valve corresponding to the button. 623 """ 624 # Attempt to get the value to ensure it's a valid float as expected 625 # If successful, call the function to update/create the valve test table 626 if self.valve_selections[valve_num] == 0: 627 button.configure(relief="sunken") 628 self.valve_selections[valve_num] = valve_num + 1 629 630 else: 631 button.configure(relief="raised") 632 self.valve_selections[valve_num] = 0 633 self.update_table_entries() 634 635 def verify_variables(self, original_data: bytes) -> bool: 636 """ 637 Verifies that the Arduino correctly received the test/prime parameters. 638 639 Waits for and reads back the schedule lengths and actuation count echoed 640 by the Arduino. Compares the received bytes with the originally sent data. 641 642 Parameters 643 ---------- 644 - **original_data** (*bytes*): The byte string containing schedule lengths and actuation count originally sent to the Arduino. 645 646 Returns 647 ------- 648 - *bool*: `True` if the received data matches the original data, `False` otherwise (logs error and shows message box on mismatch). 649 """ 650 # wait until all data is on the wire ready to be read (4 bytes), 651 # then read it all in quick succession 652 if self.arduino_controller.arduino is None: 653 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 654 GUIUtils.display_error("TRANSMISSION ERROR", msg) 655 logger.error(msg) 656 return False 657 658 while self.arduino_controller.arduino.in_waiting < 4: 659 pass 660 661 ver_len_sched_sd_one = self.arduino_controller.arduino.read() 662 ver_len_sched_sd_two = self.arduino_controller.arduino.read() 663 664 ver_valve_act_time = self.arduino_controller.arduino.read(2) 665 666 verification_data = ( 667 ver_len_sched_sd_one + ver_len_sched_sd_two + ver_valve_act_time 668 ) 669 if verification_data == original_data: 670 logger.info("====VERIFIED TEST VARIABLES====") 671 return True 672 else: 673 msg = "====INCORRECT VARIABLES DETECTED, TRY AGAIN====" 674 GUIUtils.display_error("TRANSMISSION ERROR", msg) 675 logger.error(msg) 676 return False 677 678 def verify_schedule( 679 self, len_sched: int, sent_schedule: npt.NDArray[np.int8] 680 ) -> bool: 681 """ 682 Verifies that the Arduino correctly received the valve schedule. 683 684 Waits for and reads back the valve numbers (0-indexed) echoed byte-by-byte 685 by the Arduino. Compares the reconstructed numpy array with the schedule 686 that was originally sent. 687 688 Parameters 689 ---------- 690 - **len_sched** (*int*): The total number of valves in the schedule being sent. 691 - **sent_schedule** (*npt.NDArray[np.int8]*): The numpy array (0-indexed valve numbers) representing the schedule originally sent. 692 693 Returns 694 ------- 695 - *bool*: `True` if the received schedule matches the sent schedule, `False` otherwise (logs error and shows message box on mismatch). 696 """ 697 # np array used for verification later 698 received_sched = np.zeros((len_sched,), dtype=np.int8) 699 700 if self.arduino_controller.arduino is None: 701 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 702 GUIUtils.display_error("TRANSMISSION ERROR", msg) 703 logger.error(msg) 704 return False 705 706 while self.arduino_controller.arduino.in_waiting < len_sched: 707 pass 708 709 for i in range(len_sched): 710 received_sched[i] = int.from_bytes( 711 self.arduino_controller.arduino.read(), byteorder="little", signed=False 712 ) 713 714 if np.array_equal(received_sched, sent_schedule): 715 logger.info(f"===TESTING SCHEDULE VERIFIED as ==> {received_sched}===") 716 logger.info("====================BEGIN TESTING NOW====================") 717 return True 718 else: 719 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 720 GUIUtils.display_error("TRANSMISSION ERROR", msg) 721 logger.error(msg) 722 return False 723 724 def send_schedules(self) -> None: 725 """ 726 Performs the process of sending test or prime configuration to the Arduino. 727 728 Determines selected valves and schedule lengths for each side. Sends current 729 valve durations to Arduino. Then sends the appropriate command ('TEST VOL' or 'PRIME VALVES'). 730 731 Sends test parameters (schedule lengths, actuations) and verifies them using `verify_variables`. 732 Sends the actual valve schedule (0-indexed) and verifies it using `verify_schedule`. 733 734 User is then prompted to confirm before proceeding. 735 If confirmed, initiates the test/prime sequence on the Arduino 736 and starts the listener thread (`run_test`) if Testing, or sends start command if Priming. 737 Updates button states accordingly. 738 """ 739 len_sched_one = np.count_nonzero(self.valve_selections[:VALVES_PER_SIDE]) 740 len_sched_two = np.count_nonzero(self.valve_selections[VALVES_PER_SIDE:]) 741 742 len_sched_one = np.int8(len_sched_one) 743 len_sched_two = np.int8(len_sched_two) 744 len_sched = len_sched_one + len_sched_two 745 746 # because self.actuations is a IntVar, we use .get() method to get its value 747 valve_acuations = np.int16(self.actuations.get()) 748 749 # schedule is TOTAL_VALVES long to accomodate testing of up to all valves at one time. 750 # if we are not testing them all, filter out the zero entries (valves we are not testing) 751 schedule = self.valve_selections[self.valve_selections != 0] 752 753 # decrement each element in the array by one to move to zero-indexing. We moved to 1 754 # indexing becuase using zero indexing does not work with the method of toggling valves 755 # defined above 756 schedule -= 1 757 758 self.side_one_tests = schedule[:len_sched_one] 759 self.side_two_tests = schedule[len_sched_one:] 760 761 len_sched_one = len_sched_one 762 len_sched_two = len_sched_two 763 valve_acuations = valve_acuations 764 765 self.arduino_controller.send_valve_durations() 766 if self.window_mode == WindowMode.TESTING: 767 ###====SENDING TEST COMMAND====### 768 command = "TEST VOL\n".encode("utf-8") 769 self.arduino_controller.send_command(command) 770 771 elif self.window_mode == WindowMode.PRIMING: 772 self.prime_running = True 773 774 command = "PRIME VALVES\n".encode("utf-8") 775 self.arduino_controller.send_command(command) 776 777 ###====SENDING TEST VARIABLES (len side one, two, num_actuations per test)====### 778 data_bytes = ( 779 len_sched_one.tobytes() 780 + len_sched_two.tobytes() 781 + valve_acuations.tobytes() 782 ) 783 self.arduino_controller.send_command(data_bytes) 784 785 # verify the data 786 if not self.verify_variables(data_bytes): 787 return 788 789 ###====SENDING SCHEDULE====### 790 sched_data_bytes = schedule.tobytes() 791 self.arduino_controller.send_command(sched_data_bytes) 792 793 if not self.verify_schedule(len_sched, schedule): 794 return 795 796 schedule += 1 797 798 valves = ",".join(str(valve) for valve in schedule) 799 if self.window_mode == WindowMode.TESTING: 800 ####### ARDUINO HALTS HERE UNTIL THE GO-AHEAD IS GIVEN ####### 801 # inc each sched element by one so that it is 1-indexes and matches the valve labels 802 test_confirmed = GUIUtils.askyesno( 803 "CONFIRM THE TEST SCHEDULE", 804 f"Valves {valves}, will be tested. Review the test table to confirm schedule and timings. Each valve will be actuated {valve_acuations} times. Ok to begin?", 805 ) 806 807 ### include section to manually modify timings 808 if test_confirmed: 809 self.bind("<<event0>>", self.testing_complete) 810 self.bind("<<event1>>", self.take_input) 811 threading.Thread(target=self.run_test).start() 812 else: 813 self.abort_test() 814 elif self.window_mode == WindowMode.PRIMING: 815 priming_confirmed = GUIUtils.askyesno( 816 "CONFIRM THE ACTION", 817 f"Valves {valves}, will be primed (opened and closed) {valve_acuations} times. Ok to begin?", 818 ) 819 if priming_confirmed: 820 # in case of prime, we send 0 to mean continue or start 821 # and we send a 1 at any point to abort or cancel the prime 822 start = np.int8(0).tobytes() 823 self.arduino_controller.send_command(command=start) 824 825 if isinstance(self.valve_test_button, tk.Button): 826 self.valve_test_button.configure( 827 text="STOP Priming", 828 bg="gold", 829 command=lambda: self.stop_priming(), 830 ) 831 832 def stop_priming(self) -> None: 833 """ 834 Sends the stop command to the Arduino during a priming sequence. 835 836 Sets the `prime_running` flag to False. Updates the prime button state. 837 Sends a byte representation of 1 (`np.int8(1).tobytes()`) to signal abort to the Arduino. 838 """ 839 self.prime_running = False 840 841 if isinstance(self.valve_test_button, tk.Button): 842 self.valve_test_button.configure( 843 text="Start Priming", 844 bg="coral", 845 command=lambda: self.send_schedules(), 846 ) 847 848 stop = np.int8(1).tobytes() 849 self.arduino_controller.send_command(command=stop) 850 851 def take_input( 852 self, event: tk.Event | None, pair_num_override: int | None = None 853 ) -> None: 854 """ 855 Prompts the user to enter the dispensed volume for a completed test pair. 856 857 Determines which valve(s) were just tested based on the pair number received 858 from the Arduino (via the event state or override). Uses `simpledialog.askfloat` 859 to get the dispensed volume (in mL) for each valve in the pair. Stores the 860 results in `self.ml_dispensed`. 861 862 Sends a byte back to the Arduino to instruct if further pairs will be tested or if this is the final iteration, 863 (1 if via event, 0 if via override/final pair). 864 Handles potential abort if the user cancels the dialog window. 865 866 Parameters 867 ---------- 868 - **event** (*tk.Event | None*): Uses custom thread-safe event (`<<event1>>`) generated by `run_test`, carrying the pair number in `event.state` 869 attribute. This is None if called directly by `testing_complete`, which indicated procedure is now finished. 870 - **pair_num_override** (*int | None, optional*): Allows manually specifying the pair number, used when called from `testing_complete` for the 871 final pair. Defaults to None. 872 """ 873 if event is not None: 874 pair_number = event.state 875 else: 876 pair_number = pair_num_override 877 878 # decide how many valves were tested last. if 0 we will not arrive 879 # here so we need not test for this. 880 if pair_number < len(self.side_one_tests) and pair_number < len( 881 self.side_two_tests 882 ): 883 valves_tested = [ 884 self.side_one_tests[pair_number], 885 self.side_two_tests[pair_number], 886 ] 887 elif pair_number < len(self.side_one_tests): 888 valves_tested = [ 889 self.side_one_tests[pair_number], 890 ] 891 else: 892 valves_tested = [ 893 self.side_two_tests[pair_number], 894 ] 895 896 # for each test that we ran store the amount dispensed or abort 897 for valve in valves_tested: 898 response = simpledialog.askfloat( 899 "INPUT AMOUNT DISPENSED", 900 f"Please input the amount of liquid dispensed for the test of valve {valve}", 901 ) 902 if response is None: 903 self.abort_test() 904 else: 905 self.ml_dispensed.append((valve, response)) 906 if pair_num_override is not None: 907 self.arduino_controller.send_command(np.int8(0).tobytes()) 908 else: 909 self.arduino_controller.send_command(np.int8(1).tobytes()) 910 911 def run_test(self) -> None: 912 """ 913 Listens for messages from the Arduino during an active test sequence (runs in a background thread). 914 915 Sends the initial 'start test' command byte to the Arduino. Enters a loop 916 that continues as long as `self.test_running` is True and `self.stop_event` 917 is not set. Checks for incoming data from the Arduino. Reads bytes indicating 918 if more tests remain and the number of the just-completed pair. Generates 919 custom Tkinter events (`<<event1>>` for input dispensed liquid prompt, `<<event0>>` for test completion) 920 to communicate back to the main GUI thread safely. Updates the main button state to 'ABORT TESTING'. 921 """ 922 if self.stop_event.is_set(): 923 self.stop_event.clear() 924 925 if isinstance(self.valve_test_button, tk.Button): 926 self.valve_test_button.configure(text="ABORT TESTING", bg="red") 927 928 self.test_running = True 929 ###====SENDING BEGIN TEST COMMAND====### 930 command = np.int8(1).tobytes() 931 self.arduino_controller.send_command(command) 932 933 while self.test_running: 934 # if stop event, shut down the thread 935 if self.stop_event.is_set(): 936 break 937 if self.arduino_controller.arduino.in_waiting > 0: 938 remaining_tests = self.arduino_controller.arduino.read() 939 pair_number = int.from_bytes( 940 self.arduino_controller.arduino.read(), 941 byteorder="little", 942 signed=False, 943 ) 944 945 if remaining_tests == np.int8(1).tobytes(): 946 self.event_generate("<<event1>>", when="tail", state=pair_number) 947 if remaining_tests == np.int8(0).tobytes(): 948 self.event_generate("<<event0>>", when="tail", state=pair_number) 949 950 def abort_test(self) -> None: 951 """ 952 Aborts an ongoing test sequence. 953 954 Sets the `self.stop_event` to signal the `run_test` listener thread to exit. 955 Sets `self.test_running` to False. Sends the abort *testing* command byte 956 (`np.int8(0).tobytes()`) to the Arduino. Resets the main button state to 'Start Testing'. 957 """ 958 # if test is aborted, stop the test listener thread 959 if isinstance(self.valve_test_button, tk.Button): 960 self.valve_test_button.configure(text="Start Testing", bg="green") 961 962 self.stop_event.set() 963 self.test_running = False 964 ###====SENDING ABORT TEST COMMAND====### 965 command = np.int8(0).tobytes() 966 self.arduino_controller.send_command(command) 967 968 def testing_complete(self, event: tk.Event) -> None: 969 """ 970 Handles the final steps after the Arduino signals test completion. 971 972 Calls `take_input` one last time for the final test pair (using `pair_num_override`). 973 974 Resets the main button state and `self.test_running` flag. Sets the 975 `self.stop_event` to ensure the listener thread terminates cleanly. 976 Calls `auto_update_durations` to process the collected data and suggest timing changes. 977 978 Parameters 979 ---------- 980 - **event** (*tk.Event*): The custom event (`<<event0>>`) generated by `run_test`, carrying the final pair number in `event.state`. 981 """ 982 # take input from last pair/valve 983 self.take_input(event=None, pair_num_override=event.state) 984 # reconfigure the testing button and testing state 985 if isinstance(self.valve_test_button, tk.Button): 986 self.valve_test_button.configure(text="Start Testing", bg="green") 987 988 self.test_running = False 989 # stop the arduino testing listener thread 990 self.stop_event.set() 991 992 # update valves and such 993 self.auto_update_durations() 994 995 def auto_update_durations(self): 996 """ 997 This function is similar to the `views.valve_testing.manual_time_adjustment_window.ManualTimeAdjustment.write_timing_changes` function, it 998 calculates new valve durations based on test results and desired volume. 999 1000 Loads the current 'selected' durations. Iterates through the collected 1001 `self.ml_dispensed` data. For each tested valve, it calculates the actual 1002 volume dispensed per actuation and compares it to the desired volume per actuation. 1003 It computes a new duration using a ratio (`new_duration = old_duration * (desired_vol / actual_vol)`). 1004 1005 Updates the corresponding duration in the local copies of the side_one/side_two arrays. 1006 Updates the table display for the valve using `update_table_entry_test_status`. 1007 Compiles a list of changes (`changed_durations`) and opens the `ValveChanges` 1008 confirmation window, passing the changes and the `confirm_valve_changes` callback. 1009 """ 1010 # list of tuples of tuples ==> [(valve, (old_dur, new_dur))] sent to ChangesWindow instance, so that 1011 # user can confirm duration changes 1012 changed_durations = [] 1013 1014 ## save current durations in the oldest valve duration archival location 1015 side_one, side_two, date_used = self.arduino_data.load_durations() 1016 1017 side_one_old = side_one.copy() 1018 side_two_old = side_two.copy() 1019 1020 for valve, dispensed_amt in self.ml_dispensed: 1021 logical_valve = None 1022 tested_duration = None 1023 side_durations = None 1024 1025 if valve > VALVES_PER_SIDE: 1026 logical_valve = (valve - 1) - VALVES_PER_SIDE 1027 1028 tested_duration = side_two[logical_valve] 1029 side_durations = side_two 1030 else: 1031 logical_valve = valve - 1 1032 1033 tested_duration = side_one[logical_valve] 1034 side_durations = side_one 1035 1036 # divide by 1000 to get to mL from ul 1037 desired_per_open_vol = self.desired_volume.get() / 1000 1038 1039 # divide by 1000 to get amount per opening, NOT changing units 1040 actual_per_open_vol = dispensed_amt / self.actuations.get() 1041 1042 self.update_table_entry_test_status(valve, actual_per_open_vol) 1043 1044 new_duration = round( 1045 tested_duration * (desired_per_open_vol / actual_per_open_vol) 1046 ) 1047 1048 side_durations[logical_valve] = new_duration 1049 1050 changed_durations.append((valve, (tested_duration, new_duration))) 1051 1052 ValveChanges( 1053 changed_durations, 1054 lambda: self.confirm_valve_changes( 1055 side_one, side_two, side_one_old, side_two_old 1056 ), 1057 ) 1058 1059 def confirm_valve_changes( 1060 self, 1061 side_one: npt.NDArray[np.int32], 1062 side_two: npt.NDArray[np.int32], 1063 side_one_old: npt.NDArray[np.int32], 1064 side_two_old: npt.NDArray[np.int32], 1065 ): 1066 """ 1067 Callback function executed when changes are confirmed in the ValveChanges window. 1068 1069 Instructs `self.arduino_data` to first save the `side_one_old` and `side_two_old` 1070 arrays to an archive slot, and then save the newly calculated `side_one` and `side_two` 1071 arrays as the 'selected' durations. 1072 1073 Parameters 1074 ---------- 1075 - **side_one** (*npt.NDArray[np.int32]*): The numpy array containing the newly calculated durations for side one valves. 1076 - **side_two** (*npt.NDArray[np.int32]*): The numpy array containing the newly calculated durations for side two valves. 1077 - **side_one_old** (*npt.NDArray[np.int32]*): The numpy array containing the durations for side one valves *before* the test. 1078 - **side_two_old** (*npt.NDArray[np.int32]*): The numpy array containing the durations for side two valves *before* the test. 1079 """ 1080 self.arduino_data.save_durations(side_one_old, side_two_old, "archive") 1081 self.arduino_data.save_durations(side_one, side_two, "selected")
Get the logger in use for the app.
60class WindowMode(Enum): 61 """ 62 Represents the two primary operational modes of the ValveTestWindow: 63 Testing mode for calibrating valve durations, and Priming mode for 64 actuating valves repeatedly without calibration. 65 """ 66 67 TESTING = "Testing" 68 PRIMING = "Priming"
Represents the two primary operational modes of the ValveTestWindow: Testing mode for calibrating valve durations, and Priming mode for actuating valves repeatedly without calibration.
71class ValveTestWindow(tk.Toplevel): 72 """ 73 *This is a bit of a god class and should likely be refactored... but for now... it works!* 74 75 Implements the main Tkinter Toplevel window for valve testing and priming operations. 76 77 This window provides an interface to interact with the Arduino 78 controller for calibrating valve open times based on dispensed volume or 79 for running priming sequences. It features modes for testing and 80 priming, manages valve selections, displays test progress and results, 81 and handles communication with the Arduino. 82 83 Attributes 84 ---------- 85 - **`arduino_controller`** (*ArduinoManager*): Reference to the main Arduino controller instance, used for sending commands and data. 86 - **`arduino_data`** (*ArduinoData*): Reference to the data model holding valve duration configurations. 87 - **`window_mode`** (*WindowMode*): Enum indicating the current operational mode (Testing or Priming). 88 - **`test_running`** (*bool*): Flag indicating if an automated test sequence is currently active. 89 - **`prime_running`** (*bool*): Flag indicating if a priming sequence is currently active. 90 - **`desired_volume`** (*tk.DoubleVar*): Tkinter variable storing the target volume (in microliters) per actuation for automatic duration calculation. 91 - **`actuations`** (*tk.IntVar*): Tkinter variable storing the number of times each valve should be actuated during a test or prime sequence. 92 - **`ml_dispensed`** (*list[tuple[int, float]]*): Stores tuples of (valve_number, dispensed_volume_ml) entered by the user during a test. 93 - **`valve_buttons`** (*list[tk.Button | None]*): List holding the Tkinter Button widgets for each valve selection. 94 - **`valve_test_button`** (*tk.Button | None*): The main button used to start/abort tests or start/stop priming. 95 - **`table_entries`** (*list[str | None]*): List holding the item IDs for each row in the ttk.Treeview table, used for modifying/hiding/showing specific rows. 96 - **`valve_selections`** (*npt.NDArray[np.int8]*): Numpy array representing the selection state of each valve (0 if not selected, valve_number if selected). 97 - **`side_one_tests`** (*npt.NDArray[np.int8] | None*): Numpy array holding the valve numbers (0-indexed) selected for testing/priming on side one. 98 - **`side_two_tests`** (*npt.NDArray[np.int8] | None*): Numpy array holding the valve numbers (0-indexed) selected for testing/priming on side two. 99 - **`stop_event`** (*threading.Event*): Event flag used to signal the Arduino listener thread (`run_test`) to stop. 100 - **`mode_button`** (*tk.Button*): Button to switch between Testing and Priming modes. 101 - **`dispensed_vol_frame`** (*tk.Frame*): Container frame for the desired volume input elements (visible in Testing mode). 102 - **`valve_table_frame`** (*tk.Frame*): Container frame for the valve test results table (visible in Testing mode). 103 - **`valve_table`** (*ttk.Treeview*): The table widget displaying valve numbers, test status, and timings. 104 - **`valve_buttons_frame`** (*tk.Frame*): Main container frame for the side-by-side valve selection buttons. 105 - **`side_one_valves_frame`** (*tk.Frame*): Frame holding valve selection buttons for the first side. 106 - **`side_two_valves_frame`** (*tk.Frame*): Frame holding valve selection buttons for the second side. 107 - **`manual_adjust_window`** (*ManualTimeAdjustment*): Instance of the separate window for manual timing adjustments. 108 109 Methods 110 ------- 111 - `setup_basic_window_attr`() 112 Configures basic window properties like title, key bindings, and icon. 113 - `show`() 114 Makes the window visible (deiconifies). 115 - `switch_window_mode`() 116 Toggles the window between Testing and Priming modes, adjusting UI elements accordingly. 117 - `create_interface`() 118 Builds the entire GUI layout, placing widgets and frames. 119 - `start_testing_toggle`() 120 Handles the press of the main test/prime button in Testing mode (starts or aborts test). 121 - `create_buttons`() 122 Creates and places the valve selection buttons and the main start/abort button. 123 - `create_valve_test_table`() 124 Creates the ttk.Treeview table for displaying valve test results and timings. 125 - `update_table_entry_test_status`(...) 126 Updates the 'Amount Dispensed' column for a specific valve in the table. 127 - `update_table_entries`(...) 128 Updates the entire valve table, showing/hiding rows based on selections and refreshing currently loaded durations. 129 - `toggle_valve_button`(...) 130 Handles clicks on valve selection buttons, updating their appearance and the `valve_selections` array. 131 - `verify_variables`(...) 132 Reads verification data back from Arduino to confirm test parameters were received correctly. 133 - `verify_schedule`(...) 134 Reads verification data back from Arduino to confirm the valve test/prime schedule was received correctly. 135 - `send_schedules`() 136 Sends the command, parameters (schedule lengths, actuations), and valve schedule to the Arduino. Handles mode-specific commands and confirmations. 137 - `stop_priming`() 138 Sends a command to the Arduino to stop an ongoing priming sequence. 139 - `take_input`(...) 140 Prompts the user (via simpledialog) to enter the dispensed volume for a completed valve test pair. Sends confirmation back to Arduino. 141 - `run_test`() 142 Runs in a separate thread to listen for messages from the Arduino during a test, triggering events for input prompts or completion. 143 - `abort_test`() 144 Sends a command to the Arduino to abort the current test and stops the listener thread. 145 - `testing_complete`(...) 146 Handles the event triggered when the Arduino signals the end of the entire test sequence. Initiates duration updates. 147 - `auto_update_durations`() 148 Calculates new valve durations based on test results (`ml_dispensed`) and opens the `ValveChanges` confirmation window. 149 - `confirm_valve_changes`(...) 150 Callback function executed if the user confirms changes in the `ValveChanges` window. Saves new durations and archives old ones via `ArduinoData`. 151 """ 152 153 def __init__(self, arduino_controller: ArduinoManager) -> None: 154 """ 155 Initializes the ValveTestWindow. 156 157 Parameters 158 ---------- 159 - **arduino_controller** (*ArduinoManager*): The application's instance of the Arduino controller. 160 161 Raises 162 ------ 163 - Propagates exceptions from `GUIUtils` methods during icon setting. 164 - Propagates exceptions from `arduino_data.load_durations()`. 165 """ 166 super().__init__() 167 self.arduino_controller: ArduinoManager = arduino_controller 168 self.arduino_data: ArduinoData = arduino_controller.arduino_data 169 170 self.setup_basic_window_attr() 171 172 self.window_mode: WindowMode = WindowMode.TESTING 173 174 self.test_running: bool = False 175 self.prime_running: bool = False 176 177 self.desired_volume: tk.DoubleVar = tk.DoubleVar(value=5) 178 179 self.actuations: tk.IntVar = tk.IntVar(value=1000) 180 181 self.ml_dispensed: list[tuple[int, float]] = [] 182 183 self.valve_buttons: list[tk.Button | None] = [None] * TOTAL_VALVES 184 185 self.valve_test_button: tk.Button | None = None 186 187 self.table_entries: list[str | None] = [None] * TOTAL_VALVES 188 189 self.valve_selections: npt.NDArray[np.int8] = np.zeros( 190 (TOTAL_VALVES,), dtype=np.int8 191 ) 192 193 self.side_one_tests: None | npt.NDArray[np.int8] = None 194 self.side_two_tests: None | npt.NDArray[np.int8] = None 195 196 self.stop_event: threading.Event = threading.Event() 197 198 self.create_interface() 199 200 # hide main window for now until we deliberately enter this window. 201 self.withdraw() 202 203 self.manual_adjust_window = ManualTimeAdjustment( 204 self.arduino_controller.arduino_data 205 ) 206 # hide the adjustment window until we need it. 207 self.manual_adjust_window.withdraw() 208 209 logging.info("Valve Test Window initialized.") 210 211 def setup_basic_window_attr(self) -> None: 212 """ 213 Sets up fundamental window properties. 214 215 Configures the window title, binds Ctrl+W and the close button ('X') 216 to hide the window, sets grid column/row weights for resizing behavior, 217 and applies the application icon using `GUIUtils`. 218 """ 219 220 self.title("Valve Testing") 221 self.bind("<Control-w>", lambda event: self.withdraw()) 222 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 223 224 self.grid_columnconfigure(0, weight=1) 225 226 for i in range(5): 227 self.grid_rowconfigure(i, weight=1) 228 self.grid_rowconfigure(0, weight=0) 229 230 window_icon_path = GUIUtils.get_window_icon_path() 231 GUIUtils.set_program_icon(self, icon_path=window_icon_path) 232 233 def show(self) -> None: 234 """ 235 Makes the ValveTestWindow visible. 236 237 Calls `deiconify()` to show the window if it was previously hidden (withdrawn). 238 """ 239 self.deiconify() 240 241 def switch_window_mode(self) -> None: 242 """ 243 Toggles the window's operational mode between Testing and Priming. 244 245 Updates the window title and mode button text/color. Hides or shows 246 mode-specific UI elements (desired volume input, test results table). 247 Changes the command associated with the main start/stop button. 248 Prevents mode switching if a test or prime sequence is currently running to avoid Arduino 249 Communications errors. Resizes and centers the window after GUI changes. 250 """ 251 252 if self.window_mode == WindowMode.TESTING: 253 if self.test_running: 254 # if the test is currently running we do not want to switch modes. Stop testing to switch modes 255 GUIUtils.display_error( 256 "ACTION PROHIBITED", 257 "You cannot switch modes while a test is running. If you'd like to switch modes, please abort the test and try again.", 258 ) 259 return 260 self.title("Valve Priming") 261 262 self.window_mode = WindowMode.PRIMING 263 264 self.mode_button.configure(text="Switch to Testing Mode", bg="DeepSkyBlue3") 265 266 # orig. row 1 267 self.dispensed_vol_frame.grid_forget() 268 # orig. row 3 269 self.valve_table_frame.grid_forget() 270 271 # start testing button -> start priming button / command 272 273 if isinstance(self.valve_test_button, tk.Button): 274 self.valve_test_button.configure( 275 text="Start Priming", 276 bg="coral", 277 command=lambda: self.send_schedules(), 278 ) 279 280 else: 281 if self.prime_running: 282 # if the test is currently running we do not want to switch modes. Stop testing to switch modes 283 GUIUtils.display_error( 284 "ACTION PROHIBITED", 285 "You cannot switch modes while a valve prime operation is running. If you'd like to switch modes, please abort the prime and try again.", 286 ) 287 return 288 289 self.title("Valve Testing") 290 291 self.window_mode = WindowMode.TESTING 292 293 self.mode_button.configure(text="Switch to Priming Mode", bg="coral") 294 295 self.valve_table_frame.grid( 296 row=3, column=0, sticky="nsew", pady=10, padx=10 297 ) 298 self.dispensed_vol_frame.grid(row=1, column=0, padx=5, sticky="nsew") 299 300 if isinstance(self.valve_test_button, tk.Button): 301 self.valve_test_button.configure( 302 text="Start Testing", 303 bg="green", 304 command=lambda: self.start_testing_toggle(), 305 ) 306 307 # resize the window to fit current content 308 self.update_idletasks() 309 GUIUtils.center_window(self) 310 311 def create_interface(self) -> None: 312 """ 313 Constructs and arranges all GUI elements within the window. 314 315 Creates frames for structure, input fields (desired volume, actuations), 316 the mode switch button, the valve selection buttons, the test results table, 317 and the main start/abort/prime button. Uses `GUIUtils` for 318 widget creation where possible. Calls helper methods `create_valve_test_table` 319 and `create_buttons`. Centers the window after creation. 320 """ 321 322 self.mode_button = GUIUtils.create_button( 323 self, 324 "Switch to Priming Mode", 325 command=lambda: self.switch_window_mode(), 326 bg="coral", 327 row=0, 328 column=0, 329 )[1] 330 331 # create a new frame to contain the desired dispensed volume label and frame 332 self.dispensed_vol_frame = tk.Frame(self) 333 self.dispensed_vol_frame.grid(row=1, column=0, padx=5, sticky="nsew") 334 335 self.dispensed_vol_frame.grid_columnconfigure(0, weight=1) 336 self.dispensed_vol_frame.grid_rowconfigure(0, weight=1) 337 self.dispensed_vol_frame.grid_rowconfigure(1, weight=1) 338 339 label = tk.Label( 340 self.dispensed_vol_frame, 341 text="Desired Dispensed Volume in Microliters (ul)", 342 bg="light blue", 343 font=("Helvetica", 20), 344 highlightthickness=1, 345 highlightbackground="dark blue", 346 height=2, 347 width=50, 348 ) 349 label.grid(row=0, pady=5) 350 351 entry = tk.Entry( 352 self.dispensed_vol_frame, 353 textvariable=self.desired_volume, 354 font=("Helvetica", 24), 355 highlightthickness=1, 356 highlightbackground="black", 357 ) 358 entry.grid(row=1, sticky="nsew", pady=5, ipady=14) 359 360 # create another new frame to contain the amount of times each 361 # valve should actuate label and frame 362 frame = tk.Frame(self) 363 frame.grid(row=2, column=0, padx=5, sticky="nsew") 364 365 frame.grid_columnconfigure(0, weight=1) 366 frame.grid_rowconfigure(0, weight=1) 367 frame.grid_rowconfigure(1, weight=1) 368 369 label = tk.Label( 370 frame, 371 text="How Many Times Should the Valve Actuate?", 372 bg="light blue", 373 font=("Helvetica", 20), 374 highlightthickness=1, 375 highlightbackground="dark blue", 376 height=2, 377 width=50, 378 ) 379 label.grid(row=0, pady=5) 380 381 entry = tk.Entry( 382 frame, 383 textvariable=self.actuations, 384 font=("Helvetica", 24), 385 highlightthickness=1, 386 highlightbackground="black", 387 ) 388 entry.grid(row=1, sticky="nsew", pady=5, ipady=12) 389 390 ### ROW 3 ### 391 self.create_valve_test_table() 392 393 # setup frames for valve buttons 394 self.valve_buttons_frame = tk.Frame( 395 self, highlightbackground="black", highlightthickness=1 396 ) 397 ### ROW 4 ### 398 self.valve_buttons_frame.grid(row=4, column=0, pady=10, padx=10, sticky="nsew") 399 self.valve_buttons_frame.grid_columnconfigure(0, weight=1) 400 self.valve_buttons_frame.grid_rowconfigure(0, weight=1) 401 402 self.side_one_valves_frame = tk.Frame( 403 self.valve_buttons_frame, highlightbackground="black", highlightthickness=1 404 ) 405 self.side_one_valves_frame.grid(row=0, column=0, pady=10, padx=10, sticky="w") 406 self.side_one_valves_frame.grid_columnconfigure(0, weight=1) 407 for i in range(4): 408 self.side_one_valves_frame.grid_rowconfigure(i, weight=1) 409 410 self.side_two_valves_frame = tk.Frame( 411 self.valve_buttons_frame, highlightbackground="black", highlightthickness=1 412 ) 413 self.side_two_valves_frame.grid(row=0, column=1, pady=10, padx=10, sticky="e") 414 self.side_two_valves_frame.grid_columnconfigure(0, weight=1) 415 for i in range(4): 416 self.side_two_valves_frame.grid_rowconfigure(i, weight=1) 417 418 ### ROW 5 ### 419 self.create_buttons() 420 421 self.update_idletasks() 422 GUIUtils.center_window(self) 423 424 def start_testing_toggle(self) -> None: 425 """ 426 Handles the action of the main button when in Testing mode. 427 428 If a test is running, it calls `abort_test()` and updates the button appearance. 429 If no test is running, it calls `send_schedules()` to initiate a new test. 430 """ 431 432 if self.test_running: 433 if isinstance(self.valve_test_button, tk.Button): 434 self.valve_test_button.configure(text="Start Testing", bg="green") 435 self.abort_test() 436 else: 437 self.send_schedules() 438 439 def create_buttons(self) -> None: 440 """ 441 Creates and places the individual valve selection buttons and the main action button. 442 443 Generates buttons for each valve (1 to TOTAL_VALVES), arranging them 444 into side-one and side-two frames using `GUIUtils.create_button`. 445 Assigns the `toggle_valve_button` command to each valve button. 446 447 Creates the main 'Start Testing' / 'Start Priming' / 'Abort' button. 448 """ 449 for i in range(VALVES_PER_SIDE): 450 # GUIUtils.create_button returns both the buttons frame and button object in a tuple. using [1] on the 451 # return item yields the button object. 452 453 # return the side one button and put it on the side one 'side' 454 # of the list (0-4) 455 self.valve_buttons[i] = GUIUtils.create_button( 456 self.side_one_valves_frame, 457 f"Valve {i + 1}", 458 lambda i=i: self.toggle_valve_button(self.valve_buttons[i], i), 459 "light blue", 460 i, 461 0, 462 )[1] 463 464 # return the side two button and put it on the side two 'side' 465 # of the list (5-8) 466 self.valve_buttons[i + (VALVES_PER_SIDE)] = GUIUtils.create_button( 467 self.side_two_valves_frame, 468 f"Valve {i + (VALVES_PER_SIDE) + 1}", 469 lambda i=i: self.toggle_valve_button( 470 self.valve_buttons[i + (VALVES_PER_SIDE)], i + (VALVES_PER_SIDE) 471 ), 472 "light blue", 473 i, 474 0, 475 )[1] 476 477 self.valve_test_button = GUIUtils.create_button( 478 self, "Start Testing", lambda: self.start_testing_toggle(), "green", 5, 0 479 )[1] 480 481 def create_valve_test_table(self) -> None: 482 """ 483 Creates, configures, and populates the initial state of the ttk.Treeview table. 484 485 Sets up the table columns ("Valve", "Amount Dispensed...", "Testing Opening Time..."). 486 Adds a button above the table to open the `ManualTimeAdjustment` window. 487 488 Loads the currently selected valve durations from `models.arduino_data.ArduinoData`. 489 Inserts initial rows for all possible valves, marking them as "Test Not Complete" 490 and displaying their current duration. Stores row IDs in `self.table_entries` for later access. 491 """ 492 493 # this function creates and labels the testing window table that displays the information recieved from the arduino reguarding the results of the tes 494 self.valve_table_frame = tk.Frame( 495 self, highlightbackground="black", highlightthickness=1 496 ) 497 self.valve_table_frame.grid(row=3, column=0, sticky="nsew", pady=10, padx=10) 498 # tell the frame to fill the available width 499 self.valve_table_frame.grid_columnconfigure(0, weight=1) 500 501 self.valve_table = ttk.Treeview( 502 self.valve_table_frame, show="headings", height=12 503 ) 504 505 headings = [ 506 "Valve", 507 "Amount Dispensed Per Lick (ul)", 508 "Testing Opening Time of (ms)", 509 ] 510 511 self.valve_table["columns"] = headings 512 513 self.valve_table.grid(row=1, sticky="ew") 514 515 button = tk.Button( 516 self.valve_table_frame, 517 text="Manual Valve Duration Override", 518 command=lambda: self.manual_adjust_window.show(), 519 bg="light blue", 520 highlightbackground="black", 521 highlightthickness=1, 522 ) 523 524 button.grid(row=0, sticky="e") 525 526 # Configure the columns 527 for col in self.valve_table["columns"]: 528 self.valve_table.heading(col, text=col) 529 530 arduino_data = self.arduino_controller.arduino_data 531 532 side_one_durations, side_two_durations, date_used = ( 533 arduino_data.load_durations() 534 ) 535 536 # insert default entries for each total valve until user selects how many 537 # they want to test 538 for i in range(VALVES_PER_SIDE): 539 self.table_entries[i] = self.valve_table.insert( 540 "", 541 i, 542 values=( 543 f"{i + 1}", 544 "Test Not Complete", 545 f"{side_one_durations[i]} ms", 546 ), 547 ) 548 self.table_entries[i + 4] = self.valve_table.insert( 549 "", 550 i + VALVES_PER_SIDE, 551 values=( 552 f"{i + VALVES_PER_SIDE + 1}", 553 "Test Not Complete", 554 f"{side_two_durations[i]} ms", 555 ), 556 ) 557 558 def update_table_entry_test_status(self, valve: int, l_per_lick: float) -> None: 559 """ 560 Updates the 'Amount Dispensed Per Lick (ul)' column for a specific valve row. 561 562 Parameters 563 ---------- 564 - **valve** (*int*): The 1-indexed valve number whose row needs updating. 565 - **l_per_lick** (*float*): The calculated volume dispensed per lick in milliliters (mL). 566 """ 567 568 self.valve_table.set( 569 self.table_entries[valve - 1], column=1, value=f"{l_per_lick * 1000} uL" 570 ) 571 572 def update_table_entries(self) -> None: 573 """ 574 Refreshes the entire valve test table based on current selections and durations. 575 576 Hides rows for unselected valves and shows rows for selected valves. 577 Reloads the latest durations from `ArduinoData` and updates the 578 'Testing Opening Time' column for all visible rows. Resets the 579 'Amount Dispensed' column to "Test Not Complete" for visible rows. 580 """ 581 side_one, side_two, _ = self.arduino_data.load_durations() 582 # np array where the entry is not zero but the valve number itself 583 selected_valves = self.valve_selections[self.valve_selections != 0] 584 585 # for all valve numbers, if that number is in selected_valves -1 (the entire array 586 # decremented by one) place it in the table and ensure its duration is up-to-date 587 for i in range(TOTAL_VALVES): 588 # if the iter number is also contained in the selected valves arr 589 if i in (selected_valves - 1): 590 # check if it's side two or side one 591 if i >= VALVES_PER_SIDE: 592 duration = side_two[i - VALVES_PER_SIDE] 593 else: 594 duration = side_one[i] 595 self.valve_table.item( 596 self.table_entries[i], 597 values=( 598 f"{i + 1}", 599 "Test Not Complete", 600 f"{duration} ms", 601 ), 602 ) 603 self.valve_table.reattach(self.table_entries[i], "", i) 604 else: 605 self.valve_table.detach(self.table_entries[i]) 606 607 logging.info("Valve test table updated.") 608 609 def toggle_valve_button(self, button: tk.Button, valve_num: int) -> None: 610 """ 611 Handles the click event for a valve selection button. 612 613 Toggles the button's visual state (raised/sunken). Updates the 614 corresponding entry in the `self.valve_selections` numpy array (0 for 615 deselected, valve_number + 1 for selected). Calls `update_table_entries` 616 to refresh the table display. Filling arrays this way allows us to tell 617 the arduino exactly which valves should be toggled (1, 4, 7) as long as 618 their value in the array is not 0. 619 620 Parameters 621 ---------- 622 - **button** (*tk.Button*): The specific valve button widget that was clicked. 623 - **valve_num** (*int*): The 0-indexed number of the valve corresponding to the button. 624 """ 625 # Attempt to get the value to ensure it's a valid float as expected 626 # If successful, call the function to update/create the valve test table 627 if self.valve_selections[valve_num] == 0: 628 button.configure(relief="sunken") 629 self.valve_selections[valve_num] = valve_num + 1 630 631 else: 632 button.configure(relief="raised") 633 self.valve_selections[valve_num] = 0 634 self.update_table_entries() 635 636 def verify_variables(self, original_data: bytes) -> bool: 637 """ 638 Verifies that the Arduino correctly received the test/prime parameters. 639 640 Waits for and reads back the schedule lengths and actuation count echoed 641 by the Arduino. Compares the received bytes with the originally sent data. 642 643 Parameters 644 ---------- 645 - **original_data** (*bytes*): The byte string containing schedule lengths and actuation count originally sent to the Arduino. 646 647 Returns 648 ------- 649 - *bool*: `True` if the received data matches the original data, `False` otherwise (logs error and shows message box on mismatch). 650 """ 651 # wait until all data is on the wire ready to be read (4 bytes), 652 # then read it all in quick succession 653 if self.arduino_controller.arduino is None: 654 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 655 GUIUtils.display_error("TRANSMISSION ERROR", msg) 656 logger.error(msg) 657 return False 658 659 while self.arduino_controller.arduino.in_waiting < 4: 660 pass 661 662 ver_len_sched_sd_one = self.arduino_controller.arduino.read() 663 ver_len_sched_sd_two = self.arduino_controller.arduino.read() 664 665 ver_valve_act_time = self.arduino_controller.arduino.read(2) 666 667 verification_data = ( 668 ver_len_sched_sd_one + ver_len_sched_sd_two + ver_valve_act_time 669 ) 670 if verification_data == original_data: 671 logger.info("====VERIFIED TEST VARIABLES====") 672 return True 673 else: 674 msg = "====INCORRECT VARIABLES DETECTED, TRY AGAIN====" 675 GUIUtils.display_error("TRANSMISSION ERROR", msg) 676 logger.error(msg) 677 return False 678 679 def verify_schedule( 680 self, len_sched: int, sent_schedule: npt.NDArray[np.int8] 681 ) -> bool: 682 """ 683 Verifies that the Arduino correctly received the valve schedule. 684 685 Waits for and reads back the valve numbers (0-indexed) echoed byte-by-byte 686 by the Arduino. Compares the reconstructed numpy array with the schedule 687 that was originally sent. 688 689 Parameters 690 ---------- 691 - **len_sched** (*int*): The total number of valves in the schedule being sent. 692 - **sent_schedule** (*npt.NDArray[np.int8]*): The numpy array (0-indexed valve numbers) representing the schedule originally sent. 693 694 Returns 695 ------- 696 - *bool*: `True` if the received schedule matches the sent schedule, `False` otherwise (logs error and shows message box on mismatch). 697 """ 698 # np array used for verification later 699 received_sched = np.zeros((len_sched,), dtype=np.int8) 700 701 if self.arduino_controller.arduino is None: 702 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 703 GUIUtils.display_error("TRANSMISSION ERROR", msg) 704 logger.error(msg) 705 return False 706 707 while self.arduino_controller.arduino.in_waiting < len_sched: 708 pass 709 710 for i in range(len_sched): 711 received_sched[i] = int.from_bytes( 712 self.arduino_controller.arduino.read(), byteorder="little", signed=False 713 ) 714 715 if np.array_equal(received_sched, sent_schedule): 716 logger.info(f"===TESTING SCHEDULE VERIFIED as ==> {received_sched}===") 717 logger.info("====================BEGIN TESTING NOW====================") 718 return True 719 else: 720 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 721 GUIUtils.display_error("TRANSMISSION ERROR", msg) 722 logger.error(msg) 723 return False 724 725 def send_schedules(self) -> None: 726 """ 727 Performs the process of sending test or prime configuration to the Arduino. 728 729 Determines selected valves and schedule lengths for each side. Sends current 730 valve durations to Arduino. Then sends the appropriate command ('TEST VOL' or 'PRIME VALVES'). 731 732 Sends test parameters (schedule lengths, actuations) and verifies them using `verify_variables`. 733 Sends the actual valve schedule (0-indexed) and verifies it using `verify_schedule`. 734 735 User is then prompted to confirm before proceeding. 736 If confirmed, initiates the test/prime sequence on the Arduino 737 and starts the listener thread (`run_test`) if Testing, or sends start command if Priming. 738 Updates button states accordingly. 739 """ 740 len_sched_one = np.count_nonzero(self.valve_selections[:VALVES_PER_SIDE]) 741 len_sched_two = np.count_nonzero(self.valve_selections[VALVES_PER_SIDE:]) 742 743 len_sched_one = np.int8(len_sched_one) 744 len_sched_two = np.int8(len_sched_two) 745 len_sched = len_sched_one + len_sched_two 746 747 # because self.actuations is a IntVar, we use .get() method to get its value 748 valve_acuations = np.int16(self.actuations.get()) 749 750 # schedule is TOTAL_VALVES long to accomodate testing of up to all valves at one time. 751 # if we are not testing them all, filter out the zero entries (valves we are not testing) 752 schedule = self.valve_selections[self.valve_selections != 0] 753 754 # decrement each element in the array by one to move to zero-indexing. We moved to 1 755 # indexing becuase using zero indexing does not work with the method of toggling valves 756 # defined above 757 schedule -= 1 758 759 self.side_one_tests = schedule[:len_sched_one] 760 self.side_two_tests = schedule[len_sched_one:] 761 762 len_sched_one = len_sched_one 763 len_sched_two = len_sched_two 764 valve_acuations = valve_acuations 765 766 self.arduino_controller.send_valve_durations() 767 if self.window_mode == WindowMode.TESTING: 768 ###====SENDING TEST COMMAND====### 769 command = "TEST VOL\n".encode("utf-8") 770 self.arduino_controller.send_command(command) 771 772 elif self.window_mode == WindowMode.PRIMING: 773 self.prime_running = True 774 775 command = "PRIME VALVES\n".encode("utf-8") 776 self.arduino_controller.send_command(command) 777 778 ###====SENDING TEST VARIABLES (len side one, two, num_actuations per test)====### 779 data_bytes = ( 780 len_sched_one.tobytes() 781 + len_sched_two.tobytes() 782 + valve_acuations.tobytes() 783 ) 784 self.arduino_controller.send_command(data_bytes) 785 786 # verify the data 787 if not self.verify_variables(data_bytes): 788 return 789 790 ###====SENDING SCHEDULE====### 791 sched_data_bytes = schedule.tobytes() 792 self.arduino_controller.send_command(sched_data_bytes) 793 794 if not self.verify_schedule(len_sched, schedule): 795 return 796 797 schedule += 1 798 799 valves = ",".join(str(valve) for valve in schedule) 800 if self.window_mode == WindowMode.TESTING: 801 ####### ARDUINO HALTS HERE UNTIL THE GO-AHEAD IS GIVEN ####### 802 # inc each sched element by one so that it is 1-indexes and matches the valve labels 803 test_confirmed = GUIUtils.askyesno( 804 "CONFIRM THE TEST SCHEDULE", 805 f"Valves {valves}, will be tested. Review the test table to confirm schedule and timings. Each valve will be actuated {valve_acuations} times. Ok to begin?", 806 ) 807 808 ### include section to manually modify timings 809 if test_confirmed: 810 self.bind("<<event0>>", self.testing_complete) 811 self.bind("<<event1>>", self.take_input) 812 threading.Thread(target=self.run_test).start() 813 else: 814 self.abort_test() 815 elif self.window_mode == WindowMode.PRIMING: 816 priming_confirmed = GUIUtils.askyesno( 817 "CONFIRM THE ACTION", 818 f"Valves {valves}, will be primed (opened and closed) {valve_acuations} times. Ok to begin?", 819 ) 820 if priming_confirmed: 821 # in case of prime, we send 0 to mean continue or start 822 # and we send a 1 at any point to abort or cancel the prime 823 start = np.int8(0).tobytes() 824 self.arduino_controller.send_command(command=start) 825 826 if isinstance(self.valve_test_button, tk.Button): 827 self.valve_test_button.configure( 828 text="STOP Priming", 829 bg="gold", 830 command=lambda: self.stop_priming(), 831 ) 832 833 def stop_priming(self) -> None: 834 """ 835 Sends the stop command to the Arduino during a priming sequence. 836 837 Sets the `prime_running` flag to False. Updates the prime button state. 838 Sends a byte representation of 1 (`np.int8(1).tobytes()`) to signal abort to the Arduino. 839 """ 840 self.prime_running = False 841 842 if isinstance(self.valve_test_button, tk.Button): 843 self.valve_test_button.configure( 844 text="Start Priming", 845 bg="coral", 846 command=lambda: self.send_schedules(), 847 ) 848 849 stop = np.int8(1).tobytes() 850 self.arduino_controller.send_command(command=stop) 851 852 def take_input( 853 self, event: tk.Event | None, pair_num_override: int | None = None 854 ) -> None: 855 """ 856 Prompts the user to enter the dispensed volume for a completed test pair. 857 858 Determines which valve(s) were just tested based on the pair number received 859 from the Arduino (via the event state or override). Uses `simpledialog.askfloat` 860 to get the dispensed volume (in mL) for each valve in the pair. Stores the 861 results in `self.ml_dispensed`. 862 863 Sends a byte back to the Arduino to instruct if further pairs will be tested or if this is the final iteration, 864 (1 if via event, 0 if via override/final pair). 865 Handles potential abort if the user cancels the dialog window. 866 867 Parameters 868 ---------- 869 - **event** (*tk.Event | None*): Uses custom thread-safe event (`<<event1>>`) generated by `run_test`, carrying the pair number in `event.state` 870 attribute. This is None if called directly by `testing_complete`, which indicated procedure is now finished. 871 - **pair_num_override** (*int | None, optional*): Allows manually specifying the pair number, used when called from `testing_complete` for the 872 final pair. Defaults to None. 873 """ 874 if event is not None: 875 pair_number = event.state 876 else: 877 pair_number = pair_num_override 878 879 # decide how many valves were tested last. if 0 we will not arrive 880 # here so we need not test for this. 881 if pair_number < len(self.side_one_tests) and pair_number < len( 882 self.side_two_tests 883 ): 884 valves_tested = [ 885 self.side_one_tests[pair_number], 886 self.side_two_tests[pair_number], 887 ] 888 elif pair_number < len(self.side_one_tests): 889 valves_tested = [ 890 self.side_one_tests[pair_number], 891 ] 892 else: 893 valves_tested = [ 894 self.side_two_tests[pair_number], 895 ] 896 897 # for each test that we ran store the amount dispensed or abort 898 for valve in valves_tested: 899 response = simpledialog.askfloat( 900 "INPUT AMOUNT DISPENSED", 901 f"Please input the amount of liquid dispensed for the test of valve {valve}", 902 ) 903 if response is None: 904 self.abort_test() 905 else: 906 self.ml_dispensed.append((valve, response)) 907 if pair_num_override is not None: 908 self.arduino_controller.send_command(np.int8(0).tobytes()) 909 else: 910 self.arduino_controller.send_command(np.int8(1).tobytes()) 911 912 def run_test(self) -> None: 913 """ 914 Listens for messages from the Arduino during an active test sequence (runs in a background thread). 915 916 Sends the initial 'start test' command byte to the Arduino. Enters a loop 917 that continues as long as `self.test_running` is True and `self.stop_event` 918 is not set. Checks for incoming data from the Arduino. Reads bytes indicating 919 if more tests remain and the number of the just-completed pair. Generates 920 custom Tkinter events (`<<event1>>` for input dispensed liquid prompt, `<<event0>>` for test completion) 921 to communicate back to the main GUI thread safely. Updates the main button state to 'ABORT TESTING'. 922 """ 923 if self.stop_event.is_set(): 924 self.stop_event.clear() 925 926 if isinstance(self.valve_test_button, tk.Button): 927 self.valve_test_button.configure(text="ABORT TESTING", bg="red") 928 929 self.test_running = True 930 ###====SENDING BEGIN TEST COMMAND====### 931 command = np.int8(1).tobytes() 932 self.arduino_controller.send_command(command) 933 934 while self.test_running: 935 # if stop event, shut down the thread 936 if self.stop_event.is_set(): 937 break 938 if self.arduino_controller.arduino.in_waiting > 0: 939 remaining_tests = self.arduino_controller.arduino.read() 940 pair_number = int.from_bytes( 941 self.arduino_controller.arduino.read(), 942 byteorder="little", 943 signed=False, 944 ) 945 946 if remaining_tests == np.int8(1).tobytes(): 947 self.event_generate("<<event1>>", when="tail", state=pair_number) 948 if remaining_tests == np.int8(0).tobytes(): 949 self.event_generate("<<event0>>", when="tail", state=pair_number) 950 951 def abort_test(self) -> None: 952 """ 953 Aborts an ongoing test sequence. 954 955 Sets the `self.stop_event` to signal the `run_test` listener thread to exit. 956 Sets `self.test_running` to False. Sends the abort *testing* command byte 957 (`np.int8(0).tobytes()`) to the Arduino. Resets the main button state to 'Start Testing'. 958 """ 959 # if test is aborted, stop the test listener thread 960 if isinstance(self.valve_test_button, tk.Button): 961 self.valve_test_button.configure(text="Start Testing", bg="green") 962 963 self.stop_event.set() 964 self.test_running = False 965 ###====SENDING ABORT TEST COMMAND====### 966 command = np.int8(0).tobytes() 967 self.arduino_controller.send_command(command) 968 969 def testing_complete(self, event: tk.Event) -> None: 970 """ 971 Handles the final steps after the Arduino signals test completion. 972 973 Calls `take_input` one last time for the final test pair (using `pair_num_override`). 974 975 Resets the main button state and `self.test_running` flag. Sets the 976 `self.stop_event` to ensure the listener thread terminates cleanly. 977 Calls `auto_update_durations` to process the collected data and suggest timing changes. 978 979 Parameters 980 ---------- 981 - **event** (*tk.Event*): The custom event (`<<event0>>`) generated by `run_test`, carrying the final pair number in `event.state`. 982 """ 983 # take input from last pair/valve 984 self.take_input(event=None, pair_num_override=event.state) 985 # reconfigure the testing button and testing state 986 if isinstance(self.valve_test_button, tk.Button): 987 self.valve_test_button.configure(text="Start Testing", bg="green") 988 989 self.test_running = False 990 # stop the arduino testing listener thread 991 self.stop_event.set() 992 993 # update valves and such 994 self.auto_update_durations() 995 996 def auto_update_durations(self): 997 """ 998 This function is similar to the `views.valve_testing.manual_time_adjustment_window.ManualTimeAdjustment.write_timing_changes` function, it 999 calculates new valve durations based on test results and desired volume. 1000 1001 Loads the current 'selected' durations. Iterates through the collected 1002 `self.ml_dispensed` data. For each tested valve, it calculates the actual 1003 volume dispensed per actuation and compares it to the desired volume per actuation. 1004 It computes a new duration using a ratio (`new_duration = old_duration * (desired_vol / actual_vol)`). 1005 1006 Updates the corresponding duration in the local copies of the side_one/side_two arrays. 1007 Updates the table display for the valve using `update_table_entry_test_status`. 1008 Compiles a list of changes (`changed_durations`) and opens the `ValveChanges` 1009 confirmation window, passing the changes and the `confirm_valve_changes` callback. 1010 """ 1011 # list of tuples of tuples ==> [(valve, (old_dur, new_dur))] sent to ChangesWindow instance, so that 1012 # user can confirm duration changes 1013 changed_durations = [] 1014 1015 ## save current durations in the oldest valve duration archival location 1016 side_one, side_two, date_used = self.arduino_data.load_durations() 1017 1018 side_one_old = side_one.copy() 1019 side_two_old = side_two.copy() 1020 1021 for valve, dispensed_amt in self.ml_dispensed: 1022 logical_valve = None 1023 tested_duration = None 1024 side_durations = None 1025 1026 if valve > VALVES_PER_SIDE: 1027 logical_valve = (valve - 1) - VALVES_PER_SIDE 1028 1029 tested_duration = side_two[logical_valve] 1030 side_durations = side_two 1031 else: 1032 logical_valve = valve - 1 1033 1034 tested_duration = side_one[logical_valve] 1035 side_durations = side_one 1036 1037 # divide by 1000 to get to mL from ul 1038 desired_per_open_vol = self.desired_volume.get() / 1000 1039 1040 # divide by 1000 to get amount per opening, NOT changing units 1041 actual_per_open_vol = dispensed_amt / self.actuations.get() 1042 1043 self.update_table_entry_test_status(valve, actual_per_open_vol) 1044 1045 new_duration = round( 1046 tested_duration * (desired_per_open_vol / actual_per_open_vol) 1047 ) 1048 1049 side_durations[logical_valve] = new_duration 1050 1051 changed_durations.append((valve, (tested_duration, new_duration))) 1052 1053 ValveChanges( 1054 changed_durations, 1055 lambda: self.confirm_valve_changes( 1056 side_one, side_two, side_one_old, side_two_old 1057 ), 1058 ) 1059 1060 def confirm_valve_changes( 1061 self, 1062 side_one: npt.NDArray[np.int32], 1063 side_two: npt.NDArray[np.int32], 1064 side_one_old: npt.NDArray[np.int32], 1065 side_two_old: npt.NDArray[np.int32], 1066 ): 1067 """ 1068 Callback function executed when changes are confirmed in the ValveChanges window. 1069 1070 Instructs `self.arduino_data` to first save the `side_one_old` and `side_two_old` 1071 arrays to an archive slot, and then save the newly calculated `side_one` and `side_two` 1072 arrays as the 'selected' durations. 1073 1074 Parameters 1075 ---------- 1076 - **side_one** (*npt.NDArray[np.int32]*): The numpy array containing the newly calculated durations for side one valves. 1077 - **side_two** (*npt.NDArray[np.int32]*): The numpy array containing the newly calculated durations for side two valves. 1078 - **side_one_old** (*npt.NDArray[np.int32]*): The numpy array containing the durations for side one valves *before* the test. 1079 - **side_two_old** (*npt.NDArray[np.int32]*): The numpy array containing the durations for side two valves *before* the test. 1080 """ 1081 self.arduino_data.save_durations(side_one_old, side_two_old, "archive") 1082 self.arduino_data.save_durations(side_one, side_two, "selected")
This is a bit of a god class and should likely be refactored... but for now... it works!
Implements the main Tkinter Toplevel window for valve testing and priming operations.
This window provides an interface to interact with the Arduino controller for calibrating valve open times based on dispensed volume or for running priming sequences. It features modes for testing and priming, manages valve selections, displays test progress and results, and handles communication with the Arduino.
Attributes
arduino_controller
(ArduinoManager): Reference to the main Arduino controller instance, used for sending commands and data.arduino_data
(ArduinoData): Reference to the data model holding valve duration configurations.window_mode
(WindowMode): Enum indicating the current operational mode (Testing or Priming).test_running
(bool): Flag indicating if an automated test sequence is currently active.prime_running
(bool): Flag indicating if a priming sequence is currently active.desired_volume
(tk.DoubleVar): Tkinter variable storing the target volume (in microliters) per actuation for automatic duration calculation.actuations
(tk.IntVar): Tkinter variable storing the number of times each valve should be actuated during a test or prime sequence.ml_dispensed
(list[tuple[int, float]]): Stores tuples of (valve_number, dispensed_volume_ml) entered by the user during a test.valve_buttons
(list[tk.Button | None]): List holding the Tkinter Button widgets for each valve selection.valve_test_button
(tk.Button | None): The main button used to start/abort tests or start/stop priming.table_entries
(list[str | None]): List holding the item IDs for each row in the ttk.Treeview table, used for modifying/hiding/showing specific rows.valve_selections
(npt.NDArray[np.int8]): Numpy array representing the selection state of each valve (0 if not selected, valve_number if selected).side_one_tests
(npt.NDArray[np.int8] | None): Numpy array holding the valve numbers (0-indexed) selected for testing/priming on side one.side_two_tests
(npt.NDArray[np.int8] | None): Numpy array holding the valve numbers (0-indexed) selected for testing/priming on side two.stop_event
(threading.Event): Event flag used to signal the Arduino listener thread (run_test
) to stop.mode_button
(tk.Button): Button to switch between Testing and Priming modes.dispensed_vol_frame
(tk.Frame): Container frame for the desired volume input elements (visible in Testing mode).valve_table_frame
(tk.Frame): Container frame for the valve test results table (visible in Testing mode).valve_table
(ttk.Treeview): The table widget displaying valve numbers, test status, and timings.valve_buttons_frame
(tk.Frame): Main container frame for the side-by-side valve selection buttons.side_one_valves_frame
(tk.Frame): Frame holding valve selection buttons for the first side.side_two_valves_frame
(tk.Frame): Frame holding valve selection buttons for the second side.manual_adjust_window
(ManualTimeAdjustment): Instance of the separate window for manual timing adjustments.
Methods
setup_basic_window_attr
() Configures basic window properties like title, key bindings, and icon.show
() Makes the window visible (deiconifies).switch_window_mode
() Toggles the window between Testing and Priming modes, adjusting UI elements accordingly.create_interface
() Builds the entire GUI layout, placing widgets and frames.start_testing_toggle
() Handles the press of the main test/prime button in Testing mode (starts or aborts test).create_buttons
() Creates and places the valve selection buttons and the main start/abort button.create_valve_test_table
() Creates the ttk.Treeview table for displaying valve test results and timings.update_table_entry_test_status
(...) Updates the 'Amount Dispensed' column for a specific valve in the table.update_table_entries
(...) Updates the entire valve table, showing/hiding rows based on selections and refreshing currently loaded durations.toggle_valve_button
(...) Handles clicks on valve selection buttons, updating their appearance and thevalve_selections
array.verify_variables
(...) Reads verification data back from Arduino to confirm test parameters were received correctly.verify_schedule
(...) Reads verification data back from Arduino to confirm the valve test/prime schedule was received correctly.send_schedules
() Sends the command, parameters (schedule lengths, actuations), and valve schedule to the Arduino. Handles mode-specific commands and confirmations.stop_priming
() Sends a command to the Arduino to stop an ongoing priming sequence.take_input
(...) Prompts the user (via simpledialog) to enter the dispensed volume for a completed valve test pair. Sends confirmation back to Arduino.run_test
() Runs in a separate thread to listen for messages from the Arduino during a test, triggering events for input prompts or completion.abort_test
() Sends a command to the Arduino to abort the current test and stops the listener thread.testing_complete
(...) Handles the event triggered when the Arduino signals the end of the entire test sequence. Initiates duration updates.auto_update_durations
() Calculates new valve durations based on test results (ml_dispensed
) and opens theValveChanges
confirmation window.confirm_valve_changes
(...) Callback function executed if the user confirms changes in theValveChanges
window. Saves new durations and archives old ones viaArduinoData
.
153 def __init__(self, arduino_controller: ArduinoManager) -> None: 154 """ 155 Initializes the ValveTestWindow. 156 157 Parameters 158 ---------- 159 - **arduino_controller** (*ArduinoManager*): The application's instance of the Arduino controller. 160 161 Raises 162 ------ 163 - Propagates exceptions from `GUIUtils` methods during icon setting. 164 - Propagates exceptions from `arduino_data.load_durations()`. 165 """ 166 super().__init__() 167 self.arduino_controller: ArduinoManager = arduino_controller 168 self.arduino_data: ArduinoData = arduino_controller.arduino_data 169 170 self.setup_basic_window_attr() 171 172 self.window_mode: WindowMode = WindowMode.TESTING 173 174 self.test_running: bool = False 175 self.prime_running: bool = False 176 177 self.desired_volume: tk.DoubleVar = tk.DoubleVar(value=5) 178 179 self.actuations: tk.IntVar = tk.IntVar(value=1000) 180 181 self.ml_dispensed: list[tuple[int, float]] = [] 182 183 self.valve_buttons: list[tk.Button | None] = [None] * TOTAL_VALVES 184 185 self.valve_test_button: tk.Button | None = None 186 187 self.table_entries: list[str | None] = [None] * TOTAL_VALVES 188 189 self.valve_selections: npt.NDArray[np.int8] = np.zeros( 190 (TOTAL_VALVES,), dtype=np.int8 191 ) 192 193 self.side_one_tests: None | npt.NDArray[np.int8] = None 194 self.side_two_tests: None | npt.NDArray[np.int8] = None 195 196 self.stop_event: threading.Event = threading.Event() 197 198 self.create_interface() 199 200 # hide main window for now until we deliberately enter this window. 201 self.withdraw() 202 203 self.manual_adjust_window = ManualTimeAdjustment( 204 self.arduino_controller.arduino_data 205 ) 206 # hide the adjustment window until we need it. 207 self.manual_adjust_window.withdraw() 208 209 logging.info("Valve Test Window initialized.")
Initializes the ValveTestWindow.
Parameters
- arduino_controller (ArduinoManager): The application's instance of the Arduino controller.
Raises
- Propagates exceptions from
GUIUtils
methods during icon setting. - Propagates exceptions from
arduino_data.load_durations()
.
211 def setup_basic_window_attr(self) -> None: 212 """ 213 Sets up fundamental window properties. 214 215 Configures the window title, binds Ctrl+W and the close button ('X') 216 to hide the window, sets grid column/row weights for resizing behavior, 217 and applies the application icon using `GUIUtils`. 218 """ 219 220 self.title("Valve Testing") 221 self.bind("<Control-w>", lambda event: self.withdraw()) 222 self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw()) 223 224 self.grid_columnconfigure(0, weight=1) 225 226 for i in range(5): 227 self.grid_rowconfigure(i, weight=1) 228 self.grid_rowconfigure(0, weight=0) 229 230 window_icon_path = GUIUtils.get_window_icon_path() 231 GUIUtils.set_program_icon(self, icon_path=window_icon_path)
Sets up fundamental window properties.
Configures the window title, binds Ctrl+W and the close button ('X')
to hide the window, sets grid column/row weights for resizing behavior,
and applies the application icon using GUIUtils
.
233 def show(self) -> None: 234 """ 235 Makes the ValveTestWindow visible. 236 237 Calls `deiconify()` to show the window if it was previously hidden (withdrawn). 238 """ 239 self.deiconify()
Makes the ValveTestWindow visible.
Calls deiconify()
to show the window if it was previously hidden (withdrawn).
241 def switch_window_mode(self) -> None: 242 """ 243 Toggles the window's operational mode between Testing and Priming. 244 245 Updates the window title and mode button text/color. Hides or shows 246 mode-specific UI elements (desired volume input, test results table). 247 Changes the command associated with the main start/stop button. 248 Prevents mode switching if a test or prime sequence is currently running to avoid Arduino 249 Communications errors. Resizes and centers the window after GUI changes. 250 """ 251 252 if self.window_mode == WindowMode.TESTING: 253 if self.test_running: 254 # if the test is currently running we do not want to switch modes. Stop testing to switch modes 255 GUIUtils.display_error( 256 "ACTION PROHIBITED", 257 "You cannot switch modes while a test is running. If you'd like to switch modes, please abort the test and try again.", 258 ) 259 return 260 self.title("Valve Priming") 261 262 self.window_mode = WindowMode.PRIMING 263 264 self.mode_button.configure(text="Switch to Testing Mode", bg="DeepSkyBlue3") 265 266 # orig. row 1 267 self.dispensed_vol_frame.grid_forget() 268 # orig. row 3 269 self.valve_table_frame.grid_forget() 270 271 # start testing button -> start priming button / command 272 273 if isinstance(self.valve_test_button, tk.Button): 274 self.valve_test_button.configure( 275 text="Start Priming", 276 bg="coral", 277 command=lambda: self.send_schedules(), 278 ) 279 280 else: 281 if self.prime_running: 282 # if the test is currently running we do not want to switch modes. Stop testing to switch modes 283 GUIUtils.display_error( 284 "ACTION PROHIBITED", 285 "You cannot switch modes while a valve prime operation is running. If you'd like to switch modes, please abort the prime and try again.", 286 ) 287 return 288 289 self.title("Valve Testing") 290 291 self.window_mode = WindowMode.TESTING 292 293 self.mode_button.configure(text="Switch to Priming Mode", bg="coral") 294 295 self.valve_table_frame.grid( 296 row=3, column=0, sticky="nsew", pady=10, padx=10 297 ) 298 self.dispensed_vol_frame.grid(row=1, column=0, padx=5, sticky="nsew") 299 300 if isinstance(self.valve_test_button, tk.Button): 301 self.valve_test_button.configure( 302 text="Start Testing", 303 bg="green", 304 command=lambda: self.start_testing_toggle(), 305 ) 306 307 # resize the window to fit current content 308 self.update_idletasks() 309 GUIUtils.center_window(self)
Toggles the window's operational mode between Testing and Priming.
Updates the window title and mode button text/color. Hides or shows mode-specific UI elements (desired volume input, test results table). Changes the command associated with the main start/stop button. Prevents mode switching if a test or prime sequence is currently running to avoid Arduino Communications errors. Resizes and centers the window after GUI changes.
311 def create_interface(self) -> None: 312 """ 313 Constructs and arranges all GUI elements within the window. 314 315 Creates frames for structure, input fields (desired volume, actuations), 316 the mode switch button, the valve selection buttons, the test results table, 317 and the main start/abort/prime button. Uses `GUIUtils` for 318 widget creation where possible. Calls helper methods `create_valve_test_table` 319 and `create_buttons`. Centers the window after creation. 320 """ 321 322 self.mode_button = GUIUtils.create_button( 323 self, 324 "Switch to Priming Mode", 325 command=lambda: self.switch_window_mode(), 326 bg="coral", 327 row=0, 328 column=0, 329 )[1] 330 331 # create a new frame to contain the desired dispensed volume label and frame 332 self.dispensed_vol_frame = tk.Frame(self) 333 self.dispensed_vol_frame.grid(row=1, column=0, padx=5, sticky="nsew") 334 335 self.dispensed_vol_frame.grid_columnconfigure(0, weight=1) 336 self.dispensed_vol_frame.grid_rowconfigure(0, weight=1) 337 self.dispensed_vol_frame.grid_rowconfigure(1, weight=1) 338 339 label = tk.Label( 340 self.dispensed_vol_frame, 341 text="Desired Dispensed Volume in Microliters (ul)", 342 bg="light blue", 343 font=("Helvetica", 20), 344 highlightthickness=1, 345 highlightbackground="dark blue", 346 height=2, 347 width=50, 348 ) 349 label.grid(row=0, pady=5) 350 351 entry = tk.Entry( 352 self.dispensed_vol_frame, 353 textvariable=self.desired_volume, 354 font=("Helvetica", 24), 355 highlightthickness=1, 356 highlightbackground="black", 357 ) 358 entry.grid(row=1, sticky="nsew", pady=5, ipady=14) 359 360 # create another new frame to contain the amount of times each 361 # valve should actuate label and frame 362 frame = tk.Frame(self) 363 frame.grid(row=2, column=0, padx=5, sticky="nsew") 364 365 frame.grid_columnconfigure(0, weight=1) 366 frame.grid_rowconfigure(0, weight=1) 367 frame.grid_rowconfigure(1, weight=1) 368 369 label = tk.Label( 370 frame, 371 text="How Many Times Should the Valve Actuate?", 372 bg="light blue", 373 font=("Helvetica", 20), 374 highlightthickness=1, 375 highlightbackground="dark blue", 376 height=2, 377 width=50, 378 ) 379 label.grid(row=0, pady=5) 380 381 entry = tk.Entry( 382 frame, 383 textvariable=self.actuations, 384 font=("Helvetica", 24), 385 highlightthickness=1, 386 highlightbackground="black", 387 ) 388 entry.grid(row=1, sticky="nsew", pady=5, ipady=12) 389 390 ### ROW 3 ### 391 self.create_valve_test_table() 392 393 # setup frames for valve buttons 394 self.valve_buttons_frame = tk.Frame( 395 self, highlightbackground="black", highlightthickness=1 396 ) 397 ### ROW 4 ### 398 self.valve_buttons_frame.grid(row=4, column=0, pady=10, padx=10, sticky="nsew") 399 self.valve_buttons_frame.grid_columnconfigure(0, weight=1) 400 self.valve_buttons_frame.grid_rowconfigure(0, weight=1) 401 402 self.side_one_valves_frame = tk.Frame( 403 self.valve_buttons_frame, highlightbackground="black", highlightthickness=1 404 ) 405 self.side_one_valves_frame.grid(row=0, column=0, pady=10, padx=10, sticky="w") 406 self.side_one_valves_frame.grid_columnconfigure(0, weight=1) 407 for i in range(4): 408 self.side_one_valves_frame.grid_rowconfigure(i, weight=1) 409 410 self.side_two_valves_frame = tk.Frame( 411 self.valve_buttons_frame, highlightbackground="black", highlightthickness=1 412 ) 413 self.side_two_valves_frame.grid(row=0, column=1, pady=10, padx=10, sticky="e") 414 self.side_two_valves_frame.grid_columnconfigure(0, weight=1) 415 for i in range(4): 416 self.side_two_valves_frame.grid_rowconfigure(i, weight=1) 417 418 ### ROW 5 ### 419 self.create_buttons() 420 421 self.update_idletasks() 422 GUIUtils.center_window(self)
Constructs and arranges all GUI elements within the window.
Creates frames for structure, input fields (desired volume, actuations),
the mode switch button, the valve selection buttons, the test results table,
and the main start/abort/prime button. Uses GUIUtils
for
widget creation where possible. Calls helper methods create_valve_test_table
and create_buttons
. Centers the window after creation.
424 def start_testing_toggle(self) -> None: 425 """ 426 Handles the action of the main button when in Testing mode. 427 428 If a test is running, it calls `abort_test()` and updates the button appearance. 429 If no test is running, it calls `send_schedules()` to initiate a new test. 430 """ 431 432 if self.test_running: 433 if isinstance(self.valve_test_button, tk.Button): 434 self.valve_test_button.configure(text="Start Testing", bg="green") 435 self.abort_test() 436 else: 437 self.send_schedules()
Handles the action of the main button when in Testing mode.
If a test is running, it calls abort_test()
and updates the button appearance.
If no test is running, it calls send_schedules()
to initiate a new test.
481 def create_valve_test_table(self) -> None: 482 """ 483 Creates, configures, and populates the initial state of the ttk.Treeview table. 484 485 Sets up the table columns ("Valve", "Amount Dispensed...", "Testing Opening Time..."). 486 Adds a button above the table to open the `ManualTimeAdjustment` window. 487 488 Loads the currently selected valve durations from `models.arduino_data.ArduinoData`. 489 Inserts initial rows for all possible valves, marking them as "Test Not Complete" 490 and displaying their current duration. Stores row IDs in `self.table_entries` for later access. 491 """ 492 493 # this function creates and labels the testing window table that displays the information recieved from the arduino reguarding the results of the tes 494 self.valve_table_frame = tk.Frame( 495 self, highlightbackground="black", highlightthickness=1 496 ) 497 self.valve_table_frame.grid(row=3, column=0, sticky="nsew", pady=10, padx=10) 498 # tell the frame to fill the available width 499 self.valve_table_frame.grid_columnconfigure(0, weight=1) 500 501 self.valve_table = ttk.Treeview( 502 self.valve_table_frame, show="headings", height=12 503 ) 504 505 headings = [ 506 "Valve", 507 "Amount Dispensed Per Lick (ul)", 508 "Testing Opening Time of (ms)", 509 ] 510 511 self.valve_table["columns"] = headings 512 513 self.valve_table.grid(row=1, sticky="ew") 514 515 button = tk.Button( 516 self.valve_table_frame, 517 text="Manual Valve Duration Override", 518 command=lambda: self.manual_adjust_window.show(), 519 bg="light blue", 520 highlightbackground="black", 521 highlightthickness=1, 522 ) 523 524 button.grid(row=0, sticky="e") 525 526 # Configure the columns 527 for col in self.valve_table["columns"]: 528 self.valve_table.heading(col, text=col) 529 530 arduino_data = self.arduino_controller.arduino_data 531 532 side_one_durations, side_two_durations, date_used = ( 533 arduino_data.load_durations() 534 ) 535 536 # insert default entries for each total valve until user selects how many 537 # they want to test 538 for i in range(VALVES_PER_SIDE): 539 self.table_entries[i] = self.valve_table.insert( 540 "", 541 i, 542 values=( 543 f"{i + 1}", 544 "Test Not Complete", 545 f"{side_one_durations[i]} ms", 546 ), 547 ) 548 self.table_entries[i + 4] = self.valve_table.insert( 549 "", 550 i + VALVES_PER_SIDE, 551 values=( 552 f"{i + VALVES_PER_SIDE + 1}", 553 "Test Not Complete", 554 f"{side_two_durations[i]} ms", 555 ), 556 )
Creates, configures, and populates the initial state of the ttk.Treeview table.
Sets up the table columns ("Valve", "Amount Dispensed...", "Testing Opening Time...").
Adds a button above the table to open the ManualTimeAdjustment
window.
Loads the currently selected valve durations from models.arduino_data.ArduinoData
.
Inserts initial rows for all possible valves, marking them as "Test Not Complete"
and displaying their current duration. Stores row IDs in self.table_entries
for later access.
558 def update_table_entry_test_status(self, valve: int, l_per_lick: float) -> None: 559 """ 560 Updates the 'Amount Dispensed Per Lick (ul)' column for a specific valve row. 561 562 Parameters 563 ---------- 564 - **valve** (*int*): The 1-indexed valve number whose row needs updating. 565 - **l_per_lick** (*float*): The calculated volume dispensed per lick in milliliters (mL). 566 """ 567 568 self.valve_table.set( 569 self.table_entries[valve - 1], column=1, value=f"{l_per_lick * 1000} uL" 570 )
Updates the 'Amount Dispensed Per Lick (ul)' column for a specific valve row.
Parameters
- valve (int): The 1-indexed valve number whose row needs updating.
- l_per_lick (float): The calculated volume dispensed per lick in milliliters (mL).
572 def update_table_entries(self) -> None: 573 """ 574 Refreshes the entire valve test table based on current selections and durations. 575 576 Hides rows for unselected valves and shows rows for selected valves. 577 Reloads the latest durations from `ArduinoData` and updates the 578 'Testing Opening Time' column for all visible rows. Resets the 579 'Amount Dispensed' column to "Test Not Complete" for visible rows. 580 """ 581 side_one, side_two, _ = self.arduino_data.load_durations() 582 # np array where the entry is not zero but the valve number itself 583 selected_valves = self.valve_selections[self.valve_selections != 0] 584 585 # for all valve numbers, if that number is in selected_valves -1 (the entire array 586 # decremented by one) place it in the table and ensure its duration is up-to-date 587 for i in range(TOTAL_VALVES): 588 # if the iter number is also contained in the selected valves arr 589 if i in (selected_valves - 1): 590 # check if it's side two or side one 591 if i >= VALVES_PER_SIDE: 592 duration = side_two[i - VALVES_PER_SIDE] 593 else: 594 duration = side_one[i] 595 self.valve_table.item( 596 self.table_entries[i], 597 values=( 598 f"{i + 1}", 599 "Test Not Complete", 600 f"{duration} ms", 601 ), 602 ) 603 self.valve_table.reattach(self.table_entries[i], "", i) 604 else: 605 self.valve_table.detach(self.table_entries[i]) 606 607 logging.info("Valve test table updated.")
Refreshes the entire valve test table based on current selections and durations.
Hides rows for unselected valves and shows rows for selected valves.
Reloads the latest durations from ArduinoData
and updates the
'Testing Opening Time' column for all visible rows. Resets the
'Amount Dispensed' column to "Test Not Complete" for visible rows.
636 def verify_variables(self, original_data: bytes) -> bool: 637 """ 638 Verifies that the Arduino correctly received the test/prime parameters. 639 640 Waits for and reads back the schedule lengths and actuation count echoed 641 by the Arduino. Compares the received bytes with the originally sent data. 642 643 Parameters 644 ---------- 645 - **original_data** (*bytes*): The byte string containing schedule lengths and actuation count originally sent to the Arduino. 646 647 Returns 648 ------- 649 - *bool*: `True` if the received data matches the original data, `False` otherwise (logs error and shows message box on mismatch). 650 """ 651 # wait until all data is on the wire ready to be read (4 bytes), 652 # then read it all in quick succession 653 if self.arduino_controller.arduino is None: 654 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 655 GUIUtils.display_error("TRANSMISSION ERROR", msg) 656 logger.error(msg) 657 return False 658 659 while self.arduino_controller.arduino.in_waiting < 4: 660 pass 661 662 ver_len_sched_sd_one = self.arduino_controller.arduino.read() 663 ver_len_sched_sd_two = self.arduino_controller.arduino.read() 664 665 ver_valve_act_time = self.arduino_controller.arduino.read(2) 666 667 verification_data = ( 668 ver_len_sched_sd_one + ver_len_sched_sd_two + ver_valve_act_time 669 ) 670 if verification_data == original_data: 671 logger.info("====VERIFIED TEST VARIABLES====") 672 return True 673 else: 674 msg = "====INCORRECT VARIABLES DETECTED, TRY AGAIN====" 675 GUIUtils.display_error("TRANSMISSION ERROR", msg) 676 logger.error(msg) 677 return False
Verifies that the Arduino correctly received the test/prime parameters.
Waits for and reads back the schedule lengths and actuation count echoed by the Arduino. Compares the received bytes with the originally sent data.
Parameters
- original_data (bytes): The byte string containing schedule lengths and actuation count originally sent to the Arduino.
Returns
- bool:
True
if the received data matches the original data,False
otherwise (logs error and shows message box on mismatch).
679 def verify_schedule( 680 self, len_sched: int, sent_schedule: npt.NDArray[np.int8] 681 ) -> bool: 682 """ 683 Verifies that the Arduino correctly received the valve schedule. 684 685 Waits for and reads back the valve numbers (0-indexed) echoed byte-by-byte 686 by the Arduino. Compares the reconstructed numpy array with the schedule 687 that was originally sent. 688 689 Parameters 690 ---------- 691 - **len_sched** (*int*): The total number of valves in the schedule being sent. 692 - **sent_schedule** (*npt.NDArray[np.int8]*): The numpy array (0-indexed valve numbers) representing the schedule originally sent. 693 694 Returns 695 ------- 696 - *bool*: `True` if the received schedule matches the sent schedule, `False` otherwise (logs error and shows message box on mismatch). 697 """ 698 # np array used for verification later 699 received_sched = np.zeros((len_sched,), dtype=np.int8) 700 701 if self.arduino_controller.arduino is None: 702 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 703 GUIUtils.display_error("TRANSMISSION ERROR", msg) 704 logger.error(msg) 705 return False 706 707 while self.arduino_controller.arduino.in_waiting < len_sched: 708 pass 709 710 for i in range(len_sched): 711 received_sched[i] = int.from_bytes( 712 self.arduino_controller.arduino.read(), byteorder="little", signed=False 713 ) 714 715 if np.array_equal(received_sched, sent_schedule): 716 logger.info(f"===TESTING SCHEDULE VERIFIED as ==> {received_sched}===") 717 logger.info("====================BEGIN TESTING NOW====================") 718 return True 719 else: 720 msg = "====INCORRECT SCHEDULE DETECTED, TRY AGAIN====" 721 GUIUtils.display_error("TRANSMISSION ERROR", msg) 722 logger.error(msg) 723 return False
Verifies that the Arduino correctly received the valve schedule.
Waits for and reads back the valve numbers (0-indexed) echoed byte-by-byte by the Arduino. Compares the reconstructed numpy array with the schedule that was originally sent.
Parameters
- len_sched (int): The total number of valves in the schedule being sent.
- sent_schedule (npt.NDArray[np.int8]): The numpy array (0-indexed valve numbers) representing the schedule originally sent.
Returns
- bool:
True
if the received schedule matches the sent schedule,False
otherwise (logs error and shows message box on mismatch).
725 def send_schedules(self) -> None: 726 """ 727 Performs the process of sending test or prime configuration to the Arduino. 728 729 Determines selected valves and schedule lengths for each side. Sends current 730 valve durations to Arduino. Then sends the appropriate command ('TEST VOL' or 'PRIME VALVES'). 731 732 Sends test parameters (schedule lengths, actuations) and verifies them using `verify_variables`. 733 Sends the actual valve schedule (0-indexed) and verifies it using `verify_schedule`. 734 735 User is then prompted to confirm before proceeding. 736 If confirmed, initiates the test/prime sequence on the Arduino 737 and starts the listener thread (`run_test`) if Testing, or sends start command if Priming. 738 Updates button states accordingly. 739 """ 740 len_sched_one = np.count_nonzero(self.valve_selections[:VALVES_PER_SIDE]) 741 len_sched_two = np.count_nonzero(self.valve_selections[VALVES_PER_SIDE:]) 742 743 len_sched_one = np.int8(len_sched_one) 744 len_sched_two = np.int8(len_sched_two) 745 len_sched = len_sched_one + len_sched_two 746 747 # because self.actuations is a IntVar, we use .get() method to get its value 748 valve_acuations = np.int16(self.actuations.get()) 749 750 # schedule is TOTAL_VALVES long to accomodate testing of up to all valves at one time. 751 # if we are not testing them all, filter out the zero entries (valves we are not testing) 752 schedule = self.valve_selections[self.valve_selections != 0] 753 754 # decrement each element in the array by one to move to zero-indexing. We moved to 1 755 # indexing becuase using zero indexing does not work with the method of toggling valves 756 # defined above 757 schedule -= 1 758 759 self.side_one_tests = schedule[:len_sched_one] 760 self.side_two_tests = schedule[len_sched_one:] 761 762 len_sched_one = len_sched_one 763 len_sched_two = len_sched_two 764 valve_acuations = valve_acuations 765 766 self.arduino_controller.send_valve_durations() 767 if self.window_mode == WindowMode.TESTING: 768 ###====SENDING TEST COMMAND====### 769 command = "TEST VOL\n".encode("utf-8") 770 self.arduino_controller.send_command(command) 771 772 elif self.window_mode == WindowMode.PRIMING: 773 self.prime_running = True 774 775 command = "PRIME VALVES\n".encode("utf-8") 776 self.arduino_controller.send_command(command) 777 778 ###====SENDING TEST VARIABLES (len side one, two, num_actuations per test)====### 779 data_bytes = ( 780 len_sched_one.tobytes() 781 + len_sched_two.tobytes() 782 + valve_acuations.tobytes() 783 ) 784 self.arduino_controller.send_command(data_bytes) 785 786 # verify the data 787 if not self.verify_variables(data_bytes): 788 return 789 790 ###====SENDING SCHEDULE====### 791 sched_data_bytes = schedule.tobytes() 792 self.arduino_controller.send_command(sched_data_bytes) 793 794 if not self.verify_schedule(len_sched, schedule): 795 return 796 797 schedule += 1 798 799 valves = ",".join(str(valve) for valve in schedule) 800 if self.window_mode == WindowMode.TESTING: 801 ####### ARDUINO HALTS HERE UNTIL THE GO-AHEAD IS GIVEN ####### 802 # inc each sched element by one so that it is 1-indexes and matches the valve labels 803 test_confirmed = GUIUtils.askyesno( 804 "CONFIRM THE TEST SCHEDULE", 805 f"Valves {valves}, will be tested. Review the test table to confirm schedule and timings. Each valve will be actuated {valve_acuations} times. Ok to begin?", 806 ) 807 808 ### include section to manually modify timings 809 if test_confirmed: 810 self.bind("<<event0>>", self.testing_complete) 811 self.bind("<<event1>>", self.take_input) 812 threading.Thread(target=self.run_test).start() 813 else: 814 self.abort_test() 815 elif self.window_mode == WindowMode.PRIMING: 816 priming_confirmed = GUIUtils.askyesno( 817 "CONFIRM THE ACTION", 818 f"Valves {valves}, will be primed (opened and closed) {valve_acuations} times. Ok to begin?", 819 ) 820 if priming_confirmed: 821 # in case of prime, we send 0 to mean continue or start 822 # and we send a 1 at any point to abort or cancel the prime 823 start = np.int8(0).tobytes() 824 self.arduino_controller.send_command(command=start) 825 826 if isinstance(self.valve_test_button, tk.Button): 827 self.valve_test_button.configure( 828 text="STOP Priming", 829 bg="gold", 830 command=lambda: self.stop_priming(), 831 )
Performs the process of sending test or prime configuration to the Arduino.
Determines selected valves and schedule lengths for each side. Sends current valve durations to Arduino. Then sends the appropriate command ('TEST VOL' or 'PRIME VALVES').
Sends test parameters (schedule lengths, actuations) and verifies them using verify_variables
.
Sends the actual valve schedule (0-indexed) and verifies it using verify_schedule
.
User is then prompted to confirm before proceeding.
If confirmed, initiates the test/prime sequence on the Arduino
and starts the listener thread (run_test
) if Testing, or sends start command if Priming.
Updates button states accordingly.
833 def stop_priming(self) -> None: 834 """ 835 Sends the stop command to the Arduino during a priming sequence. 836 837 Sets the `prime_running` flag to False. Updates the prime button state. 838 Sends a byte representation of 1 (`np.int8(1).tobytes()`) to signal abort to the Arduino. 839 """ 840 self.prime_running = False 841 842 if isinstance(self.valve_test_button, tk.Button): 843 self.valve_test_button.configure( 844 text="Start Priming", 845 bg="coral", 846 command=lambda: self.send_schedules(), 847 ) 848 849 stop = np.int8(1).tobytes() 850 self.arduino_controller.send_command(command=stop)
Sends the stop command to the Arduino during a priming sequence.
Sets the prime_running
flag to False. Updates the prime button state.
Sends a byte representation of 1 (np.int8(1).tobytes()
) to signal abort to the Arduino.
852 def take_input( 853 self, event: tk.Event | None, pair_num_override: int | None = None 854 ) -> None: 855 """ 856 Prompts the user to enter the dispensed volume for a completed test pair. 857 858 Determines which valve(s) were just tested based on the pair number received 859 from the Arduino (via the event state or override). Uses `simpledialog.askfloat` 860 to get the dispensed volume (in mL) for each valve in the pair. Stores the 861 results in `self.ml_dispensed`. 862 863 Sends a byte back to the Arduino to instruct if further pairs will be tested or if this is the final iteration, 864 (1 if via event, 0 if via override/final pair). 865 Handles potential abort if the user cancels the dialog window. 866 867 Parameters 868 ---------- 869 - **event** (*tk.Event | None*): Uses custom thread-safe event (`<<event1>>`) generated by `run_test`, carrying the pair number in `event.state` 870 attribute. This is None if called directly by `testing_complete`, which indicated procedure is now finished. 871 - **pair_num_override** (*int | None, optional*): Allows manually specifying the pair number, used when called from `testing_complete` for the 872 final pair. Defaults to None. 873 """ 874 if event is not None: 875 pair_number = event.state 876 else: 877 pair_number = pair_num_override 878 879 # decide how many valves were tested last. if 0 we will not arrive 880 # here so we need not test for this. 881 if pair_number < len(self.side_one_tests) and pair_number < len( 882 self.side_two_tests 883 ): 884 valves_tested = [ 885 self.side_one_tests[pair_number], 886 self.side_two_tests[pair_number], 887 ] 888 elif pair_number < len(self.side_one_tests): 889 valves_tested = [ 890 self.side_one_tests[pair_number], 891 ] 892 else: 893 valves_tested = [ 894 self.side_two_tests[pair_number], 895 ] 896 897 # for each test that we ran store the amount dispensed or abort 898 for valve in valves_tested: 899 response = simpledialog.askfloat( 900 "INPUT AMOUNT DISPENSED", 901 f"Please input the amount of liquid dispensed for the test of valve {valve}", 902 ) 903 if response is None: 904 self.abort_test() 905 else: 906 self.ml_dispensed.append((valve, response)) 907 if pair_num_override is not None: 908 self.arduino_controller.send_command(np.int8(0).tobytes()) 909 else: 910 self.arduino_controller.send_command(np.int8(1).tobytes())
Prompts the user to enter the dispensed volume for a completed test pair.
Determines which valve(s) were just tested based on the pair number received
from the Arduino (via the event state or override). Uses simpledialog.askfloat
to get the dispensed volume (in mL) for each valve in the pair. Stores the
results in self.ml_dispensed
.
Sends a byte back to the Arduino to instruct if further pairs will be tested or if this is the final iteration, (1 if via event, 0 if via override/final pair). Handles potential abort if the user cancels the dialog window.
Parameters
- event (tk.Event | None): Uses custom thread-safe event (
<<event1>>
) generated byrun_test
, carrying the pair number inevent.state
attribute. This is None if called directly bytesting_complete
, which indicated procedure is now finished. - pair_num_override (int | None, optional): Allows manually specifying the pair number, used when called from
testing_complete
for the final pair. Defaults to None.
912 def run_test(self) -> None: 913 """ 914 Listens for messages from the Arduino during an active test sequence (runs in a background thread). 915 916 Sends the initial 'start test' command byte to the Arduino. Enters a loop 917 that continues as long as `self.test_running` is True and `self.stop_event` 918 is not set. Checks for incoming data from the Arduino. Reads bytes indicating 919 if more tests remain and the number of the just-completed pair. Generates 920 custom Tkinter events (`<<event1>>` for input dispensed liquid prompt, `<<event0>>` for test completion) 921 to communicate back to the main GUI thread safely. Updates the main button state to 'ABORT TESTING'. 922 """ 923 if self.stop_event.is_set(): 924 self.stop_event.clear() 925 926 if isinstance(self.valve_test_button, tk.Button): 927 self.valve_test_button.configure(text="ABORT TESTING", bg="red") 928 929 self.test_running = True 930 ###====SENDING BEGIN TEST COMMAND====### 931 command = np.int8(1).tobytes() 932 self.arduino_controller.send_command(command) 933 934 while self.test_running: 935 # if stop event, shut down the thread 936 if self.stop_event.is_set(): 937 break 938 if self.arduino_controller.arduino.in_waiting > 0: 939 remaining_tests = self.arduino_controller.arduino.read() 940 pair_number = int.from_bytes( 941 self.arduino_controller.arduino.read(), 942 byteorder="little", 943 signed=False, 944 ) 945 946 if remaining_tests == np.int8(1).tobytes(): 947 self.event_generate("<<event1>>", when="tail", state=pair_number) 948 if remaining_tests == np.int8(0).tobytes(): 949 self.event_generate("<<event0>>", when="tail", state=pair_number)
Listens for messages from the Arduino during an active test sequence (runs in a background thread).
Sends the initial 'start test' command byte to the Arduino. Enters a loop
that continues as long as self.test_running
is True and self.stop_event
is not set. Checks for incoming data from the Arduino. Reads bytes indicating
if more tests remain and the number of the just-completed pair. Generates
custom Tkinter events (<<event1>>
for input dispensed liquid prompt, <<event0>>
for test completion)
to communicate back to the main GUI thread safely. Updates the main button state to 'ABORT TESTING'.
951 def abort_test(self) -> None: 952 """ 953 Aborts an ongoing test sequence. 954 955 Sets the `self.stop_event` to signal the `run_test` listener thread to exit. 956 Sets `self.test_running` to False. Sends the abort *testing* command byte 957 (`np.int8(0).tobytes()`) to the Arduino. Resets the main button state to 'Start Testing'. 958 """ 959 # if test is aborted, stop the test listener thread 960 if isinstance(self.valve_test_button, tk.Button): 961 self.valve_test_button.configure(text="Start Testing", bg="green") 962 963 self.stop_event.set() 964 self.test_running = False 965 ###====SENDING ABORT TEST COMMAND====### 966 command = np.int8(0).tobytes() 967 self.arduino_controller.send_command(command)
Aborts an ongoing test sequence.
Sets the self.stop_event
to signal the run_test
listener thread to exit.
Sets self.test_running
to False. Sends the abort testing command byte
(np.int8(0).tobytes()
) to the Arduino. Resets the main button state to 'Start Testing'.
969 def testing_complete(self, event: tk.Event) -> None: 970 """ 971 Handles the final steps after the Arduino signals test completion. 972 973 Calls `take_input` one last time for the final test pair (using `pair_num_override`). 974 975 Resets the main button state and `self.test_running` flag. Sets the 976 `self.stop_event` to ensure the listener thread terminates cleanly. 977 Calls `auto_update_durations` to process the collected data and suggest timing changes. 978 979 Parameters 980 ---------- 981 - **event** (*tk.Event*): The custom event (`<<event0>>`) generated by `run_test`, carrying the final pair number in `event.state`. 982 """ 983 # take input from last pair/valve 984 self.take_input(event=None, pair_num_override=event.state) 985 # reconfigure the testing button and testing state 986 if isinstance(self.valve_test_button, tk.Button): 987 self.valve_test_button.configure(text="Start Testing", bg="green") 988 989 self.test_running = False 990 # stop the arduino testing listener thread 991 self.stop_event.set() 992 993 # update valves and such 994 self.auto_update_durations()
Handles the final steps after the Arduino signals test completion.
Calls take_input
one last time for the final test pair (using pair_num_override
).
Resets the main button state and self.test_running
flag. Sets the
self.stop_event
to ensure the listener thread terminates cleanly.
Calls auto_update_durations
to process the collected data and suggest timing changes.
Parameters
- event (tk.Event): The custom event (
<<event0>>
) generated byrun_test
, carrying the final pair number inevent.state
.
996 def auto_update_durations(self): 997 """ 998 This function is similar to the `views.valve_testing.manual_time_adjustment_window.ManualTimeAdjustment.write_timing_changes` function, it 999 calculates new valve durations based on test results and desired volume. 1000 1001 Loads the current 'selected' durations. Iterates through the collected 1002 `self.ml_dispensed` data. For each tested valve, it calculates the actual 1003 volume dispensed per actuation and compares it to the desired volume per actuation. 1004 It computes a new duration using a ratio (`new_duration = old_duration * (desired_vol / actual_vol)`). 1005 1006 Updates the corresponding duration in the local copies of the side_one/side_two arrays. 1007 Updates the table display for the valve using `update_table_entry_test_status`. 1008 Compiles a list of changes (`changed_durations`) and opens the `ValveChanges` 1009 confirmation window, passing the changes and the `confirm_valve_changes` callback. 1010 """ 1011 # list of tuples of tuples ==> [(valve, (old_dur, new_dur))] sent to ChangesWindow instance, so that 1012 # user can confirm duration changes 1013 changed_durations = [] 1014 1015 ## save current durations in the oldest valve duration archival location 1016 side_one, side_two, date_used = self.arduino_data.load_durations() 1017 1018 side_one_old = side_one.copy() 1019 side_two_old = side_two.copy() 1020 1021 for valve, dispensed_amt in self.ml_dispensed: 1022 logical_valve = None 1023 tested_duration = None 1024 side_durations = None 1025 1026 if valve > VALVES_PER_SIDE: 1027 logical_valve = (valve - 1) - VALVES_PER_SIDE 1028 1029 tested_duration = side_two[logical_valve] 1030 side_durations = side_two 1031 else: 1032 logical_valve = valve - 1 1033 1034 tested_duration = side_one[logical_valve] 1035 side_durations = side_one 1036 1037 # divide by 1000 to get to mL from ul 1038 desired_per_open_vol = self.desired_volume.get() / 1000 1039 1040 # divide by 1000 to get amount per opening, NOT changing units 1041 actual_per_open_vol = dispensed_amt / self.actuations.get() 1042 1043 self.update_table_entry_test_status(valve, actual_per_open_vol) 1044 1045 new_duration = round( 1046 tested_duration * (desired_per_open_vol / actual_per_open_vol) 1047 ) 1048 1049 side_durations[logical_valve] = new_duration 1050 1051 changed_durations.append((valve, (tested_duration, new_duration))) 1052 1053 ValveChanges( 1054 changed_durations, 1055 lambda: self.confirm_valve_changes( 1056 side_one, side_two, side_one_old, side_two_old 1057 ), 1058 )
This function is similar to the views.valve_testing.manual_time_adjustment_window.ManualTimeAdjustment.write_timing_changes
function, it
calculates new valve durations based on test results and desired volume.
Loads the current 'selected' durations. Iterates through the collected
self.ml_dispensed
data. For each tested valve, it calculates the actual
volume dispensed per actuation and compares it to the desired volume per actuation.
It computes a new duration using a ratio (new_duration = old_duration * (desired_vol / actual_vol)
).
Updates the corresponding duration in the local copies of the side_one/side_two arrays.
Updates the table display for the valve using update_table_entry_test_status
.
Compiles a list of changes (changed_durations
) and opens the ValveChanges
confirmation window, passing the changes and the confirm_valve_changes
callback.
1060 def confirm_valve_changes( 1061 self, 1062 side_one: npt.NDArray[np.int32], 1063 side_two: npt.NDArray[np.int32], 1064 side_one_old: npt.NDArray[np.int32], 1065 side_two_old: npt.NDArray[np.int32], 1066 ): 1067 """ 1068 Callback function executed when changes are confirmed in the ValveChanges window. 1069 1070 Instructs `self.arduino_data` to first save the `side_one_old` and `side_two_old` 1071 arrays to an archive slot, and then save the newly calculated `side_one` and `side_two` 1072 arrays as the 'selected' durations. 1073 1074 Parameters 1075 ---------- 1076 - **side_one** (*npt.NDArray[np.int32]*): The numpy array containing the newly calculated durations for side one valves. 1077 - **side_two** (*npt.NDArray[np.int32]*): The numpy array containing the newly calculated durations for side two valves. 1078 - **side_one_old** (*npt.NDArray[np.int32]*): The numpy array containing the durations for side one valves *before* the test. 1079 - **side_two_old** (*npt.NDArray[np.int32]*): The numpy array containing the durations for side two valves *before* the test. 1080 """ 1081 self.arduino_data.save_durations(side_one_old, side_two_old, "archive") 1082 self.arduino_data.save_durations(side_one, side_two, "selected")
Callback function executed when changes are confirmed in the ValveChanges window.
Instructs self.arduino_data
to first save the side_one_old
and side_two_old
arrays to an archive slot, and then save the newly calculated side_one
and side_two
arrays as the 'selected' durations.
Parameters
- side_one (npt.NDArray[np.int32]): The numpy array containing the newly calculated durations for side one valves.
- side_two (npt.NDArray[np.int32]): The numpy array containing the newly calculated durations for side two valves.
- side_one_old (npt.NDArray[np.int32]): The numpy array containing the durations for side one valves before the test.
- side_two_old (npt.NDArray[np.int32]): The numpy array containing the durations for side two valves before the test.