manual_time_adjustment_window
This module defines the ManualTimeAdjustment class, a Tkinter Toplevel window responsible for allowing users to view, modify, and save valve open duration timings used by the Arduino controller.
It provides an interface to load different timing profiles (default, last used,
archived), edit individual valve timings (in microseconds), and save the
updated configuration, which also involves archiving the previously active
timings. It relies on configuration loaded via system_config
and interacts
with an models.arduino_data
object so that the Arduino can 'remember' durations
over many experiments.
1""" 2This module defines the ManualTimeAdjustment class, a Tkinter Toplevel window 3responsible for allowing users to view, modify, and save valve open duration 4timings used by the Arduino controller. 5 6It provides an interface to load different timing profiles (default, last used, 7archived), edit individual valve timings (in microseconds), and save the 8updated configuration, which also involves archiving the previously active 9timings. It relies on configuration loaded via `system_config` and interacts 10with an `models.arduino_data` object so that the Arduino can 'remember' durations 11over many experiments. 12""" 13 14import tkinter as tk 15from tkinter import ttk 16import numpy as np 17import toml 18from models.arduino_data import ArduinoData 19import system_config 20import logging 21 22from views.gui_common import GUIUtils 23 24###TYPE HINTS### 25import numpy.typing as npt 26import datetime 27###TYPE HINTS### 28 29logger = logging.getLogger() 30 31rig_config = system_config.get_rig_config() 32 33with open(rig_config, "r") as f: 34 VALVE_CONFIG = toml.load(f)["valve_config"] 35 36# pull total valves constant from toml config 37TOTAL_VALVES = VALVE_CONFIG["TOTAL_CURRENT_VALVES"] 38 39VALVES_PER_SIDE = TOTAL_VALVES // 2 40 41 42class ManualTimeAdjustment(tk.Toplevel): 43 """ 44 Implements a Tkinter Toplevel window for manually adjusting valve open durations. 45 46 This window allows users to: 47 - Select and load different valve timing profiles (Default, Last Used, Archives). 48 - View and edit the open duration (in microseconds) for each valve. 49 - Save the modified timings, which automatically archives the previously 50 'selected' timings and sets the new ones as 'selected'. 51 52 It interacts with `models.arduino_data` object (passed during initialization) 53 to load and persist timing configurations. Uses `views.gui_common.GUIUtils` for standard 54 widget creation and layout. 55 56 Attributes 57 ---------- 58 - **arduino_data** (*ArduinoData*): This is a reference to the program instance of `models.arduino_data` ArduinoData. It allows access to this 59 class and its methods which allows for the loading of data stored there such as valve duration times and schedule indicies. 60 - **`tk_vars`** (*dict[str, tk.IntVar]*): Dictionary mapping valve names (e.g., "Valve 1") to Tkinter IntVars holding their durations. 61 - **`labelled_entries`** (*list[tuple OR None]*): List storing tuples of (Frame, Label, Entry) widgets for each valve timing input. Stored to make possible later 62 modification of the widgets. 63 - **`dropdown`** (*ttk.Combobox*): Widget for selecting the duration profile to load/view. 64 - **`date_used_label`** (*tk.Label*): Label displaying the timestamp of the currently loaded profile. 65 - **`timing_frame`** (*tk.Frame*): Frame containing the grid of labeled entries for valve timings. 66 - **`save_changes_bttn`** (*tk.Button*): Button to trigger the save operation. 67 - **`duration_types`** (*dict[str, str]*): Maps user friendly display names in the dropdown to internal keys used for loading profiles. 68 69 Methods 70 ------- 71 - `show`() 72 Updates the interface with the latest data and makes the window visible. 73 - `update_interface`(...) 74 Callback for dropdown selection; loads and displays the chosen timing profile. 75 - `create_dropdown`() 76 Creates and configures the Combobox for selecting duration profiles. 77 - `create_interface`(...) 78 Constructs the main GUI layout, including labels, dropdown, entry fields, and buttons. 79 - `fill_tk_vars`(...) 80 Populates `self.tk_vars` with Tkinter variables based on loaded duration arrays. 81 - `write_timing_changes`() 82 Handles archiving old timings, validating user input, and saving new timings in 'selected' profile. 83 """ 84 85 def __init__(self, arduino_data: ArduinoData): 86 """ 87 Initializes the ManualTimeAdjustment Toplevel window. 88 89 Parameters 90 ---------- 91 - **arduino_data** (*ArduinoData*): Reference to program instance of `models.arduino_data.ArduinoData`, used for loading and saving valve durations. 92 93 Raises 94 ------ 95 - Propagates exceptions from `GUIUtils` methods during icon setting. 96 - Propagates exceptions from `arduino_data.load_durations()`. 97 """ 98 super().__init__() 99 100 self.arduino_data = arduino_data 101 102 self.title("Manual Valve Timing Adjustment") 103 self.bind("<Control-w>", lambda event: self.withdraw()) 104 105 self.resizable(False, False) 106 107 self.tk_vars: dict[str, tk.IntVar] = {} 108 self.labelled_entries: list[tuple | None] = [None] * TOTAL_VALVES 109 110 # load default 'selected' dur profile 111 side_one, side_two, date_used = self.arduino_data.load_durations() 112 113 self.fill_tk_vars(side_one, side_two) 114 self.create_interface(date_used) 115 116 window_icon_path = GUIUtils.get_window_icon_path() 117 GUIUtils.set_program_icon(self, icon_path=window_icon_path) 118 119 def show(self): 120 """ 121 Updates the interface with either default 'Last Used' profile data or selected profile from dropdown and makes the window visible. 122 """ 123 self.update_interface(event=None) 124 self.deiconify() 125 126 def update_interface(self, event: tk.Event | None): 127 """ 128 Loads and displays the valve timings based on the selected profile from the dropdown. 129 130 If `event` is None (e.g., called from `show`), it loads the 'Last Used' 131 profile. Otherwise, it uses the profile selected in the dropdown. Updates 132 the *tk.IntVar* variables, which refreshes the Entry widgets, and updates 133 the 'date last used' label. 134 135 Parameters 136 ---------- 137 - **event** (*tk.Event | None*): The Combobox selection event, or None to default 138 to loading the 'Last Used' profile. 139 140 Raises 141 ------ 142 - Propagates exceptions from `arduino_data.load_durations()`. 143 """ 144 145 # event is the selection event of the self.combobox. using .get gets the self.duration_type dict key 146 load_durations_key = None 147 if event is None: 148 load_durations_key = self.duration_types["Last Used"] 149 else: 150 load_durations_key = self.duration_types[f"{event.widget.get()}"] 151 152 side_one, side_two, _ = self.arduino_data.load_durations(load_durations_key) 153 154 # set default durations for side one 155 for i, duration in enumerate(side_one): 156 self.tk_vars[f"Valve {i + 1}"].set(duration) 157 # set default durations for side two 158 for i, duration in enumerate(side_two): 159 self.tk_vars[f"Valve {i + 8 + 1}"].set(duration) 160 161 def create_dropdown(self): 162 """ 163 Creates the `ttk.Combobox` widget for selecting duration profiles. 164 165 Populates the dropdown with user-friendly names for duration profiles and maps them to keys in 166 `self.duration_types` dict, sets the default selection to 'Last Used', 167 and binds the selection event to `self.update_interface`. 168 """ 169 self.duration_types = { 170 "Default": "default_durations", 171 "Last Used": "selected_durations", 172 "(most recent) Archive 1": "archive_1", 173 "Archive 2": "archive_2", 174 "(oldest) Archive 3": "archive_3", 175 } 176 self.dropdown = ttk.Combobox(self, values=list(self.duration_types.keys())) 177 self.dropdown.current(1) 178 self.dropdown.grid(row=1, column=0, sticky="e", padx=10) 179 self.dropdown.bind("<<ComboboxSelected>>", lambda e: self.update_interface(e)) 180 181 def create_interface(self, date_used: datetime.datetime): 182 """ 183 Constructs the main GUI elements within the Toplevel window. 184 185 Creates labels, the duration profile dropdown, the grid of labeled 186 entries for valve timings, and the save button. Uses `views.gui_common.GUIUtils` for 187 widget creation. 188 189 Parameters 190 ---------- 191 - **date_used** (*datetime.datetime*): The timestamp for date selected profile was created, 192 used to populate the date label. 193 194 Raises 195 ------ 196 - Propagates exceptions from `GUIUtils` methods during widget creation. 197 """ 198 warning = tk.Label( 199 self, 200 text="ALL TIMINGS IN MICROSECONDS (e.g 24125 equivalent to 24.125 ms)", 201 bg="white", 202 fg="black", 203 font=("Helvetica", 15), 204 highlightthickness=1, 205 highlightbackground="black", 206 ) 207 208 warning.grid(row=0, column=0, sticky="nsew", pady=10, padx=10) 209 210 self.create_dropdown() 211 212 formatted_date = date_used.strftime("%B/%d/%Y") 213 formatted_time = date_used.strftime("%I:%M %p") 214 self.date_used_label = tk.Label( 215 self, 216 text=f"Timing profile created on: {formatted_date} at {formatted_time}", 217 bg="white", 218 fg="black", 219 font=("Helvetica", 15), 220 highlightthickness=1, 221 highlightbackground="black", 222 ) 223 self.date_used_label.grid(row=1, column=0, sticky="w", padx=10) 224 225 self.timing_frame = tk.Frame( 226 self, highlightbackground="black", highlightthickness=1 227 ) 228 229 self.timing_frame.grid(row=2, column=0, pady=10, padx=10, sticky="nsew") 230 self.timing_frame.grid_columnconfigure(0, weight=1) 231 232 for i in range(VALVES_PER_SIDE): 233 # GUIUtils.create_button returns both the buttons frame and button object in a tuple. using [1] on the 234 # return item yields the button object. 235 236 self.labelled_entries[i] = GUIUtils.create_labeled_entry( 237 self.timing_frame, 238 f"Valve {i + 1} Open Time", 239 self.tk_vars[f"Valve {i + 1}"], 240 i, 241 0, 242 ) 243 244 self.labelled_entries[i] = GUIUtils.create_labeled_entry( 245 self.timing_frame, 246 f"Valve {i + (VALVES_PER_SIDE) + 1} Open Time", 247 self.tk_vars[f"Valve {i + (TOTAL_VALVES) + 1}"], 248 i, 249 1, 250 ) 251 252 warning = tk.Label( 253 self, 254 text="This action will archive the current timing configuration into assets/valve_durations.toml\n and set these new timings to selected timings", 255 bg="white", 256 fg="black", 257 font=("Helvetica", 15), 258 highlightthickness=1, 259 highlightbackground="black", 260 ) 261 262 warning.grid(row=3, column=0, padx=10) 263 self.save_changes_bttn = GUIUtils.create_button( 264 self, 265 "Write Timing Changes", 266 lambda: self.write_timing_changes(), 267 bg="green", 268 row=4, 269 column=0, 270 )[1] 271 272 def fill_tk_vars( 273 self, side_one: npt.NDArray[np.int32], side_two: npt.NDArray[np.int32] 274 ): 275 """ 276 Populates the `self.tk_vars` dictionary with *tk.IntVar* objects. 277 278 Initializes each *tk.IntVar* with the corresponding duration value 279 loaded from the provided numpy arrays (`side_one`, `side_two`). These 280 variables are then linked to the Entry widgets in the UI. 281 282 Parameters 283 ---------- 284 - **side_one** (*np.ndarray*): Numpy array of np.int32 durations for the first side of valves. 285 - **side_two** (*np.ndarray*): Numpy array of np.int32 durations for the second side of valves. 286 """ 287 288 for i, duration in enumerate(side_one): 289 self.tk_vars[f"Valve {i + 1}"] = tk.IntVar(value=duration) 290 for i, duration in enumerate(side_two): 291 self.tk_vars[f"Valve {i + 9}"] = tk.IntVar(value=duration) 292 293 def write_timing_changes(self): 294 """ 295 Archives the current 'selected' timings, validates the user-entered 296 timings, and saves the new timings as the 'selected' profile. 297 298 Retrieves values from the Entry widgets via their linked *tk.IntVar*s. 299 Performs basic validation (checks for empty fields). If valid, it 300 instructs `models.arduino_data` to archive the old 'selected' profile and 301 save the new profile as 'selected'. Displays error messages on failure. 302 303 Raises 304 ------ 305 - Propagates exceptions from `arduino_data.load_durations()` or 306 `arduino_data.save_durations()`. 307 """ 308 side_one, side_two, _ = self.arduino_data.load_durations() 309 310 ## save durations in the oldest valve duration archival location 311 self.arduino_data.save_durations(side_one, side_two, "archive") 312 313 timings = [GUIUtils.safe_tkinter_get(value) for value in self.tk_vars.values()] 314 315 # check if one of the fields are empty 316 if None in timings: 317 msg = "ONE OR MORE FIELDS ARE EMPTY, FILL BEFORE TRYING ANOTHER ADJUSTMENT" 318 logger.error(msg) 319 GUIUtils.display_error("FIELD ERROR", msg) 320 return 321 322 # using total possible valves per side here (maximum of 16 valves) for future proofing 323 side_one_new = np.full(8, np.int32(0)) 324 side_two_new = np.full(8, np.int32(0)) 325 326 for i in range(len(timings) // 2): 327 side_one_new[i] = timings[i] 328 side_two_new[i] = timings[i + 8] 329 330 # save new durations in the selected slot of the configuration file 331 self.arduino_data.save_durations(side_one_new, side_two_new, "selected")
43class ManualTimeAdjustment(tk.Toplevel): 44 """ 45 Implements a Tkinter Toplevel window for manually adjusting valve open durations. 46 47 This window allows users to: 48 - Select and load different valve timing profiles (Default, Last Used, Archives). 49 - View and edit the open duration (in microseconds) for each valve. 50 - Save the modified timings, which automatically archives the previously 51 'selected' timings and sets the new ones as 'selected'. 52 53 It interacts with `models.arduino_data` object (passed during initialization) 54 to load and persist timing configurations. Uses `views.gui_common.GUIUtils` for standard 55 widget creation and layout. 56 57 Attributes 58 ---------- 59 - **arduino_data** (*ArduinoData*): This is a reference to the program instance of `models.arduino_data` ArduinoData. It allows access to this 60 class and its methods which allows for the loading of data stored there such as valve duration times and schedule indicies. 61 - **`tk_vars`** (*dict[str, tk.IntVar]*): Dictionary mapping valve names (e.g., "Valve 1") to Tkinter IntVars holding their durations. 62 - **`labelled_entries`** (*list[tuple OR None]*): List storing tuples of (Frame, Label, Entry) widgets for each valve timing input. Stored to make possible later 63 modification of the widgets. 64 - **`dropdown`** (*ttk.Combobox*): Widget for selecting the duration profile to load/view. 65 - **`date_used_label`** (*tk.Label*): Label displaying the timestamp of the currently loaded profile. 66 - **`timing_frame`** (*tk.Frame*): Frame containing the grid of labeled entries for valve timings. 67 - **`save_changes_bttn`** (*tk.Button*): Button to trigger the save operation. 68 - **`duration_types`** (*dict[str, str]*): Maps user friendly display names in the dropdown to internal keys used for loading profiles. 69 70 Methods 71 ------- 72 - `show`() 73 Updates the interface with the latest data and makes the window visible. 74 - `update_interface`(...) 75 Callback for dropdown selection; loads and displays the chosen timing profile. 76 - `create_dropdown`() 77 Creates and configures the Combobox for selecting duration profiles. 78 - `create_interface`(...) 79 Constructs the main GUI layout, including labels, dropdown, entry fields, and buttons. 80 - `fill_tk_vars`(...) 81 Populates `self.tk_vars` with Tkinter variables based on loaded duration arrays. 82 - `write_timing_changes`() 83 Handles archiving old timings, validating user input, and saving new timings in 'selected' profile. 84 """ 85 86 def __init__(self, arduino_data: ArduinoData): 87 """ 88 Initializes the ManualTimeAdjustment Toplevel window. 89 90 Parameters 91 ---------- 92 - **arduino_data** (*ArduinoData*): Reference to program instance of `models.arduino_data.ArduinoData`, used for loading and saving valve durations. 93 94 Raises 95 ------ 96 - Propagates exceptions from `GUIUtils` methods during icon setting. 97 - Propagates exceptions from `arduino_data.load_durations()`. 98 """ 99 super().__init__() 100 101 self.arduino_data = arduino_data 102 103 self.title("Manual Valve Timing Adjustment") 104 self.bind("<Control-w>", lambda event: self.withdraw()) 105 106 self.resizable(False, False) 107 108 self.tk_vars: dict[str, tk.IntVar] = {} 109 self.labelled_entries: list[tuple | None] = [None] * TOTAL_VALVES 110 111 # load default 'selected' dur profile 112 side_one, side_two, date_used = self.arduino_data.load_durations() 113 114 self.fill_tk_vars(side_one, side_two) 115 self.create_interface(date_used) 116 117 window_icon_path = GUIUtils.get_window_icon_path() 118 GUIUtils.set_program_icon(self, icon_path=window_icon_path) 119 120 def show(self): 121 """ 122 Updates the interface with either default 'Last Used' profile data or selected profile from dropdown and makes the window visible. 123 """ 124 self.update_interface(event=None) 125 self.deiconify() 126 127 def update_interface(self, event: tk.Event | None): 128 """ 129 Loads and displays the valve timings based on the selected profile from the dropdown. 130 131 If `event` is None (e.g., called from `show`), it loads the 'Last Used' 132 profile. Otherwise, it uses the profile selected in the dropdown. Updates 133 the *tk.IntVar* variables, which refreshes the Entry widgets, and updates 134 the 'date last used' label. 135 136 Parameters 137 ---------- 138 - **event** (*tk.Event | None*): The Combobox selection event, or None to default 139 to loading the 'Last Used' profile. 140 141 Raises 142 ------ 143 - Propagates exceptions from `arduino_data.load_durations()`. 144 """ 145 146 # event is the selection event of the self.combobox. using .get gets the self.duration_type dict key 147 load_durations_key = None 148 if event is None: 149 load_durations_key = self.duration_types["Last Used"] 150 else: 151 load_durations_key = self.duration_types[f"{event.widget.get()}"] 152 153 side_one, side_two, _ = self.arduino_data.load_durations(load_durations_key) 154 155 # set default durations for side one 156 for i, duration in enumerate(side_one): 157 self.tk_vars[f"Valve {i + 1}"].set(duration) 158 # set default durations for side two 159 for i, duration in enumerate(side_two): 160 self.tk_vars[f"Valve {i + 8 + 1}"].set(duration) 161 162 def create_dropdown(self): 163 """ 164 Creates the `ttk.Combobox` widget for selecting duration profiles. 165 166 Populates the dropdown with user-friendly names for duration profiles and maps them to keys in 167 `self.duration_types` dict, sets the default selection to 'Last Used', 168 and binds the selection event to `self.update_interface`. 169 """ 170 self.duration_types = { 171 "Default": "default_durations", 172 "Last Used": "selected_durations", 173 "(most recent) Archive 1": "archive_1", 174 "Archive 2": "archive_2", 175 "(oldest) Archive 3": "archive_3", 176 } 177 self.dropdown = ttk.Combobox(self, values=list(self.duration_types.keys())) 178 self.dropdown.current(1) 179 self.dropdown.grid(row=1, column=0, sticky="e", padx=10) 180 self.dropdown.bind("<<ComboboxSelected>>", lambda e: self.update_interface(e)) 181 182 def create_interface(self, date_used: datetime.datetime): 183 """ 184 Constructs the main GUI elements within the Toplevel window. 185 186 Creates labels, the duration profile dropdown, the grid of labeled 187 entries for valve timings, and the save button. Uses `views.gui_common.GUIUtils` for 188 widget creation. 189 190 Parameters 191 ---------- 192 - **date_used** (*datetime.datetime*): The timestamp for date selected profile was created, 193 used to populate the date label. 194 195 Raises 196 ------ 197 - Propagates exceptions from `GUIUtils` methods during widget creation. 198 """ 199 warning = tk.Label( 200 self, 201 text="ALL TIMINGS IN MICROSECONDS (e.g 24125 equivalent to 24.125 ms)", 202 bg="white", 203 fg="black", 204 font=("Helvetica", 15), 205 highlightthickness=1, 206 highlightbackground="black", 207 ) 208 209 warning.grid(row=0, column=0, sticky="nsew", pady=10, padx=10) 210 211 self.create_dropdown() 212 213 formatted_date = date_used.strftime("%B/%d/%Y") 214 formatted_time = date_used.strftime("%I:%M %p") 215 self.date_used_label = tk.Label( 216 self, 217 text=f"Timing profile created on: {formatted_date} at {formatted_time}", 218 bg="white", 219 fg="black", 220 font=("Helvetica", 15), 221 highlightthickness=1, 222 highlightbackground="black", 223 ) 224 self.date_used_label.grid(row=1, column=0, sticky="w", padx=10) 225 226 self.timing_frame = tk.Frame( 227 self, highlightbackground="black", highlightthickness=1 228 ) 229 230 self.timing_frame.grid(row=2, column=0, pady=10, padx=10, sticky="nsew") 231 self.timing_frame.grid_columnconfigure(0, weight=1) 232 233 for i in range(VALVES_PER_SIDE): 234 # GUIUtils.create_button returns both the buttons frame and button object in a tuple. using [1] on the 235 # return item yields the button object. 236 237 self.labelled_entries[i] = GUIUtils.create_labeled_entry( 238 self.timing_frame, 239 f"Valve {i + 1} Open Time", 240 self.tk_vars[f"Valve {i + 1}"], 241 i, 242 0, 243 ) 244 245 self.labelled_entries[i] = GUIUtils.create_labeled_entry( 246 self.timing_frame, 247 f"Valve {i + (VALVES_PER_SIDE) + 1} Open Time", 248 self.tk_vars[f"Valve {i + (TOTAL_VALVES) + 1}"], 249 i, 250 1, 251 ) 252 253 warning = tk.Label( 254 self, 255 text="This action will archive the current timing configuration into assets/valve_durations.toml\n and set these new timings to selected timings", 256 bg="white", 257 fg="black", 258 font=("Helvetica", 15), 259 highlightthickness=1, 260 highlightbackground="black", 261 ) 262 263 warning.grid(row=3, column=0, padx=10) 264 self.save_changes_bttn = GUIUtils.create_button( 265 self, 266 "Write Timing Changes", 267 lambda: self.write_timing_changes(), 268 bg="green", 269 row=4, 270 column=0, 271 )[1] 272 273 def fill_tk_vars( 274 self, side_one: npt.NDArray[np.int32], side_two: npt.NDArray[np.int32] 275 ): 276 """ 277 Populates the `self.tk_vars` dictionary with *tk.IntVar* objects. 278 279 Initializes each *tk.IntVar* with the corresponding duration value 280 loaded from the provided numpy arrays (`side_one`, `side_two`). These 281 variables are then linked to the Entry widgets in the UI. 282 283 Parameters 284 ---------- 285 - **side_one** (*np.ndarray*): Numpy array of np.int32 durations for the first side of valves. 286 - **side_two** (*np.ndarray*): Numpy array of np.int32 durations for the second side of valves. 287 """ 288 289 for i, duration in enumerate(side_one): 290 self.tk_vars[f"Valve {i + 1}"] = tk.IntVar(value=duration) 291 for i, duration in enumerate(side_two): 292 self.tk_vars[f"Valve {i + 9}"] = tk.IntVar(value=duration) 293 294 def write_timing_changes(self): 295 """ 296 Archives the current 'selected' timings, validates the user-entered 297 timings, and saves the new timings as the 'selected' profile. 298 299 Retrieves values from the Entry widgets via their linked *tk.IntVar*s. 300 Performs basic validation (checks for empty fields). If valid, it 301 instructs `models.arduino_data` to archive the old 'selected' profile and 302 save the new profile as 'selected'. Displays error messages on failure. 303 304 Raises 305 ------ 306 - Propagates exceptions from `arduino_data.load_durations()` or 307 `arduino_data.save_durations()`. 308 """ 309 side_one, side_two, _ = self.arduino_data.load_durations() 310 311 ## save durations in the oldest valve duration archival location 312 self.arduino_data.save_durations(side_one, side_two, "archive") 313 314 timings = [GUIUtils.safe_tkinter_get(value) for value in self.tk_vars.values()] 315 316 # check if one of the fields are empty 317 if None in timings: 318 msg = "ONE OR MORE FIELDS ARE EMPTY, FILL BEFORE TRYING ANOTHER ADJUSTMENT" 319 logger.error(msg) 320 GUIUtils.display_error("FIELD ERROR", msg) 321 return 322 323 # using total possible valves per side here (maximum of 16 valves) for future proofing 324 side_one_new = np.full(8, np.int32(0)) 325 side_two_new = np.full(8, np.int32(0)) 326 327 for i in range(len(timings) // 2): 328 side_one_new[i] = timings[i] 329 side_two_new[i] = timings[i + 8] 330 331 # save new durations in the selected slot of the configuration file 332 self.arduino_data.save_durations(side_one_new, side_two_new, "selected")
Implements a Tkinter Toplevel window for manually adjusting valve open durations.
This window allows users to:
- Select and load different valve timing profiles (Default, Last Used, Archives).
- View and edit the open duration (in microseconds) for each valve.
- Save the modified timings, which automatically archives the previously 'selected' timings and sets the new ones as 'selected'.
It interacts with models.arduino_data
object (passed during initialization)
to load and persist timing configurations. Uses views.gui_common.GUIUtils
for standard
widget creation and layout.
Attributes
- arduino_data (ArduinoData): This is a reference to the program instance of
models.arduino_data
ArduinoData. It allows access to this class and its methods which allows for the loading of data stored there such as valve duration times and schedule indicies. tk_vars
(dict[str, tk.IntVar]): Dictionary mapping valve names (e.g., "Valve 1") to Tkinter IntVars holding their durations.labelled_entries
(list[tuple OR None]): List storing tuples of (Frame, Label, Entry) widgets for each valve timing input. Stored to make possible later modification of the widgets.dropdown
(ttk.Combobox): Widget for selecting the duration profile to load/view.date_used_label
(tk.Label): Label displaying the timestamp of the currently loaded profile.timing_frame
(tk.Frame): Frame containing the grid of labeled entries for valve timings.save_changes_bttn
(tk.Button): Button to trigger the save operation.duration_types
(dict[str, str]): Maps user friendly display names in the dropdown to internal keys used for loading profiles.
Methods
show
() Updates the interface with the latest data and makes the window visible.update_interface
(...) Callback for dropdown selection; loads and displays the chosen timing profile.create_dropdown
() Creates and configures the Combobox for selecting duration profiles.create_interface
(...) Constructs the main GUI layout, including labels, dropdown, entry fields, and buttons.fill_tk_vars
(...) Populatesself.tk_vars
with Tkinter variables based on loaded duration arrays.write_timing_changes
() Handles archiving old timings, validating user input, and saving new timings in 'selected' profile.
86 def __init__(self, arduino_data: ArduinoData): 87 """ 88 Initializes the ManualTimeAdjustment Toplevel window. 89 90 Parameters 91 ---------- 92 - **arduino_data** (*ArduinoData*): Reference to program instance of `models.arduino_data.ArduinoData`, used for loading and saving valve durations. 93 94 Raises 95 ------ 96 - Propagates exceptions from `GUIUtils` methods during icon setting. 97 - Propagates exceptions from `arduino_data.load_durations()`. 98 """ 99 super().__init__() 100 101 self.arduino_data = arduino_data 102 103 self.title("Manual Valve Timing Adjustment") 104 self.bind("<Control-w>", lambda event: self.withdraw()) 105 106 self.resizable(False, False) 107 108 self.tk_vars: dict[str, tk.IntVar] = {} 109 self.labelled_entries: list[tuple | None] = [None] * TOTAL_VALVES 110 111 # load default 'selected' dur profile 112 side_one, side_two, date_used = self.arduino_data.load_durations() 113 114 self.fill_tk_vars(side_one, side_two) 115 self.create_interface(date_used) 116 117 window_icon_path = GUIUtils.get_window_icon_path() 118 GUIUtils.set_program_icon(self, icon_path=window_icon_path)
Initializes the ManualTimeAdjustment Toplevel window.
Parameters
- arduino_data (ArduinoData): Reference to program instance of
models.arduino_data.ArduinoData
, used for loading and saving valve durations.
Raises
- Propagates exceptions from
GUIUtils
methods during icon setting. - Propagates exceptions from
arduino_data.load_durations()
.
120 def show(self): 121 """ 122 Updates the interface with either default 'Last Used' profile data or selected profile from dropdown and makes the window visible. 123 """ 124 self.update_interface(event=None) 125 self.deiconify()
Updates the interface with either default 'Last Used' profile data or selected profile from dropdown and makes the window visible.
127 def update_interface(self, event: tk.Event | None): 128 """ 129 Loads and displays the valve timings based on the selected profile from the dropdown. 130 131 If `event` is None (e.g., called from `show`), it loads the 'Last Used' 132 profile. Otherwise, it uses the profile selected in the dropdown. Updates 133 the *tk.IntVar* variables, which refreshes the Entry widgets, and updates 134 the 'date last used' label. 135 136 Parameters 137 ---------- 138 - **event** (*tk.Event | None*): The Combobox selection event, or None to default 139 to loading the 'Last Used' profile. 140 141 Raises 142 ------ 143 - Propagates exceptions from `arduino_data.load_durations()`. 144 """ 145 146 # event is the selection event of the self.combobox. using .get gets the self.duration_type dict key 147 load_durations_key = None 148 if event is None: 149 load_durations_key = self.duration_types["Last Used"] 150 else: 151 load_durations_key = self.duration_types[f"{event.widget.get()}"] 152 153 side_one, side_two, _ = self.arduino_data.load_durations(load_durations_key) 154 155 # set default durations for side one 156 for i, duration in enumerate(side_one): 157 self.tk_vars[f"Valve {i + 1}"].set(duration) 158 # set default durations for side two 159 for i, duration in enumerate(side_two): 160 self.tk_vars[f"Valve {i + 8 + 1}"].set(duration)
Loads and displays the valve timings based on the selected profile from the dropdown.
If event
is None (e.g., called from show
), it loads the 'Last Used'
profile. Otherwise, it uses the profile selected in the dropdown. Updates
the tk.IntVar variables, which refreshes the Entry widgets, and updates
the 'date last used' label.
Parameters
- event (tk.Event | None): The Combobox selection event, or None to default to loading the 'Last Used' profile.
Raises
- Propagates exceptions from
arduino_data.load_durations()
.
162 def create_dropdown(self): 163 """ 164 Creates the `ttk.Combobox` widget for selecting duration profiles. 165 166 Populates the dropdown with user-friendly names for duration profiles and maps them to keys in 167 `self.duration_types` dict, sets the default selection to 'Last Used', 168 and binds the selection event to `self.update_interface`. 169 """ 170 self.duration_types = { 171 "Default": "default_durations", 172 "Last Used": "selected_durations", 173 "(most recent) Archive 1": "archive_1", 174 "Archive 2": "archive_2", 175 "(oldest) Archive 3": "archive_3", 176 } 177 self.dropdown = ttk.Combobox(self, values=list(self.duration_types.keys())) 178 self.dropdown.current(1) 179 self.dropdown.grid(row=1, column=0, sticky="e", padx=10) 180 self.dropdown.bind("<<ComboboxSelected>>", lambda e: self.update_interface(e))
Creates the ttk.Combobox
widget for selecting duration profiles.
Populates the dropdown with user-friendly names for duration profiles and maps them to keys in
self.duration_types
dict, sets the default selection to 'Last Used',
and binds the selection event to self.update_interface
.
182 def create_interface(self, date_used: datetime.datetime): 183 """ 184 Constructs the main GUI elements within the Toplevel window. 185 186 Creates labels, the duration profile dropdown, the grid of labeled 187 entries for valve timings, and the save button. Uses `views.gui_common.GUIUtils` for 188 widget creation. 189 190 Parameters 191 ---------- 192 - **date_used** (*datetime.datetime*): The timestamp for date selected profile was created, 193 used to populate the date label. 194 195 Raises 196 ------ 197 - Propagates exceptions from `GUIUtils` methods during widget creation. 198 """ 199 warning = tk.Label( 200 self, 201 text="ALL TIMINGS IN MICROSECONDS (e.g 24125 equivalent to 24.125 ms)", 202 bg="white", 203 fg="black", 204 font=("Helvetica", 15), 205 highlightthickness=1, 206 highlightbackground="black", 207 ) 208 209 warning.grid(row=0, column=0, sticky="nsew", pady=10, padx=10) 210 211 self.create_dropdown() 212 213 formatted_date = date_used.strftime("%B/%d/%Y") 214 formatted_time = date_used.strftime("%I:%M %p") 215 self.date_used_label = tk.Label( 216 self, 217 text=f"Timing profile created on: {formatted_date} at {formatted_time}", 218 bg="white", 219 fg="black", 220 font=("Helvetica", 15), 221 highlightthickness=1, 222 highlightbackground="black", 223 ) 224 self.date_used_label.grid(row=1, column=0, sticky="w", padx=10) 225 226 self.timing_frame = tk.Frame( 227 self, highlightbackground="black", highlightthickness=1 228 ) 229 230 self.timing_frame.grid(row=2, column=0, pady=10, padx=10, sticky="nsew") 231 self.timing_frame.grid_columnconfigure(0, weight=1) 232 233 for i in range(VALVES_PER_SIDE): 234 # GUIUtils.create_button returns both the buttons frame and button object in a tuple. using [1] on the 235 # return item yields the button object. 236 237 self.labelled_entries[i] = GUIUtils.create_labeled_entry( 238 self.timing_frame, 239 f"Valve {i + 1} Open Time", 240 self.tk_vars[f"Valve {i + 1}"], 241 i, 242 0, 243 ) 244 245 self.labelled_entries[i] = GUIUtils.create_labeled_entry( 246 self.timing_frame, 247 f"Valve {i + (VALVES_PER_SIDE) + 1} Open Time", 248 self.tk_vars[f"Valve {i + (TOTAL_VALVES) + 1}"], 249 i, 250 1, 251 ) 252 253 warning = tk.Label( 254 self, 255 text="This action will archive the current timing configuration into assets/valve_durations.toml\n and set these new timings to selected timings", 256 bg="white", 257 fg="black", 258 font=("Helvetica", 15), 259 highlightthickness=1, 260 highlightbackground="black", 261 ) 262 263 warning.grid(row=3, column=0, padx=10) 264 self.save_changes_bttn = GUIUtils.create_button( 265 self, 266 "Write Timing Changes", 267 lambda: self.write_timing_changes(), 268 bg="green", 269 row=4, 270 column=0, 271 )[1]
Constructs the main GUI elements within the Toplevel window.
Creates labels, the duration profile dropdown, the grid of labeled
entries for valve timings, and the save button. Uses views.gui_common.GUIUtils
for
widget creation.
Parameters
- date_used (datetime.datetime): The timestamp for date selected profile was created, used to populate the date label.
Raises
- Propagates exceptions from
GUIUtils
methods during widget creation.
273 def fill_tk_vars( 274 self, side_one: npt.NDArray[np.int32], side_two: npt.NDArray[np.int32] 275 ): 276 """ 277 Populates the `self.tk_vars` dictionary with *tk.IntVar* objects. 278 279 Initializes each *tk.IntVar* with the corresponding duration value 280 loaded from the provided numpy arrays (`side_one`, `side_two`). These 281 variables are then linked to the Entry widgets in the UI. 282 283 Parameters 284 ---------- 285 - **side_one** (*np.ndarray*): Numpy array of np.int32 durations for the first side of valves. 286 - **side_two** (*np.ndarray*): Numpy array of np.int32 durations for the second side of valves. 287 """ 288 289 for i, duration in enumerate(side_one): 290 self.tk_vars[f"Valve {i + 1}"] = tk.IntVar(value=duration) 291 for i, duration in enumerate(side_two): 292 self.tk_vars[f"Valve {i + 9}"] = tk.IntVar(value=duration)
Populates the self.tk_vars
dictionary with tk.IntVar objects.
Initializes each tk.IntVar with the corresponding duration value
loaded from the provided numpy arrays (side_one
, side_two
). These
variables are then linked to the Entry widgets in the UI.
Parameters
- side_one (np.ndarray): Numpy array of np.int32 durations for the first side of valves.
- side_two (np.ndarray): Numpy array of np.int32 durations for the second side of valves.
294 def write_timing_changes(self): 295 """ 296 Archives the current 'selected' timings, validates the user-entered 297 timings, and saves the new timings as the 'selected' profile. 298 299 Retrieves values from the Entry widgets via their linked *tk.IntVar*s. 300 Performs basic validation (checks for empty fields). If valid, it 301 instructs `models.arduino_data` to archive the old 'selected' profile and 302 save the new profile as 'selected'. Displays error messages on failure. 303 304 Raises 305 ------ 306 - Propagates exceptions from `arduino_data.load_durations()` or 307 `arduino_data.save_durations()`. 308 """ 309 side_one, side_two, _ = self.arduino_data.load_durations() 310 311 ## save durations in the oldest valve duration archival location 312 self.arduino_data.save_durations(side_one, side_two, "archive") 313 314 timings = [GUIUtils.safe_tkinter_get(value) for value in self.tk_vars.values()] 315 316 # check if one of the fields are empty 317 if None in timings: 318 msg = "ONE OR MORE FIELDS ARE EMPTY, FILL BEFORE TRYING ANOTHER ADJUSTMENT" 319 logger.error(msg) 320 GUIUtils.display_error("FIELD ERROR", msg) 321 return 322 323 # using total possible valves per side here (maximum of 16 valves) for future proofing 324 side_one_new = np.full(8, np.int32(0)) 325 side_two_new = np.full(8, np.int32(0)) 326 327 for i in range(len(timings) // 2): 328 side_one_new[i] = timings[i] 329 side_two_new[i] = timings[i + 8] 330 331 # save new durations in the selected slot of the configuration file 332 self.arduino_data.save_durations(side_one_new, side_two_new, "selected")
Archives the current 'selected' timings, validates the user-entered timings, and saves the new timings as the 'selected' profile.
Retrieves values from the Entry widgets via their linked tk.IntVars.
Performs basic validation (checks for empty fields). If valid, it
instructs models.arduino_data
to archive the old 'selected' profile and
save the new profile as 'selected'. Displays error messages on failure.
Raises
- Propagates exceptions from
arduino_data.load_durations()
orarduino_data.save_durations()
.