valve_control_window

This module defines the ValveControlWindow class, a Toplevel window in the GUI designed for direct manual control of individual valves and the door motor on the experimental rig.

It reads valve configuration (number of valves) from a TOML file, creates buttons for each valve, allows toggling their state (Open/Closed), and provides a button to move the motor (Up/Down). Valve state changes and motor commands are sent to Arduino via controllers.arduino_control.ArduinoManager.

  1"""
  2This module defines the ValveControlWindow class, a Toplevel window in the GUI
  3designed for direct manual control of individual valves and the door motor
  4on the experimental rig.
  5
  6It reads valve configuration (number of valves) from a TOML file, creates buttons for each valve,
  7allows toggling their state (Open/Closed), and provides a button to move the motor
  8(Up/Down). Valve state changes and motor commands are sent to Arduino via `controllers.arduino_control.ArduinoManager`.
  9"""
 10
 11import tkinter as tk
 12import toml
 13import numpy as np
 14from controllers.arduino_control import ArduinoManager
 15import system_config
 16
 17from views.gui_common import GUIUtils
 18
 19rig_config = system_config.get_rig_config()
 20
 21with open(rig_config, "r") as f:
 22    VALVE_CONFIG = toml.load(f)["valve_config"]
 23
 24# pull total valves constant from toml config
 25TOTAL_CURRENT_VALVES = VALVE_CONFIG["TOTAL_CURRENT_VALVES"]
 26VALVES_PER_SIDE = TOTAL_CURRENT_VALVES // 2
 27
 28
 29class ValveControlWindow(tk.Toplevel):
 30    """
 31    A Toplevel window providing manual control over individual valves and the main motor.
 32
 33    This window displays buttons for each configured valve, allowing users to toggle them
 34    between 'Open' and 'Closed' states. It also includes a button to command the
 35    main motor to move 'Up' or 'Down'. State changes and commands are sent to the
 36    connected Arduino controller.
 37
 38    Attributes
 39    ----------
 40    - **arduino_controller** (*ArduinoManager*): An instance of the controller class
 41      responsible for communicating with the Arduino board.
 42    - **valve_selections** (*np.ndarray*): A NumPy array storing the current state (0 for Closed, 1 for Open)
 43      of each valve. The index corresponds to the valve number (0-indexed).
 44    - **buttons** (*List[tk.Button OR None]*): A list holding references to the `tk.Button` widgets
 45      for each valve, allowing individual access for state updates.
 46    - **motor_button** (*tk.Button*): The `tk.Button` widget used to control the motor's position.
 47    - **motor_down** (*bool*): A flag indicating the current assumed state of the motor
 48      (True if commanded Down, False if commanded Up).
 49    - **valves_frame** (*tk.Frame*): The main container frame within the Toplevel window holding the control buttons.
 50
 51    Methods
 52    -------
 53    - `show()`
 54        Makes the Valve Control window visible (`deiconify`).
 55    - `create_interface()`
 56        Sets up the main frame (`valves_frame`) and calls `create_buttons` to populate it.
 57    - `toggle_motor()`
 58        Callback for the motor control button. Toggles the `motor_down` state, updates the button
 59        text/color, and sends the corresponding "UP" or "DOWN" command to the Arduino.
 60    - `create_buttons()`
 61        Creates and arranges the `tk.Button` widgets for each valve based on
 62        `TOTAL_CURRENT_VALVES` and `VALVES_PER_SIDE`. Creates the motor control button.
 63    - `toggle_valve_button(valve_num)`
 64        Callback for individual valve buttons. Toggles the state of the specified valve
 65        in `valve_selections`, updates the button's text/color, and sends the "OPEN SPECIFIC"
 66        command followed by the current state of all valves (`valve_selections`) to the Arduino.
 67    """
 68
 69    def __init__(self, arduino_controller: ArduinoManager) -> None:
 70        super().__init__()
 71        self.title("Valve Control")
 72        self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw())
 73        self.bind("<Control-w>", lambda event: self.withdraw())
 74
 75        for i in range(3):
 76            self.grid_columnconfigure(i, weight=1)
 77        for i in range(2):
 78            self.grid_rowconfigure(i, weight=1)
 79
 80        self.resizable(False, False)
 81
 82        self.arduino_controller = arduino_controller
 83
 84        window_icon_path = GUIUtils.get_window_icon_path()
 85        GUIUtils.set_program_icon(self, window_icon_path)
 86
 87        self.valve_selections = np.zeros((TOTAL_CURRENT_VALVES,), dtype=np.int8)
 88
 89        self.create_interface()
 90        self.motor_down = False
 91
 92        # we don't want to show this window until the user decides to click the corresponding button,
 93        # withdraw (hide) it for now
 94        self.withdraw()
 95
 96    def show(self):
 97        """Makes the window visible by calling `deiconify`."""
 98        self.deiconify()
 99
100    def create_interface(self):
101        """Creates the main frame for holding the valve and motor buttons."""
102        self.valves_frame = tk.Frame(self)
103
104        self.valves_frame.grid(row=0, column=0, pady=10, sticky="nsew")
105        self.valves_frame.grid_columnconfigure(0, weight=1)
106        self.valves_frame.grid_rowconfigure(0, weight=1)
107
108        self.create_buttons()
109
110    def toggle_motor(self):
111        """
112        Toggles the motor's position between 'Up' and 'Down'.
113
114        Updates the motor control button's appearance (text and color) to reflect the
115        new target state and sends the appropriate command ("UP\n" or "DOWN\n")
116        to the Arduino controller. Also updates the internal `motor_down` flag.
117        """
118        # if motor is not down, command it down
119        if self.motor_down is False:
120            self.motor_button.configure(text="Move Motor Up", bg="blue", fg="white")
121
122            motor_command = "DOWN\n".encode("utf-8")
123            self.motor_down = True
124        else:
125            self.motor_button.configure(text="Move motor down", bg="green", fg="black")
126
127            motor_command = "UP\n".encode("utf-8")
128            self.motor_down = False
129
130        self.arduino_controller.send_command(command=motor_command)
131
132    def create_buttons(self):
133        """
134        Creates and arranges the buttons for valve and motor control.
135
136        Populates the `self.buttons` list with `tk.Button` widgets for each valve,
137        placing them in two columns ("Side One" and "Side Two"). Creates the
138        motor control button and places it in the center column.
139        """
140        # Create list of buttons to be able to access each one easily later
141        self.buttons = [None] * TOTAL_CURRENT_VALVES
142
143        # Side one valves (1-4)
144        for i in range(VALVES_PER_SIDE):  # This runs 0, 1, 2, 3
145            btn_text = f"Valve {i + 1}: Closed"
146            button = GUIUtils.create_button(
147                self.valves_frame,
148                btn_text,
149                lambda i=i: self.toggle_valve_button(i),
150                "firebrick1",
151                row=i,
152                column=0,
153            )[1]
154            self.buttons[i] = button
155
156            # This runs 4, 5, 6, 7
157            btn_text = f"Valve {i + VALVES_PER_SIDE + 1}: Closed"
158            button = GUIUtils.create_button(
159                self.valves_frame,
160                btn_text,
161                lambda i=i + VALVES_PER_SIDE: self.toggle_valve_button(i),
162                "firebrick1",
163                row=i,
164                column=2,
165            )[1]
166            self.buttons[i + VALVES_PER_SIDE] = button
167
168        self.motor_button = GUIUtils.create_button(
169            self.valves_frame,
170            "Move Motor Down",
171            command=lambda: self.toggle_motor(),
172            bg="green",
173            row=TOTAL_CURRENT_VALVES,
174            column=1,
175        )[1]
176
177    def toggle_valve_button(self, valve_num: int):
178        """
179        Updates the state of the clicked valve (`valve_num`) in the `valve_selections` array.
180        Changes the button's text and background color to reflect the new state (Open/Green or Closed/Red).
181        Sends an "OPEN SPECIFIC" command to the Arduino, followed by the byte representation
182        of the entire `valve_selections` array, instructing the Arduino which valves should be open or closed.
183
184        Parameters
185        ----------
186        - **valve_num** (*int*): The 0-based index of the valve whose button was clicked.
187        """
188        # if a valve is currently a zero (off), make it a 1, and turn the button green
189        if self.valve_selections[valve_num] == 0:
190            self.buttons[valve_num].configure(
191                text=f"Valve {valve_num + 1}: Open", bg="green"
192            )
193            self.valve_selections[valve_num] = 1
194
195        # if a valve is currently a one (on), make it a zero (off), and turn the button red
196        else:
197            self.buttons[valve_num].configure(
198                text=f"Valve {valve_num + 1}: Closed", bg="firebrick1"
199            )
200            self.valve_selections[valve_num] = 0
201
202        open_command = "OPEN SPECIFIC\n".encode("utf-8")
203        self.arduino_controller.send_command(command=open_command)
204
205        # send desired valve states to the arduino
206        selections = self.valve_selections.tobytes()
207        self.arduino_controller.send_command(command=selections)
rig_config = '/home/blake/Documents/Photologic-Experiment-Rig-Files/assets/rig_config.toml'
TOTAL_CURRENT_VALVES = 8
VALVES_PER_SIDE = 4
class ValveControlWindow(tkinter.Toplevel):
 30class ValveControlWindow(tk.Toplevel):
 31    """
 32    A Toplevel window providing manual control over individual valves and the main motor.
 33
 34    This window displays buttons for each configured valve, allowing users to toggle them
 35    between 'Open' and 'Closed' states. It also includes a button to command the
 36    main motor to move 'Up' or 'Down'. State changes and commands are sent to the
 37    connected Arduino controller.
 38
 39    Attributes
 40    ----------
 41    - **arduino_controller** (*ArduinoManager*): An instance of the controller class
 42      responsible for communicating with the Arduino board.
 43    - **valve_selections** (*np.ndarray*): A NumPy array storing the current state (0 for Closed, 1 for Open)
 44      of each valve. The index corresponds to the valve number (0-indexed).
 45    - **buttons** (*List[tk.Button OR None]*): A list holding references to the `tk.Button` widgets
 46      for each valve, allowing individual access for state updates.
 47    - **motor_button** (*tk.Button*): The `tk.Button` widget used to control the motor's position.
 48    - **motor_down** (*bool*): A flag indicating the current assumed state of the motor
 49      (True if commanded Down, False if commanded Up).
 50    - **valves_frame** (*tk.Frame*): The main container frame within the Toplevel window holding the control buttons.
 51
 52    Methods
 53    -------
 54    - `show()`
 55        Makes the Valve Control window visible (`deiconify`).
 56    - `create_interface()`
 57        Sets up the main frame (`valves_frame`) and calls `create_buttons` to populate it.
 58    - `toggle_motor()`
 59        Callback for the motor control button. Toggles the `motor_down` state, updates the button
 60        text/color, and sends the corresponding "UP" or "DOWN" command to the Arduino.
 61    - `create_buttons()`
 62        Creates and arranges the `tk.Button` widgets for each valve based on
 63        `TOTAL_CURRENT_VALVES` and `VALVES_PER_SIDE`. Creates the motor control button.
 64    - `toggle_valve_button(valve_num)`
 65        Callback for individual valve buttons. Toggles the state of the specified valve
 66        in `valve_selections`, updates the button's text/color, and sends the "OPEN SPECIFIC"
 67        command followed by the current state of all valves (`valve_selections`) to the Arduino.
 68    """
 69
 70    def __init__(self, arduino_controller: ArduinoManager) -> None:
 71        super().__init__()
 72        self.title("Valve Control")
 73        self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw())
 74        self.bind("<Control-w>", lambda event: self.withdraw())
 75
 76        for i in range(3):
 77            self.grid_columnconfigure(i, weight=1)
 78        for i in range(2):
 79            self.grid_rowconfigure(i, weight=1)
 80
 81        self.resizable(False, False)
 82
 83        self.arduino_controller = arduino_controller
 84
 85        window_icon_path = GUIUtils.get_window_icon_path()
 86        GUIUtils.set_program_icon(self, window_icon_path)
 87
 88        self.valve_selections = np.zeros((TOTAL_CURRENT_VALVES,), dtype=np.int8)
 89
 90        self.create_interface()
 91        self.motor_down = False
 92
 93        # we don't want to show this window until the user decides to click the corresponding button,
 94        # withdraw (hide) it for now
 95        self.withdraw()
 96
 97    def show(self):
 98        """Makes the window visible by calling `deiconify`."""
 99        self.deiconify()
100
101    def create_interface(self):
102        """Creates the main frame for holding the valve and motor buttons."""
103        self.valves_frame = tk.Frame(self)
104
105        self.valves_frame.grid(row=0, column=0, pady=10, sticky="nsew")
106        self.valves_frame.grid_columnconfigure(0, weight=1)
107        self.valves_frame.grid_rowconfigure(0, weight=1)
108
109        self.create_buttons()
110
111    def toggle_motor(self):
112        """
113        Toggles the motor's position between 'Up' and 'Down'.
114
115        Updates the motor control button's appearance (text and color) to reflect the
116        new target state and sends the appropriate command ("UP\n" or "DOWN\n")
117        to the Arduino controller. Also updates the internal `motor_down` flag.
118        """
119        # if motor is not down, command it down
120        if self.motor_down is False:
121            self.motor_button.configure(text="Move Motor Up", bg="blue", fg="white")
122
123            motor_command = "DOWN\n".encode("utf-8")
124            self.motor_down = True
125        else:
126            self.motor_button.configure(text="Move motor down", bg="green", fg="black")
127
128            motor_command = "UP\n".encode("utf-8")
129            self.motor_down = False
130
131        self.arduino_controller.send_command(command=motor_command)
132
133    def create_buttons(self):
134        """
135        Creates and arranges the buttons for valve and motor control.
136
137        Populates the `self.buttons` list with `tk.Button` widgets for each valve,
138        placing them in two columns ("Side One" and "Side Two"). Creates the
139        motor control button and places it in the center column.
140        """
141        # Create list of buttons to be able to access each one easily later
142        self.buttons = [None] * TOTAL_CURRENT_VALVES
143
144        # Side one valves (1-4)
145        for i in range(VALVES_PER_SIDE):  # This runs 0, 1, 2, 3
146            btn_text = f"Valve {i + 1}: Closed"
147            button = GUIUtils.create_button(
148                self.valves_frame,
149                btn_text,
150                lambda i=i: self.toggle_valve_button(i),
151                "firebrick1",
152                row=i,
153                column=0,
154            )[1]
155            self.buttons[i] = button
156
157            # This runs 4, 5, 6, 7
158            btn_text = f"Valve {i + VALVES_PER_SIDE + 1}: Closed"
159            button = GUIUtils.create_button(
160                self.valves_frame,
161                btn_text,
162                lambda i=i + VALVES_PER_SIDE: self.toggle_valve_button(i),
163                "firebrick1",
164                row=i,
165                column=2,
166            )[1]
167            self.buttons[i + VALVES_PER_SIDE] = button
168
169        self.motor_button = GUIUtils.create_button(
170            self.valves_frame,
171            "Move Motor Down",
172            command=lambda: self.toggle_motor(),
173            bg="green",
174            row=TOTAL_CURRENT_VALVES,
175            column=1,
176        )[1]
177
178    def toggle_valve_button(self, valve_num: int):
179        """
180        Updates the state of the clicked valve (`valve_num`) in the `valve_selections` array.
181        Changes the button's text and background color to reflect the new state (Open/Green or Closed/Red).
182        Sends an "OPEN SPECIFIC" command to the Arduino, followed by the byte representation
183        of the entire `valve_selections` array, instructing the Arduino which valves should be open or closed.
184
185        Parameters
186        ----------
187        - **valve_num** (*int*): The 0-based index of the valve whose button was clicked.
188        """
189        # if a valve is currently a zero (off), make it a 1, and turn the button green
190        if self.valve_selections[valve_num] == 0:
191            self.buttons[valve_num].configure(
192                text=f"Valve {valve_num + 1}: Open", bg="green"
193            )
194            self.valve_selections[valve_num] = 1
195
196        # if a valve is currently a one (on), make it a zero (off), and turn the button red
197        else:
198            self.buttons[valve_num].configure(
199                text=f"Valve {valve_num + 1}: Closed", bg="firebrick1"
200            )
201            self.valve_selections[valve_num] = 0
202
203        open_command = "OPEN SPECIFIC\n".encode("utf-8")
204        self.arduino_controller.send_command(command=open_command)
205
206        # send desired valve states to the arduino
207        selections = self.valve_selections.tobytes()
208        self.arduino_controller.send_command(command=selections)

A Toplevel window providing manual control over individual valves and the main motor.

This window displays buttons for each configured valve, allowing users to toggle them between 'Open' and 'Closed' states. It also includes a button to command the main motor to move 'Up' or 'Down'. State changes and commands are sent to the connected Arduino controller.

Attributes

  • arduino_controller (ArduinoManager): An instance of the controller class responsible for communicating with the Arduino board.
  • valve_selections (np.ndarray): A NumPy array storing the current state (0 for Closed, 1 for Open) of each valve. The index corresponds to the valve number (0-indexed).
  • buttons (List[tk.Button OR None]): A list holding references to the tk.Button widgets for each valve, allowing individual access for state updates.
  • motor_button (tk.Button): The tk.Button widget used to control the motor's position.
  • motor_down (bool): A flag indicating the current assumed state of the motor (True if commanded Down, False if commanded Up).
  • valves_frame (tk.Frame): The main container frame within the Toplevel window holding the control buttons.

Methods

  • show() Makes the Valve Control window visible (deiconify).
  • create_interface() Sets up the main frame (valves_frame) and calls create_buttons to populate it.
  • toggle_motor() Callback for the motor control button. Toggles the motor_down state, updates the button text/color, and sends the corresponding "UP" or "DOWN" command to the Arduino.
  • create_buttons() Creates and arranges the tk.Button widgets for each valve based on TOTAL_CURRENT_VALVES and VALVES_PER_SIDE. Creates the motor control button.
  • toggle_valve_button(valve_num) Callback for individual valve buttons. Toggles the state of the specified valve in valve_selections, updates the button's text/color, and sends the "OPEN SPECIFIC" command followed by the current state of all valves (valve_selections) to the Arduino.
ValveControlWindow(arduino_controller: controllers.arduino_control.ArduinoManager)
70    def __init__(self, arduino_controller: ArduinoManager) -> None:
71        super().__init__()
72        self.title("Valve Control")
73        self.protocol("WM_DELETE_WINDOW", lambda: self.withdraw())
74        self.bind("<Control-w>", lambda event: self.withdraw())
75
76        for i in range(3):
77            self.grid_columnconfigure(i, weight=1)
78        for i in range(2):
79            self.grid_rowconfigure(i, weight=1)
80
81        self.resizable(False, False)
82
83        self.arduino_controller = arduino_controller
84
85        window_icon_path = GUIUtils.get_window_icon_path()
86        GUIUtils.set_program_icon(self, window_icon_path)
87
88        self.valve_selections = np.zeros((TOTAL_CURRENT_VALVES,), dtype=np.int8)
89
90        self.create_interface()
91        self.motor_down = False
92
93        # we don't want to show this window until the user decides to click the corresponding button,
94        # withdraw (hide) it for now
95        self.withdraw()

Construct a toplevel widget with the parent MASTER.

Valid resource names: background, bd, bg, borderwidth, class, colormap, container, cursor, height, highlightbackground, highlightcolor, highlightthickness, menu, relief, screen, takefocus, use, visual, width.

arduino_controller
valve_selections
motor_down
def show(self):
97    def show(self):
98        """Makes the window visible by calling `deiconify`."""
99        self.deiconify()

Makes the window visible by calling deiconify.

def create_interface(self):
101    def create_interface(self):
102        """Creates the main frame for holding the valve and motor buttons."""
103        self.valves_frame = tk.Frame(self)
104
105        self.valves_frame.grid(row=0, column=0, pady=10, sticky="nsew")
106        self.valves_frame.grid_columnconfigure(0, weight=1)
107        self.valves_frame.grid_rowconfigure(0, weight=1)
108
109        self.create_buttons()

Creates the main frame for holding the valve and motor buttons.

def toggle_motor(self):
111    def toggle_motor(self):
112        """
113        Toggles the motor's position between 'Up' and 'Down'.
114
115        Updates the motor control button's appearance (text and color) to reflect the
116        new target state and sends the appropriate command ("UP\n" or "DOWN\n")
117        to the Arduino controller. Also updates the internal `motor_down` flag.
118        """
119        # if motor is not down, command it down
120        if self.motor_down is False:
121            self.motor_button.configure(text="Move Motor Up", bg="blue", fg="white")
122
123            motor_command = "DOWN\n".encode("utf-8")
124            self.motor_down = True
125        else:
126            self.motor_button.configure(text="Move motor down", bg="green", fg="black")
127
128            motor_command = "UP\n".encode("utf-8")
129            self.motor_down = False
130
131        self.arduino_controller.send_command(command=motor_command)

Toggles the motor's position between 'Up' and 'Down'.

    Updates the motor control button's appearance (text and color) to reflect the
    new target state and sends the appropriate command ("UP

" or "DOWN ") to the Arduino controller. Also updates the internal motor_down flag.

def create_buttons(self):
133    def create_buttons(self):
134        """
135        Creates and arranges the buttons for valve and motor control.
136
137        Populates the `self.buttons` list with `tk.Button` widgets for each valve,
138        placing them in two columns ("Side One" and "Side Two"). Creates the
139        motor control button and places it in the center column.
140        """
141        # Create list of buttons to be able to access each one easily later
142        self.buttons = [None] * TOTAL_CURRENT_VALVES
143
144        # Side one valves (1-4)
145        for i in range(VALVES_PER_SIDE):  # This runs 0, 1, 2, 3
146            btn_text = f"Valve {i + 1}: Closed"
147            button = GUIUtils.create_button(
148                self.valves_frame,
149                btn_text,
150                lambda i=i: self.toggle_valve_button(i),
151                "firebrick1",
152                row=i,
153                column=0,
154            )[1]
155            self.buttons[i] = button
156
157            # This runs 4, 5, 6, 7
158            btn_text = f"Valve {i + VALVES_PER_SIDE + 1}: Closed"
159            button = GUIUtils.create_button(
160                self.valves_frame,
161                btn_text,
162                lambda i=i + VALVES_PER_SIDE: self.toggle_valve_button(i),
163                "firebrick1",
164                row=i,
165                column=2,
166            )[1]
167            self.buttons[i + VALVES_PER_SIDE] = button
168
169        self.motor_button = GUIUtils.create_button(
170            self.valves_frame,
171            "Move Motor Down",
172            command=lambda: self.toggle_motor(),
173            bg="green",
174            row=TOTAL_CURRENT_VALVES,
175            column=1,
176        )[1]

Creates and arranges the buttons for valve and motor control.

Populates the self.buttons list with tk.Button widgets for each valve, placing them in two columns ("Side One" and "Side Two"). Creates the motor control button and places it in the center column.

def toggle_valve_button(self, valve_num: int):
178    def toggle_valve_button(self, valve_num: int):
179        """
180        Updates the state of the clicked valve (`valve_num`) in the `valve_selections` array.
181        Changes the button's text and background color to reflect the new state (Open/Green or Closed/Red).
182        Sends an "OPEN SPECIFIC" command to the Arduino, followed by the byte representation
183        of the entire `valve_selections` array, instructing the Arduino which valves should be open or closed.
184
185        Parameters
186        ----------
187        - **valve_num** (*int*): The 0-based index of the valve whose button was clicked.
188        """
189        # if a valve is currently a zero (off), make it a 1, and turn the button green
190        if self.valve_selections[valve_num] == 0:
191            self.buttons[valve_num].configure(
192                text=f"Valve {valve_num + 1}: Open", bg="green"
193            )
194            self.valve_selections[valve_num] = 1
195
196        # if a valve is currently a one (on), make it a zero (off), and turn the button red
197        else:
198            self.buttons[valve_num].configure(
199                text=f"Valve {valve_num + 1}: Closed", bg="firebrick1"
200            )
201            self.valve_selections[valve_num] = 0
202
203        open_command = "OPEN SPECIFIC\n".encode("utf-8")
204        self.arduino_controller.send_command(command=open_command)
205
206        # send desired valve states to the arduino
207        selections = self.valve_selections.tobytes()
208        self.arduino_controller.send_command(command=selections)

Updates the state of the clicked valve (valve_num) in the valve_selections array. Changes the button's text and background color to reflect the new state (Open/Green or Closed/Red). Sends an "OPEN SPECIFIC" command to the Arduino, followed by the byte representation of the entire valve_selections array, instructing the Arduino which valves should be open or closed.

Parameters

  • valve_num (int): The 0-based index of the valve whose button was clicked.