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