Basic PCA level automated test example using Phidgets to power cycle a circuit board, take voltage data, plot, and store data in a CSV.
Wiring Diagram
For the purposes of this example the PCA under test is just a power supply in order to accentuate the output waveform. For a practical application this would be tied to whatever output or test signal that you wish to measure.
BOM
- VINT Phidget HUB0001_0
- Votage Input Phidget VCP1000_0
- Relay Phidget REL2001_0
- Generic Power Supply
GUI
Basic form accepts:
- Serial Number: Tagged serial number for DUT that’s tagged in output file names
- Start Up: Time in seconds data is collected prior to test (before power-up)
- Duration: Time in seconds data is collected during the test
- Shutdown: Time in seconds data is collected after test (after power-down)
- Trials: Number of iterations of the test
Output Data Files
The output data files are named in the syntax “YYYY-MM-DD HH-MM-SS [Serial Number]”. For example “2023-09-03 13-05-21 SN12345”
The output data plot would appear as follows if the the voltage input Phidget was connected to a 3.3V supply. Each trial is overlayed.
The output CSV file is in the format below.
Sample | Trial | Elapsed | Data |
---|---|---|---|
0 | 0 | 0 | -0.0027 |
1 | 0 | 0.012917 | -0.0027 |
… | … | … | … |
Code Breakdown
The GUI itself is repeat of the previous blog post. It uses tkinter to create a basic GUI with our test criteria.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
window = Tk() window.geometry("400x250") window.title("Data Acquisition") window.columnconfigure(0, weight=1) window.rowconfigure(0, weight=1) reg = window.register(callback) mainframe = ttk.Frame(window, padding="12 12 12 12") mainframe.grid(column=0, row=0) ttk.Label(mainframe,text="Serial Number",font=("Arial",16),width=15,justify=LEFT).grid(row=1,column=1) ttk.Label(mainframe,text="Start Up",font=("Arial",16),width=15,justify=LEFT).grid(row=2,column=1) ttk.Label(mainframe,text="Duration",font=("Arial",16),width=15,justify=LEFT).grid(row=3,column=1) ttk.Label(mainframe,text="Shutdown",font=("Arial",16),width=15,justify=LEFT).grid(row=4,column=1) ttk.Label(mainframe,text="Trials",font=("Arial",16),width=15,justify=LEFT).grid(row=5,column=1) SerialNumber_Box = Entry(mainframe, width=10, font=("Arial", 16)) SerialNumber_Box.grid(row=1, column=2) SerialNumber_Box.insert(0,serialNumberString) StartUp_Box = Entry(mainframe, width=10, font=("Arial", 16)) StartUp_Box.grid(row=2, column=2) StartUp_Box.insert(0,startUpDelay) StartUp_Box.config(validate="key", validatecommand=(reg, '%P')) Duration_Box = Entry(mainframe, width=10, font=("Arial", 16)) Duration_Box.grid(row=3, column=2) Duration_Box.insert(0,duration) Duration_Box.config(validate="key", validatecommand=(reg, '%P')) Shutdown_Box = Entry(mainframe, width=10, font=("Arial", 16)) Shutdown_Box.grid(row=4, column=2) Shutdown_Box.insert(0,shutdownTime) Shutdown_Box.config(validate="key", validatecommand=(reg, '%P')) Trial_Box = Entry(mainframe, width=10, font=("Arial", 16)) Trial_Box.grid(row=5, column=2) Trial_Box.insert(0,numberOfTrials) Trial_Box.config(validate="key", validatecommand=(reg, '%P')) ttk.Label(mainframe,text="",font=("Arial",15),width=20,justify=LEFT).grid(row=6,column=1,columnspan=2) f = font.Font(weight="bold",size=18,family="Arial") btn = Button(mainframe, text="RUN", command=runFunction,width=20,bg="green",fg="white",activebackground="white",activeforeground="green") btn['font'] = f btn.grid(row=7,column=1,columnspan=2) ttk.Label(mainframe,text="",font=("Arial",16),width=15,justify=LEFT).grid(row=8,column=1,columnspan=2) window.mainloop() |
When you click the RUN button. it executes the runFunction() which checks the inputs, prints out the test configuration in the terminal and calls the functions to collect the data, save the data, and plots it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
def runFunction (): global serialNumberString global startUpDelay global duration global shutdownTime global numberOfTrials try: serialNumberString = SerialNumber_Box.get() startUpDelay = int(StartUp_Box.get()) duration = int(Duration_Box.get()) shutdownTime = int(Shutdown_Box.get()) numberOfTrials = int(Trial_Box.get()) except Exception: print("ERROR: Invalid inputs. Values must be greater than zero and not empty.") return print(" ") print("======================") print(" TEST CONFIGURATION ") print("======================") print(" ") print("Serial Number: " + str(serialNumberString)) print("Startup Delay: " + str(startUpDelay)) print("Test Duration: " + str(duration)) print("Shutdown Time: " + str(shutdownTime)) print("Trials : " + str(numberOfTrials)) print(" ") global testStartTime global fileNameString testStartTime = time.strftime('%Y-%m-%d %H-%M-%S') fileNameString = testStartTime + " " + 'SN' + str(serialNumberString) dataCollection() exportData() plotData() return |
In the data collection phase we attempt to connect to the phidgets and error out if they do not exist. It should be noted that the default data rate is rather slow for the Voltage Input Phidget so we set it to what it tells us is its maximum data rate. Otherwise you’ll get a rather stair-stepping output. Then we collect our data and add the arrays to our primary one. This creates an array-of-arrays for our resulting data. Each element in the primary array is the data for that trial. The index of the primary array is the trial number so we don’t need to keep track of that. Keeping track of the overall elapsed time and not simply the sample number can be important when sampling quite fast as any delay less than 15ms can be hit-and-miss with overall timing which can distort the resulting graphs when trying to compare data between trials.
Appending a copy of the trial array to the primary array is important. Otherwise when you delete the contents for the next trial it’ll delete the entry in the primary array.
When you finish, be sure to close the connections to the phidgets. Otherwise you will need to close the GUI for the next experiment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
def dataCollection(): # Attempt to connect to Phidets try: voltageInput0 = VoltageInput() voltageInput0.setHubPort(0) voltageInput0.openWaitForAttachment(5000) voltageInput0.setDataRate(voltageInput0.getMaxDataRate()) relayOutput1 = DigitalOutput() relayOutput1.setIsHubPortDevice(True) relayOutput1.setHubPort(1) relayOutput1.openWaitForAttachment(5000) relayOutput1.setDutyCycle(0) except: print("ERROR: Phidgets not attached.") return tempVoltageDataArray = [] tempSampleTimeArray = [] global sampleTimeArrayFull global voltageDataArrayFull del sampleTimeArrayFull[:] del voltageDataArrayFull[:] currentTrial = 0 while currentTrial < numberOfTrials: testStartTime = time.time() # Startup data collection phase relayOutput1.setDutyCycle(0) currentTime = time.time() while (currentTime - testStartTime) < startUpDelay: tempVoltageDataArray.append(voltageInput0.getVoltage()) tempSampleTimeArray.append(currentTime - testStartTime) time.sleep(samplingRate) currentTime = time.time() # Primary Data collection phase relayOutput1.setDutyCycle(1) currentTime = time.time() while (currentTime - testStartTime) < (duration + startUpDelay): tempVoltageDataArray.append(voltageInput0.getVoltage()) tempSampleTimeArray.append(currentTime - testStartTime) time.sleep(samplingRate) currentTime = time.time() # Shutdown data collection phase relayOutput1.setDutyCycle(0) currentTime = time.time() while (currentTime - testStartTime) < (duration + startUpDelay + shutdownTime): tempVoltageDataArray.append(voltageInput0.getVoltage()) tempSampleTimeArray.append(currentTime - testStartTime) time.sleep(samplingRate) currentTime = time.time() sampleTimeArrayFull.append(tempSampleTimeArray.copy()) voltageDataArrayFull.append(tempVoltageDataArray.copy()) del tempVoltageDataArray[:] del tempSampleTimeArray[:] currentTrial = currentTrial + 1 # Close phidgets at end of test voltageInput0.close() relayOutput1.close() return |
Exporting the data needs to come before plotting since when you display the plot it’ll hold your script and not proceed until you close it. The exportData() function is a basic CSV script using two for loops to iterate through the primary data arrays to write everything. There’s likely a cleaner way to do this but this implementation was quick to implement and it works.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def exportData(): global sampleTimeArrayFull global voltageDataArrayFull global fileNameString f = open(fileNameString + " Data.csv", "w", newline='', encoding='utf-8') c = csv.writer(f) header = ['Sample', 'Trial', 'Elapsed', 'Data'] c.writerow(header) Sample = 0 for index, item in enumerate(sampleTimeArrayFull): for index2, item2 in enumerate(sampleTimeArrayFull[index]): tempSampleTime = sampleTimeArrayFull[index] tempVoltageData = voltageDataArrayFull[index] data = [Sample,index,tempSampleTime[index2],tempVoltageData[index2]] c.writerow(data) Sample = Sample + 1 f.close() return |
Finally, we plot our data. Likewise with the exportData() function we iterate through our arrays and plot. We use the same filename string as the data function. The file name for this was captured on the runFunction() so we capture the time stamp at the beginning of the test rather than at the end.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
def plotData(): global sampleTimeArrayFull global voltageDataArrayFull global fileNameString plt.close('all') # Close existing plots for subsequent runs plt.figure(figsize=(8,4),num="Output Data Plot") # Set size and title # plot our data for index, item in enumerate(sampleTimeArrayFull): plt.plot(sampleTimeArrayFull[index],voltageDataArrayFull[index]) # Format plt.title('SN' + str(serialNumberString) + ' Output Data Plot') plt.xlabel('Time') plt.ylabel('Voltage') plt.tight_layout() plt.savefig(fileNameString+'.png') plt.show() return |