From 7d8266d3ac432eed29a4eb034faa83438eeeda55 Mon Sep 17 00:00:00 2001
From: Corby Schmitz <cschmitz@anl.gov>
Date: Thu, 5 Dec 2024 08:53:59 -0600
Subject: [PATCH] Updating documentation on README and GUI.

---
 README.md                       |   4 +-
 client-code/gui-orchestrator.py | 187 ++++++++++++++++++--------------
 2 files changed, 108 insertions(+), 83 deletions(-)

diff --git a/README.md b/README.md
index b755423..89c455a 100644
--- a/README.md
+++ b/README.md
@@ -5,11 +5,9 @@ Valve Control Project for CSE
 Software for managing solenoid-style valves. The valve-control code runs on any CircuitPython-compatible system with at least 4 available GPIO pins and an auxiliary NeoPixel for status. Tested with both SAMD and ESP32 boards with success. The client-code runs on any system that can run python3. The choice of serial port identifiers differs by OS with Windows using COM## values and Mac/Linux systems using /dev/usbS*.
 
 ## Installation
-The client-code runs natively via the CLI on any python3-compatible system.
+The client-code runs natively as a GUI on any python3-compatible system. It leverages the TKinter libraries to render the two-window application.
 The valve-code leverages CircuitPython with drag-and-drop support to the CircuitPython drive that appears when the control board is attached to a system. This is built with the CircuitPython v9.2. It requires the addition of the following libraries from the Adafruit CircuitPython bundle (v9.2):
-asyncio (folder) - for using collaborative execution
 adafruit_pixelbuf.py - for using NeoPixel as a status indicator
-adafruit_ticks.py - for counting time
 neopixel.py - for using NeoPixel as a status indicator
 
 ## Support
diff --git a/client-code/gui-orchestrator.py b/client-code/gui-orchestrator.py
index dd99936..a716b19 100644
--- a/client-code/gui-orchestrator.py
+++ b/client-code/gui-orchestrator.py
@@ -4,80 +4,94 @@ import serial
 import sys
 import argparse
 
-statusOptions = ["opening", "Open", "closing", "closed", "starting", "initiating", "done"]
+# Used for iterative operations ... can be ammended if needed
 valves = ["Valve01", "Valve02", "Valve03", "Valve04"]
 
+# Event handler for selecting a valve from the drop-down list on the Change Valves dialogue
 def on_select(event):
     selected_item = updateComboBox.get()
     labelUpdate.config(text="Selected Item: " + selected_item)
     updateOpenTime.set(currentStatus[updateComboBox.current()][1])
     updateCloseTime.set(currentStatus[updateComboBox.current()][3])
 
+# Sets up the argument processing and help files for the program
 parser = argparse.ArgumentParser(
                     prog="valve-control-gui",
                     description="Control application for valve management",
                     epilog="Created by Corby Schmitz (cschmitz@anl.gov)"
 )
 
+# Creates a context argument to specify the COM port 
+# On Windows, the format would be COM##, where ## is replaced with a numerical value from 1-32
+# On linux/mac systems, the serial port presents as a /dev/ element
 parser.add_argument("-c", '--comport')     # Required argument to establish the com port
 args = parser.parse_args()
 
+# Kills the program if the comport is not selected
 if args.comport == None:
     print("No comport provided")
     sys.exit(0)
+# Attempts to open the comport, if fails, the application will terminate
 s = serial.Serial(args.comport)
 
+# Function to process a clean exit from the GUI, destroying both windows (even if hidden)
 def leave():
     sys.exit(0) 
 
+# Establish the main window
 window = tk.Tk()
-window.title("Valve Orchestrator")
-window.geometry("650x400")
-window.protocol('WM_DELETE_WINDOW', leave)
-label = tk.Label(window, text="Valve Orchestrator")
-label.pack()
+window.title("Valve Orchestrator")                  # Name of window
+window.geometry("650x400")                          # 640 wide x 400 high
+window.protocol('WM_DELETE_WINDOW', leave)          # Handler for clicking the x in the top corner
+label = tk.Label(window, text="Valve Orchestrator") # Label showing the window title
+label.pack()                                        # Place label at top middle of the window
 
+# Reveals the sub-dialogue for setting valve specifications
 def show():
     updateWindow.deiconify()
 
+# Hides the sub-dialogue for setting valve specifications
 def hide():
     updateWindow.withdraw()
 
+# When the update button is clicked, this function runs to set the new valve directives
+# At the end, it hides the dialog and returns focus to the main window
 def change():
     writable = (str(updateComboBox.current()+1) + "," + str(updateOpenTime.get()) + "," + str(updateCloseTime.get()) + "\r").encode(encoding="ascii", errors="strict")
-    # print(writable)
     s.write(writable)
-    # print(updateComboBox.current())
-    # print(updateOpenTime.get())
-    # print(updateCloseTime.get())
     updateWindow.withdraw()
 
-   
-
+# Set up the sub-dialogue window for setting valve directives
 updateWindow = tk.Tk()
-updateWindow.title("Valve Directive Updates")
-updateWindow.geometry("300x300")
-labelUpdate = tk.Label(updateWindow, text="Select Valve")
-labelUpdate.place(x=20, y=10)
+updateWindow.title("Valve Directive Updates")               # Name of the window
+updateWindow.geometry("300x300")                            # 300 wide x 300 high
+labelUpdate = tk.Label(updateWindow, text="Select Valve")   # Label to show the function of the dialogue
+labelUpdate.place(x=20, y=10)                               # Anchor the label at 20x10 on the new dialogue
+# Create a combobox for selecting the valve with focus ... do not autoselect a valve
 updateComboBox = ttk.Combobox(updateWindow, values=["Valve01", "Valve02", "Valve03", "Valve04"])
-#updateComboBox.set("Valve01")
-updateComboBox.bind("<<ComboboxSelected>>", on_select)
-updateComboBox.place(x=100, y=10)
-labelOpen = tk.Label(updateWindow, text="Open Time")
-labelOpen.place(x=10, y=50)
-labelClose = tk.Label(updateWindow, text="Close Time")
-labelClose.place(x=10, y=120)
+updateComboBox.bind("<<ComboboxSelected>>", on_select)      # Action on selection of a valve, function on_select above
+updateComboBox.place(x=100, y=10)                           # Combo box placement at 100x10 on the new dialogue
+labelOpen = tk.Label(updateWindow, text="Open Time")        # Label for slider for open time
+labelOpen.place(x=10, y=50)                                 # Place the label for open time at 10x50
+labelClose = tk.Label(updateWindow, text="Close Time")      # Label for slider for close time
+labelClose.place(x=10, y=120)                               # Place the label for close time at 10x120
+# Create slider to set the open time, values from 1sec to 90sec
 updateOpenTime = tk.Scale(updateWindow, from_=1, to=90, orient=tk.HORIZONTAL, length=270)
-updateOpenTime.place(x=10, y=65)
-updateCloseTime = tk.Scale(updateWindow, from_=1, to=600, orient=tk.HORIZONTAL, length=270)
-updateCloseTime.place(x=10, y=135)
+updateOpenTime.place(x=10, y=65)                            # Place open slider at 10x65 on the new dialogue
+# Create a slider to set the close time, values from 10sec to 10min
+updateCloseTime = tk.Scale(updateWindow, from_=10, to=600, orient=tk.HORIZONTAL, length=270)
+updateCloseTime.place(x=10, y=135)                          # Place close slider at 10x135 on the new dialogue
+# Create a button to commit changes to the selected valve with the new values, calse the change function above
 updateValueButton = tk.Button(updateWindow, text="Commit Updates", command=change)
-updateValueButton.place(x=100, y=200)
-hide()
+updateValueButton.place(x=100, y=200)                       # Place the button at 100x200 on the new dialogue
+hide()                                                      # Hide the window by default
 
-frame = tk.Frame(window, width=630, height=200)
-frame.place(x=10, y=20)
+# Create a frame to hold the status information on the main window
+frame = tk.Frame(window, width=630, height=200)             # Create a size of 630 wide x 200 high
+frame.place(x=10, y=20)                                     # Place the frame at 10x20 in the main window, leaving a 10pixel border
+# Create a Treeview (table) for displaying information about the current valve statuses
 valveStatus = ttk.Treeview(frame, columns=("ValveID", "ValveState", "ValveEnabled","ValveOpenCount", "ValveOpenValue", "ValveCloseCount", "ValveCloseValue"), show="headings")
+# Set the width of all columns uniformly at 90 pixels (enough for all status data)
 valveStatus.column("ValveID", width=90)
 valveStatus.column("ValveState", width=90)
 valveStatus.column("ValveEnabled", width=90)
@@ -85,96 +99,109 @@ valveStatus.column("ValveOpenCount", width=90)
 valveStatus.column("ValveOpenValue", width=90)
 valveStatus.column("ValveCloseCount", width=90)
 valveStatus.column("ValveCloseValue", width=90)
-valveStatus.pack()
-valveStatus.heading("ValveID", text="Valve")
-valveStatus.heading("ValveState", text="State")
-valveStatus.heading("ValveEnabled", text="Status")
-valveStatus.heading("ValveOpenCount", text="Open Counter")
-valveStatus.heading("ValveOpenValue", text="Open Value")
-valveStatus.heading("ValveCloseCount", text="Close Counter")
-valveStatus.heading("ValveCloseValue", text="Close Value")
-iid=0
-runAll = "startall\r".encode(encoding='ascii', errors='strict')
-stopAll = "stopall\r".encode(encoding='ascii', errors='strict')
-start1 = "start1\r".encode(encoding='ascii', errors='strict')
-start2 = "start2\r".encode(encoding='ascii', errors='strict')
-start3 = "start3\r".encode(encoding='ascii', errors='strict')
-start4 = "start4\r".encode(encoding='ascii', errors='strict')
-stop1 = "stop1\r".encode(encoding='ascii', errors='strict')
-stop2 = "stop2\r".encode(encoding='ascii', errors='strict')
-stop3 = "stop3\r".encode(encoding='ascii', errors='strict')
-stop4 = "stop4\r".encode(encoding='ascii', errors='strict')
+valveStatus.pack()                                                  # Place the table in the frame, floating at the top middle
+# Set the column names
+valveStatus.heading("ValveID", text="Valve")                        # Shows the valve ID
+valveStatus.heading("ValveState", text="State")                     # Shows state of open|closed
+valveStatus.heading("ValveEnabled", text="Status")                  # Shows running|stoppped
+valveStatus.heading("ValveOpenCount", text="Open Counter")          # Shows remaining seconds in open cycle
+valveStatus.heading("ValveOpenValue", text="Open Value")            # Shows directive value for open time
+valveStatus.heading("ValveCloseCount", text="Close Counter")        # Shows remaining seconds in close cycle
+valveStatus.heading("ValveCloseValue", text="Close Value")          # Shows directive value for close time
+iid=0                                                               # Row ID for updating valveStatus
+# Bytecode control sequences for the control unit. The use of the \r is required to complete the lines
+runAll = "startall\r".encode(encoding='ascii', errors='strict')     # Bytecode to send to valve control starting up all valves
+stopAll = "stopall\r".encode(encoding='ascii', errors='strict')     # Bytecode to send to valve control stopping all valves
+start1 = "start1\r".encode(encoding='ascii', errors='strict')       # Bytecode to send to valve control starting valve 1
+start2 = "start2\r".encode(encoding='ascii', errors='strict')       # Bytecode to send to valve control starting valve 2
+start3 = "start3\r".encode(encoding='ascii', errors='strict')       # Bytecode to send to valve control starting valve 3
+start4 = "start4\r".encode(encoding='ascii', errors='strict')       # Bytecode to send to valve control starting valve 4
+stop1 = "stop1\r".encode(encoding='ascii', errors='strict')         # Bytecode to send to valve control stopping valve 1
+stop2 = "stop2\r".encode(encoding='ascii', errors='strict')         # Bytecode to send to valve control stopping valve 2
+stop3 = "stop3\r".encode(encoding='ascii', errors='strict')         # Bytecode to send to valve control stopping valve 3
+stop4 = "stop4\r".encode(encoding='ascii', errors='strict')         # Bytecode to send to valve control stopping valve 4
+# Create color differential using even and odd tags on sequential lines
 tag="even"
 for instance in valves:
     if tag == "even":
-        valveStatus.insert("", tk.END, iid=iid, values=(instance, "Pending", "Pending", "0", "0", "0", "0"), tag="even")
+        valveStatus.insert("", tk.END, iid=iid, values=(instance, "Pending", "Pending", "0", "0", "0", "0"), tag="even")    # Tag as even
         tag = "odd"
     else:
-        valveStatus.insert("", tk.END, iid=iid, values=(instance, "Pending", "Pending", "0", "0", "0", "0"), tag="odd")
+        valveStatus.insert("", tk.END, iid=iid, values=(instance, "Pending", "Pending", "0", "0", "0", "0"), tag="odd")     # Tag as odd
         tag = "even"
-    iid +=1
-valveStatus.tag_configure("even", background="#f0f0f0")
-valveStatus.tag_configure("odd", background="#ffffff")
-# valveStatus.set(0, tk.Button(window, text="Start Program", command=lambda: s.write(start1) ), column=7)
-# encoding = 'ascii', errors = 'strict'
-# valveStatus.set()
-
+    iid +=1     # Increment the positional ID
+valveStatus.tag_configure("even", background="#f0f0f0")     # Set background to grey
+valveStatus.tag_configure("odd", background="#ffffff")      # Set background to white
+# Create buttons for initiating functions or single commands (via lambdas)
+# Button to start all valves
 startButton = tk.Button(window, text="Start All Valves", command=lambda: s.write(runAll), bg="green", fg="white" )
 startButton.place(x=10, y=250)
+# Button to stop all valves
 stopButton = tk.Button(window, text="Stop All Valves", command=lambda: s.write(stopAll), bg="red" )
 stopButton.place(x=10, y=280)
+# Button to start valve 1
 start1Button = tk.Button(window, text="Start Valve 1", command=lambda: s.write(start1), bg="green", fg="white" )
 start1Button.place(x=130, y=250)
+# Button to stop valve 1
 stop1Button = tk.Button(window, text="Stop Valve 1", command=lambda: s.write(stop1), bg="red" )
 stop1Button.place(x=130, y=280)
+# Button to start valve 2
 start2Button = tk.Button(window, text="Start Valve 2", command=lambda: s.write(start2), bg="green", fg="white" )
 start2Button.place(x=215, y=250)
+# Button to stop valve 2
 stop2Button = tk.Button(window, text="Stop Valve 2", command=lambda: s.write(stop2), bg="red" )
 stop2Button.place(x=215, y=280)
+# Button to start valve 3
 start3Button = tk.Button(window, text="Start Valve 3", command=lambda: s.write(start3), bg="green", fg="white" )
 start3Button.place(x=300, y=250)
+# Button to stop valve 3
 stop3Button = tk.Button(window, text="Stop Valve 3", command=lambda: s.write(stop3), bg="red" )
 stop3Button.place(x=300, y=280)
+# Button to start valve 4
 start4Button = tk.Button(window, text="Start Valve 4", command=lambda: s.write(start4), bg="green", fg="white" )
 start4Button.place(x=385, y=250)
+# Button to stop valve 4
 stop4Button = tk.Button(window, text="Stop Valve 4", command=lambda: s.write(stop4), bg="red" )
 stop4Button.place(x=385, y=280)
+# Button to make the update dialogue visible
 updateButton = tk.Button(window, text="Update Values", command=show, bg="blue", fg="white")
 updateButton.place(x=550, y=250)
+# Button to cleanly close the application, destroying all windows
 exitButton = tk.Button(window, text="Exit GUI", command=leave, fg="red", bg="black")
 exitButton.place(x=585, y=280)
 
+# Variable to hold the status data, multidimentional array
 currentStatus = [[],[],[],[]]
 
+# Core function to run the overall program
 def custom_mainloop():
-    global currentStatus
-    inputData = s.readline().decode("utf-8")
-    # print(inputData)
-    #while inputData:
+    global currentStatus                        # Use global status data
+    inputData = s.readline().decode("utf-8")    # Read information from the serial port connected to valve control
+    # Example data from the controller
     # 1:5:0:10:running:open;0:10:2:30:running:closed;0:30:10:90:running:closed;0:10:101:180:running:closed;
     try:
-        (valve1, valve2, valve3, valve4, drop) = inputData.split(';')
-        # (v1OpenCount, v1OpenValue, v1CloseCount, v1CloseValue, v1Enabled, v1State) = valve1.split(',')
-        # print(valve1)
-        currentStatus[0] = valve1.split(':')
-        currentStatus[1] = valve2.split(':')
-        currentStatus[2] = valve3.split(':')
-        currentStatus[3] = valve4.split(':')
-        # print(currentStatus)
+        (valve1, valve2, valve3, valve4, drop) = inputData.split(';')       # Split the 4 valves out
+        currentStatus[0] = valve1.split(':')                                # Render valve 1 data as a list
+        currentStatus[1] = valve2.split(':')                                # Render valve 2 data as a list
+        currentStatus[2] = valve3.split(':')                                # Render valve 3 data as a list
+        currentStatus[3] = valve4.split(':')                                # Render valve 4 data as a list
     except ValueError:
+        # if the splits above fail due to a bad read, simply move on
         pass
-    count = 0
+    count = 0       # Counter value to update each line of the table/tree
     for instance in valves:
-        valveStatus.set(count, column=1, value=currentStatus[count][5])
-        valveStatus.set(count, column=2, value=currentStatus[count][4])
-        valveStatus.set(count, column=3, value=currentStatus[count][0])
-        valveStatus.set(count, column=4, value=currentStatus[count][1])
-        valveStatus.set(count, column=5, value=currentStatus[count][2])
-        valveStatus.set(count, column=6, value=currentStatus[count][3])
-        count += 1
-    window.after(750, custom_mainloop)
-
+        valveStatus.set(count, column=1, value=currentStatus[count][5])     # Update valve state (open|closed)
+        valveStatus.set(count, column=2, value=currentStatus[count][4])     # Update valve status (running|stopped)
+        valveStatus.set(count, column=3, value=currentStatus[count][0])     # Update valve open counter (decrements to 0)
+        valveStatus.set(count, column=4, value=currentStatus[count][1])     # Update valve open cycle (update dialogue impacts)
+        valveStatus.set(count, column=5, value=currentStatus[count][2])     # Update valve close counter (decrements to 0)
+        valveStatus.set(count, column=6, value=currentStatus[count][3])     # Update valve close cycle (update dialogue impacts)
+        count += 1                                                          # move to next row
+    window.after(750, custom_mainloop)                                      # wait for 0.75s and then re-run the program, waiting for input
+
+# Initiate the main loop immediately
 window.after(0, custom_mainloop)
+# Handle keyboard interrupts via CTRL-C
 try:
     window.mainloop()
 except KeyboardInterrupt:
-- 
GitLab