pyetest is a Python library designed to simplify pass/fail test scripting for electronics by leveraging spreadsheets as test descriptions. The goal is to reduce custom code and make automated test setups more accessible to users who may not be programmers. By using structured spreadsheets to define measurements and criteria, pyetest generates both intermediate test data and final pass/fail outcomes making reports easier to generate and debug.
This will be easier to describe with an example case. I have a in circuit test stand which is using a LabJack U3 (here after referred to as a data acquisition system AKA a DAQ) connected to a probe board. It is measuring several analog and digital channels as well as controlling the power to the device under test (DUT). The device has a suite of self tests and internal measurements which are readout over RS-485 using a USB-RS-485 dongle.
The test jig runs the following steps:
pyetest aims to improve steps 3, 4, & 5 with a minimal amount of single use code.
Here’s an example of a useful measurement:
Say we are measuring across a 500 mohm current sense resistor with a 0.1% tolerance. We care about the current but we the input voltage can vary by say 5%. In the template spreadsheet we have measurement fields using our DAQ to measure across the sense resistor and a calculated field for the current. The formula for this would look like: =(A1-A2)/0.5
. Since our DAQ is measuring in ADC instead of volts we’ll have to add calculation fields to first calculate volts and then the current which is straight forward. The calculated value is filled in and accessible to the pass/fail test which might then use a min/max test with a some error bars. Sure this is easy to put into a python script but it’s even easier to put into a spreadsheet. In this case the control software is all the same and the tests are defined in the input files only. Different board versions can use the same control software with a different set of input files.
pyetest relies on using what I believe to be the most commonly used interpreted language out there: excel. I’m not advocating for huge unwieldy excel spreadsheets but most people know how to use them and therefore it’s easier to get new people to maintain a test system built this way than one written from custom python. Spreadsheets have a large number of useful function built in and we don’t need to worry about supporting them or sweat the security issues that could come from other possible solutions (using eval statements did come to mind). pyetest uses the libreoffice calc program as a math interpreter to do the intermediate calculations and set the pass/fail criteria. The main bit of hackery here is save the workbooks as temporary files and forcing them to be recalculated. If there’s a better way to do this let me know!
The spreadsheets act as a plugin module to the software without needing the operator to know any python. You express the relationships in the data template and create the pass/fail criteria in the test template.
This is an xlsx spreadsheet which contains the test names, data source, and field type. The field type specifies if the data is measured, fixed, or calculated. Measured values will be filled in during the test. Fixed values are used in the spreadsheet for calculations of other values such as the number of bits in an ADC. Calculated values have a formula in the value field.
name | value | unit | type | |
---|---|---|---|---|
0 | version | 0.8.X | nan | measured |
1 | firmware_version | 0.1.8 | nan | measured |
2 | compile_time | 09:09:00 | nan | measured |
3 | compile_date | September 9th, 2023 | nan | measured |
4 | slave_address | 246 | nan | measured |
5 | p3_3_micro_volts | 3260000 | uV | measured |
6 | p3_3_reading | 2024 | ADC | measured |
7 | +3.3GAIN | =1/2 | nan | fixed |
8 | +3.3OFFSET | 0 | V | fixed |
9 | +3.3V | =B8/4096*3.3/B9 | V | calculated |
10 | +3.3ERROR | =ABS(B11/24-1)*100 | % | calculated |
11 | +3.3COMPARISONERROR | =ABS(B11/(B7/1000000)-1)*100 | % | calculated |
12 | p23_micro_volts | 23000000 | uV | measured |
13 | p23_reading | 1200 | ADC | measured |
14 | +23GAIN | =1/25 | nan | fixed |
15 | +23OFFSET | 0 | V | fixed |
16 | +23V | =B15/4096*3.3/B16 | V | calculated |
17 | +23ERROR | =ABS(B18/23.3-1)*100 | % | calculated |
18 | +23COMPARISONERROR | =ABS(B18/(B14/1000000)-1)*100 | % | calculated |
19 | p5_micro_volts | 5000000 | uV | measured |
20 | p5_reading | 2500 | ADC | measured |
21 | +5OFFSET | 0 | V | fixed |
22 | +5GAIN | =5/2 | nan | fixed |
23 | +5 | =B22/4096B243.3 | V | calculated |
24 | +5ERROR | =ABS((B25/5-1)*100) | % | calculated |
25 | +5COMPARISONERROR | =ABS(B25/(B21/1000000)-1)*100 | % | calculated |
26 | fault_status | 9 | nan | measured |
27 | +5 Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,0)),2) | nan | calculated |
28 | +24 Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,1)),2) | nan | calculated |
29 | Error Code 2 | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,2)),2) | nan | calculated |
30 | DAQ Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,3)),2) | nan | calculated |
31 | Error Code 4 | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,4)),2) | nan | calculated |
32 | MW Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,5)),2) | nan | calculated |
33 | Error Code 6 | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,6)),2) | nan | calculated |
34 | Error Code 7 | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,7)),2) | nan | calculated |
35 | Visible Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,8)),2) | nan | calculated |
36 | Temperature Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,9)),2) | nan | calculated |
37 | Moisture Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,10)),2) | nan | calculated |
38 | Watchdog Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,11)),2) | nan | calculated |
39 | Hardware Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,12)),2) | nan | calculated |
40 | I2C Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,13)),2) | nan | calculated |
41 | Error Code 14 | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,14)),2) | nan | calculated |
42 | Modbus Fault | =MOD(_xlfn.FLOOR.MATH(B$28/POWER(2,15)),2) | nan | calculated |
A spreadsheet which defines the pass/fail criteria. The structure is open for extension but includes at a minimum the field name, a value field that will be filled in, and a passes column that contains an expression (usually something like =E1=B1
or =E1>=C1 & E1<=D1
).
name | expected value | unit | min | max | value | passes | |
---|---|---|---|---|---|---|---|
0 | version | 0.8.X | nan | nan | nan | 0.8.X | =B2=F2 |
1 | firmware_version | 0.1.8 | nan | nan | nan | 0.1.8 | =B3=F3 |
2 | compile_time | 09:09:00 | nan | nan | nan | 09:09:00 | =B4=F4 |
3 | compile_date | September 9th, 2023 | nan | nan | nan | September 9th, 2023 | =B5=F5 |
4 | slave_address | 246 | nan | nan | nan | 246 | =B6=F6 |
5 | p3_3_micro_volts | 3300000 | uV | 3.2e+06 | 3.4e+06 | 3260000 | =F7>=D7 & F7<=E7 |
6 | p3_3_reading | 2100 | ADC | nan | nan | 2024 | nan |
7 | +3.3ERROR | 0 | % | 0 | 3 | 86.4111328125 | =F9>=D9 & F9<=E9 |
8 | p23_micro_volts | 23000000 | uV | 1.8e+07 | 3e+07 | 23000000 | =F10>=D10 & F10<=E10 |
9 | p23_reading | 1200 | ADC | nan | nan | 1200 | nan |
10 | +23ERROR | 0 | % | 0 | 3 | 3.73357027896994 | =F12>=D12 & F12<=E12 |
11 | p5_micro_volts | 5000000 | uV | 4.9e+06 | 5.1e+06 | 5000000 | =F13>=D13 & F13<=E13 |
12 | p5_reading | 2500 | ADC | nan | nan | 2500 | nan |
13 | +5ERROR | 0 | % | 0 | 3 | 0.7080078125 | =F15>=D15 & F15<=E15 |
14 | fault_status | 0 | nan | nan | nan | 9 | nan |
15 | +5 Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,0)),2) | nan | nan | nan | 1 | =B17=F17 |
16 | +24 Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,1)),2) | nan | nan | nan | 0 | =B18=F18 |
17 | Error Code 2 | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,2)),2) | nan | nan | nan | 0 | =B19=F19 |
18 | DAQ Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,3)),2) | nan | nan | nan | 1 | =B20=F20 |
19 | Error Code 4 | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,4)),2) | nan | nan | nan | 0 | =B21=F21 |
20 | MW Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,5)),2) | nan | nan | nan | 0 | =B22=F22 |
21 | Error Code 6 | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,6)),2) | nan | nan | nan | 0 | =B23=F23 |
22 | Error Code 7 | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,7)),2) | nan | nan | nan | 0 | =B24=F24 |
23 | Visible Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,8)),2) | nan | nan | nan | 0 | =B25=F25 |
24 | Temperature Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,9)),2) | nan | nan | nan | 0 | =B27=F27 |
25 | Moisture Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,10)),2) | nan | nan | nan | 0 | nan |
26 | Watchdog Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,11)),2) | nan | nan | nan | 0 | =B28=F28 |
27 | Hardware Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,12)),2) | nan | nan | nan | 0 | =B29=F29 |
28 | I2C Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,13)),2) | nan | nan | nan | 0 | =B30=F30 |
29 | Error Code 14 | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,14)),2) | nan | nan | nan | 0 | =B31=F31 |
30 | Modbus Fault | =MOD(_xlfn.FLOOR.MATH(B$16/POWER(2,15)),2) | nan | nan | nan | 0 | =B32=F32 |
Data taken from all our sources.
name | value | unit | type | |
---|---|---|---|---|
0 | version | 0.8.X | nan | measured |
1 | firmware_version | 0.1.8 | nan | measured |
2 | compile_time | 09:09:00 | nan | measured |
3 | compile_date | September 9th, 2023 | nan | measured |
4 | slave_address | 246 | nan | measured |
5 | p3_3_micro_volts | 3260000 | uV | measured |
6 | p3_3_reading | 2024 | ADC | measured |
7 | p23_micro_volts | 23000000 | uV | measured |
8 | p23_reading | 1200 | ADC | measured |
9 | p5_micro_volts | 5000000 | uV | measured |
10 | p5_reading | 2500 | ADC | measured |
11 | fault_status | 9 | nan | measured |
This is the data template file with the measured values filled in. All the calculation expressions are retained in the file.
name | value | unit | type | |
---|---|---|---|---|
0 | version | 0.8.X | nan | measured |
1 | firmware_version | 0.1.8 | nan | measured |
2 | compile_time | 09:09:00 | nan | measured |
3 | compile_date | September 9th, 2023 | nan | measured |
4 | slave_address | 246 | nan | measured |
5 | p3_3_micro_volts | 3260000 | uV | measured |
6 | p3_3_reading | 2024 | ADC | measured |
7 | +3.3GAIN | 0.5 | nan | fixed |
8 | +3.3OFFSET | 0 | V | fixed |
9 | +3.3V | 3.261328125 | V | calculated |
10 | +3.3ERROR | 86.4111328125 | % | calculated |
11 | +3.3COMPARISONERROR | 0.0407400306748462 | % | calculated |
12 | p23_micro_volts | 23000000 | uV | measured |
13 | p23_reading | 1200 | ADC | measured |
14 | +23GAIN | 0.04 | nan | fixed |
15 | +23OFFSET | 0 | V | fixed |
16 | +23V | 24.169921875 | V | calculated |
17 | +23ERROR | 3.73357027896994 | % | calculated |
18 | +23COMPARISONERROR | 5.0866168478261 | % | calculated |
19 | p5_micro_volts | 5000000 | uV | measured |
20 | p5_reading | 2500 | ADC | measured |
21 | +5OFFSET | 0 | V | fixed |
22 | +5GAIN | 2.5 | nan | fixed |
23 | +5 | 5.035400390625 | V | calculated |
24 | +5ERROR | 0.7080078125 | % | calculated |
25 | +5COMPARISONERROR | 0.7080078125 | % | calculated |
26 | fault_status | 9 | nan | measured |
27 | +5 Fault | 1 | nan | calculated |
28 | +24 Fault | 0 | nan | calculated |
29 | Error Code 2 | 0 | nan | calculated |
30 | DAQ Fault | 1 | nan | calculated |
31 | Error Code 4 | 0 | nan | calculated |
32 | MW Fault | 0 | nan | calculated |
33 | Error Code 6 | 0 | nan | calculated |
34 | Error Code 7 | 0 | nan | calculated |
35 | Visible Fault | 0 | nan | calculated |
36 | Temperature Fault | 0 | nan | calculated |
37 | Moisture Fault | 0 | nan | calculated |
38 | Watchdog Fault | 0 | nan | calculated |
39 | Hardware Fault | 0 | nan | calculated |
40 | I2C Fault | 0 | nan | calculated |
41 | Error Code 14 | 0 | nan | calculated |
42 | Modbus Fault | 0 | nan | calculated |
This is the test template file with the values filled in. The boolean passes values can then be read out to determine which tests passed.
name | expected value | unit | min | max | value | passes | |
---|---|---|---|---|---|---|---|
0 | version | 0.8.X | nan | nan | nan | 0.8.X | 1 |
1 | firmware_version | 0.1.8 | nan | nan | nan | 0.1.8 | 1 |
2 | compile_time | 09:09:00 | nan | nan | nan | 09:09:00 | 1 |
3 | compile_date | September 9th, 2023 | nan | nan | nan | September 9th, 2023 | 1 |
4 | slave_address | 246 | nan | nan | nan | 246 | 1 |
5 | p3_3_micro_volts | 3300000 | uV | 3.2e+06 | 3.4e+06 | 3260000 | 1 |
6 | p3_3_reading | 2100 | ADC | nan | nan | 2024 | nan |
7 | +3.3ERROR | 0 | % | 0 | 3 | 86.4111328125 | 1 |
8 | p23_micro_volts | 23000000 | uV | 1.8e+07 | 3e+07 | 23000000 | 1 |
9 | p23_reading | 1200 | ADC | nan | nan | 1200 | nan |
10 | +23ERROR | 0 | % | 0 | 3 | 3.73357027896994 | 1 |
11 | p5_micro_volts | 5000000 | uV | 4.9e+06 | 5.1e+06 | 5000000 | 1 |
12 | p5_reading | 2500 | ADC | nan | nan | 2500 | nan |
13 | +5ERROR | 0 | % | 0 | 3 | 0.7080078125 | 1 |
14 | fault_status | 0 | nan | nan | nan | 9 | nan |
15 | +5 Fault | 0 | nan | nan | nan | 1 | 0 |
16 | +24 Fault | 0 | nan | nan | nan | 0 | 1 |
17 | Error Code 2 | 0 | nan | nan | nan | 0 | 1 |
18 | DAQ Fault | 0 | nan | nan | nan | 1 | 0 |
19 | Error Code 4 | 0 | nan | nan | nan | 0 | 1 |
20 | MW Fault | 0 | nan | nan | nan | 0 | 1 |
21 | Error Code 6 | 0 | nan | nan | nan | 0 | 1 |
22 | Error Code 7 | 0 | nan | nan | nan | 0 | 1 |
23 | Visible Fault | 0 | nan | nan | nan | 0 | 1 |
24 | Temperature Fault | 0 | nan | nan | nan | 0 | 1 |
25 | Moisture Fault | 0 | nan | nan | nan | 0 | nan |
26 | Watchdog Fault | 0 | nan | nan | nan | 0 | 1 |
27 | Hardware Fault | 0 | nan | nan | nan | 0 | 1 |
28 | I2C Fault | 0 | nan | nan | nan | 0 | 1 |
29 | Error Code 14 | 0 | nan | nan | nan | 0 | 1 |
30 | Modbus Fault | 0 | nan | nan | nan | 0 | 1 |
import pandas
import openpyxl
import pyetest
data_template_file = "data_template.xlsx"
measurement_result_file = "measurement_result.xlsx"
measured_data_file = "dummy-values.xlsx"
test_template_file = "test_template.xlsx"
test_result_file = "test-results.xlsx"
data_template_workbook = openpyxl.open(data_template_file)
test_template_workbook = openpyxl.open(test_template_file)
dummy_values_df = pandas.read_excel(measured_data_file)
data_workbook, test_workbook = pyetest.pyetest_run(data_template_workbook, test_template_workbook, dummy_values_df)
test_results_df = pyetest.worksheet_to_df(test_workbook.active)
print(len(test_results_df[test_results_df["passes"] == False]) == 0)
print(test_results_df[test_results_df["passes"] == False])
test_workbook.save(test_result_file)
data_workbook.save(measurement_result_file)
False
name expected value unit min max value passes
15 +5 Fault 0 None NaN NaN 1 False
18 DAQ Fault 0 None NaN NaN 1 False
There are some functions that excel doesn’t do easily that would be useful. When needing to add a new measurement type the first goal is to figure out an excel way to do it. It’s a well understood interpreter after all. Those that can’t be done using excel functions I am adding as plugins. The danger there is taking the plugin route and increasing complexity when a simpler solution is available.
I would like to add curve fitting but as this is a specialized function I would rather add this and other special functions to the data collection step. I expect pyetest to stay as it is with other libraries being used to add additional functionality to the system.
For completeness some examples of existing ICT software are:
I could not find any open source projects that fit the use case, if I missed any please let me know, I’d love to hear about them!