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")
logger = <RootLogger root (INFO)>
rig_config = '/home/blake/Documents/Photologic-Experiment-Rig-Files/assets/rig_config.toml'
TOTAL_VALVES = 8
VALVES_PER_SIDE = 4
class ManualTimeAdjustment(tkinter.Toplevel):
 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(...) Populates self.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.
ManualTimeAdjustment(arduino_data: models.arduino_data.ArduinoData)
 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().
arduino_data
tk_vars: dict[str, tkinter.IntVar]
labelled_entries: list[tuple | None]
def show(self):
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.

def update_interface(self, event: tkinter.Event | None):
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().
def create_dropdown(self):
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.

def create_interface(self, date_used: datetime.datetime):
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.
def fill_tk_vars( self, side_one: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.int32]], side_two: numpy.ndarray[tuple[int, ...], numpy.dtype[numpy.int32]]):
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.
def write_timing_changes(self):
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() or arduino_data.save_durations().