diff --git a/.gitignore b/.gitignore index 7d9e731a..ee7c1c98 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +PythonFiles/Scanner/bin/runScanner /__pycache__ /PREV-VERSION/__pycache__ /virtualenvironment @@ -12,3 +13,4 @@ __pycache__/ *.o *.log *.json +GUILogs/* diff --git a/CheckInGUI/Configs/Demo_Local_cfg.yaml b/CheckInGUI/Configs/Demo_Local_cfg.yaml new file mode 100644 index 00000000..dcc1447c --- /dev/null +++ b/CheckInGUI/Configs/Demo_Local_cfg.yaml @@ -0,0 +1,84 @@ +--- + +# Specify which board you want to test here +GUIType: Demo + +# Specify if a scanner is used for barcodes +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# Uncomment the test handler which you would like to use +# +# Note that for SSH, the username and hostname must be specified. +# It is assumed that you have set up SSH key access for this username +# On the specified host. +# +# ZMQ will use the built-in request server and client. +# The IP address of the GUI node and testing node must be specified separately +# Additionally, SSH key access removes the need to start the server +# on the tester by hand. Path and server file name needed for remote +# server start up +# +TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +#TestHandler: {name: ZMQ, localip: , remoteip: , username: , serverpath: , serverscript: } + + +# Let the GUI know if you want to check serial numbers for multiple board types at a single testing location +SerialCheckSafe: false + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py + +######################################## +# NOTE TO SELF: Add in test sequence as # +# a separate entry in config # +# All tests in one area, not physical # +# vs. automatic and add a test type # +# # +######################################## + + +# Tests which require a physical measurement +# The pass fail criteria for these tests are specified here +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + ################################# + # REMOVE FOR DEMO # + ################################# + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: false + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/CheckInGUI/Configs/Demo_SSH_cfg.yaml b/CheckInGUI/Configs/Demo_SSH_cfg.yaml new file mode 100644 index 00000000..42026ecf --- /dev/null +++ b/CheckInGUI/Configs/Demo_SSH_cfg.yaml @@ -0,0 +1,71 @@ +--- +#----------------------------------------------- +# Note: Demo will not work out of the box +# Need to follow installation procedure in +# README.md +#----------------------------------------------- + +# Specify which board you want to test here +# this doesn't actually matter much, the GUI type will update based on the board type entered in scan scene +GUIType: Demo + +# Specify if a scanner is used for barcodes +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# Uncomment the test handler which you would like to use +# +# Note that for SSH, the username and hostname must be specified. +# It is assumed that you have set up SSH key access for this username on the specified host. +# +# ZMQ will use the built-in request server and client. +# The IP address of the GUI node and testing node must be specified separately +# Additionally, SSH key access removes the need to start the server +# on the tester by hand. Path and server file name needed for remote +# server start up +#TestHandler: {name: Local, remoteip: localhost} +TestHandler: {name: SSH, username: bovar008, hostname: cmsfactory2, remoteip: } +#TestHandler: {name: ZMQ, localip: , remoteip: , username: , serverpath: , serverscript: } + +# Let the GUI know if you want to check serial numbers for multiple board types at a single testing location +SerialCheckSafe: false + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestPath and TestScript aren't used in SSH +# TestCommand field is the command run in SSH, -u is needed for realtime output from a python script +# TestPath should be in reference to the testing home directory +Test: +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py + TestCommand: python3 -u HGCALTestGUI/Tests/demo_count.py + TestConfig: HGCALTestGUI/Tests/test_configs/counting.yaml + +# Tests which require a physical measurement +# The pass fail criteria for these tests are specified here +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: false + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/CheckInGUI/Configs/Engine_cfg.py b/CheckInGUI/Configs/Engine_cfg.py new file mode 100644 index 00000000..0afac8a1 --- /dev/null +++ b/CheckInGUI/Configs/Engine_cfg.py @@ -0,0 +1,223 @@ +base_path = "/home/HGCAL_dev/test_scripts" + +from dumpToYaml import dump_to_yaml + +masterCfg = { + + "GUIType": "Engine", + + "UsingScanner": True, + + + # Order of tests matters here + # This should be the same order that you want the tests to be run in + # Number of test will also be decide by this list so don't miss any + "Test": [ + { + "name": "Power-Ground Resistance", + "required": 1, + "desc_short": "Measure resistance between power and ground", + "desc_long": "Check that the power and grounds are not shorted at the terminal, or between the inputs.", + "TestClass" : "TestPowerGround", + }, + { + "name": "1.5V Input Check", + "required": 1, + "desc_short": "Check that the 1.5V input is not shorted.", + "desc_long": "Check that resistance between across C906 or C908 is non-zero.", + "TestClass" : "Test1.5VInput", + }, + { + "name": "10V Input Check", + "required": 1, + "desc_short": "Check that the 10V input is not shorted.", + "desc_long": "Check that resistance between across C907 or C909 is non-zero.", + "TestClass" : "Test10VInput", + }, + { + "name": "1.2V Output Check", + "required": 1, + "desc_short": "Check that the 1.2V output is not shorted.", + "desc_long": "Check that resistance between across C904 or C904 or TP901 is non-zero.", + "TestClass" : "Test1.2VOutput", + }, + { + "name": "RX 2.5V Output Check", + "required": 1, + "desc_short": "Check that the RX 2.5V output is not shorted.", + "desc_long": "Check that resistance across C902 is non-zero.", + "TestClass" : "TestRX2.5VOutput", + }, + { + "name": "TX 2.5V Output Check", + "required": 1, + "desc_short": "Check that the TX 2.5V output is not shorted.", + "desc_long": "Check that resistance across either C903 or TP902 is non-zero.", + "TestClass" : "TestTX2.5VOutput", + }, + # Power on Tests + { + "name": "LDO Output", + "required": 1, + "desc_short": "Check that the LDO output voltage is around 1.2V", + "desc_long": "Measure the votlage across either R911 or TP901 and verify that it is appropriate.", + "TestClass" : "TestLDOOutput", + }, + { + "name": "LinPol RX Check", + "required": 1, + "desc_short": "Check that the RX voltage from the linppol is operating correctly", + "desc_long": "Check that voltages across either R905 or R902 is 2.5V.", + "TestClass" : "TestLinPolRX", + }, + { + "name": "LinPol TX Check", + "required": 1, + "desc_short": "Check that the TX voltage from the linppol is operating correctly", + "desc_long": "Measure the voltage across either TP902 or R906 or C903 is 2.5V.", + "TestClass" : "TestLinPolTX", + }, + + #Operations Tests + { + "name": "X_PWR", + "required": 1, + "desc_short": "Check the the X_PWR voltage is correct.", + "desc_long": "Measure using the tester, and should find approximately 1.2V.", + "TestClass" : "TestXPWR", + }, + { + "name": "lpGBT setup", + "required": 1, + "desc_short": "Ensure setup can be performed", + "desc_long": "Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz", + "TestClass" : "TestlpGBTsetup", + }, + { + "name": "EClock Rates", + "required": 1, + "desc_short": "Ensure EClock rates are correct", + "desc_long": "Check that all EClocks are running at 320MHz.", + "TestClass" : "TestEClock", + }, + { + "name": "lpGBT IC/EC communication", + "required": 1, + "desc_short": "Check operability of lpGBT IC/EC communication", + "desc_long": "Read and write to lpBGT registers via ICEC. Check DAQ lpGBT read of registers via IC. Check Trigger lpGBTs: successful read registers via EC. Ensure write and readback to user ID registers (0x004 - 0x007)", + "TestClass" : "TestlpGBTcom", + }, + { + "name": "I2C", + "required": 1, + "desc_short": "Engine can use I2C master", + "desc_long": "Check that engine can communicate as an I2C master", + #"TestScript": "engine_test_suite.py", + "TestClass" : "TestI2C", + + }, + { + "name": "GPIO functionality", + "required": 1, + "desc_short": "Check the quality of the GPIOs", + "desc_long": "Read and write to all GPIO channels and verify levels. Write nominal configuration and then toggle each line one-by-one and verify change in both lpGBT status and voltage level", + "TestClass" : "TestGpio" + }, + { + "name": "ADC functionality", + "required": 1, + "desc_short": "Check quality of the ADCs", + "desc_long": "Measure known voltages/resistances. Check measured values for all 4 gains within tolerances, (only need to do all 4 gains for one measurement).", + "TestClass" : "TestAdc" + }, + { + "name": "Uplink quality", + "required": 1, + "desc_short": "Check the quality of the uplinks", + "desc_long": "PRBS validation from lpGBTs. Check bit error rate below threshold.", + "TestClass" : "TestUplink" + }, + { + "name": "Downlink quality", + "required": 1, + "desc_short": "Check the quality of the downlinks", + "desc_long": "Eye opening test. Check eye opening width and height below threshold.", + "TestClass" : "TestDownlink", + }, + { + "name": "Fast Command quality", + "required": 1, + "desc_short": "Check the quality of the Fast Command path", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestFC" + }, + { + "name": "Elink quality", + "required": 1, + "desc_short": "Check the quality of the elinks", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestElinkUp" + }, + { + "name": "Crossover link quality", + "required": 1, + "desc_short": "Check the quality of the crossover links", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestCrossover", + }, + ], + + + "PhysicalTest": [ + #{ + # "name": "SAMPLE test", + # "required": 1, + # "desc_short": "Some short description", + # "desc_long": "Really long description for later purposes.", + # "criteria": { + # "first testing criteria", + # "second testing criteria", + # "third testing criteria", + # }, + + #}, + + ], + + "Board_type": [ + { + "name": "Engine V3 Right", + "type_sn": "100300", + "requiredTests": [0, 1, 2, 3, 4], + }, + { + "name": "Engine V3 Left", + "type_sn": "100310", + "requiredTests": [0, 1, 2, 3, 4], + }, + ], + # People who you would like to add as testers by default + # HGCAL_dev can be used for debug testing in the beginning + # The GUI will require everyone to have their own "account" + "People": [ + "Nadja", + "Charlie", + "Bryan", + "Devin", + "HGCAL_dev", + ], + # Information for sending and receiving data to/from the database + # Needs to be different based on board type + "DBInfo": { + "use_database": True, + "name": "EngineDB", + "reader": "EngineDBReadUser", + "inserter": "EngineDBInserter", + "admin": "EngineDBInserter", + "baseURL": "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/EngineDB", + }, +} + +masterCfg["Test"] = [dict(**x, TestPath=base_path, TestScript= "engine_test_suite.py") for x in masterCfg["Test"] if "TestClass" in x] + +dump_to_yaml(masterCfg) diff --git a/CheckInGUI/Configs/Engine_cfg.yaml b/CheckInGUI/Configs/Engine_cfg.yaml new file mode 100644 index 00000000..001cadbd --- /dev/null +++ b/CheckInGUI/Configs/Engine_cfg.yaml @@ -0,0 +1,131 @@ +GUIType: Engine + +People: [Nadja, Charlie, Bryan, Devin, HGCAL_dev] + +Board_type: + - name: Engine V3 Right + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100300' + - name: Engine V3 Left + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100310' + +DBInfo: + admin: FactoryInserter + #baseURL: http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/EngineDB + baseURL: http://cmslab1.spa.umn.edu/Factory/EngineDB + inserter: FactoryInserter + name: EngineDB_PRO + reader: FactoryReadUser + use_database: true + + +SerialCheckSafe: true +UsingScanner: true + +# TestHandler: {name: Local, remoteip: localhost} +# TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: "localhost", remoteip: "umn-zcu102-b", username: "test_server", + serverpath: , serverscript: } + + +PhysicalTest: [] +Test: + - TestClass: TestXPWR + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Measure using the tester, and should find approximately 1.2V. + desc_short: Check the the X_PWR voltage is correct. + name: X_PWR + required: 1 + + - TestClass: TestSetupLpgbt + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus + (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All + 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz + desc_short: Ensure setup can be performed + name: lpGBT setup + required: 1 + + - TestClass: TestLpgbtId + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Read lpgbt ids + desc_short: Check ids + name: LPGBT ID + required: 1 + + + - TestClass: TestI2C + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_i2c.py + desc_long: Check that engine can communicate as an I2C master + desc_short: Engine can use I2C master + name: I2C + required: 1 + + - TestClass: TestGpio + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_gpio.py + desc_long: Read and write to all GPIO channels and verify levels. Write nominal + configuration and then toggle each line one-by-one and verify change in both + lpGBT status and voltage level + desc_short: Check the quality of the GPIOs + name: GPIO functionality + required: 1 + + - TestClass: TestAdc + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_adc.py + desc_long: Measure known voltages/resistances. Check measured values for all 4 + gains within tolerances, (only need to do all 4 gains for one measurement). + desc_short: Check quality of the ADCs + name: ADC functionality + required: 1 + + - TestClass: TestUplink + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from lpGBTs. Check bit error rate below threshold. + desc_short: Check the quality of the uplinks + name: Uplink quality + required: 1 + + + - TestClass: TestFC + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the Fast Command path + name: Fast Command quality + required: 1 + + - TestClass: TestElinkUp + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the elinks + name: Elink quality + required: 1 + + - TestClass: TestEClockRates + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: Check that all EClocks are running at 320MHz. + desc_short: Ensure EClock rates are correct + name: EClock Rates + required: 1 + + +InspectionTest: +- TestClass: Inspection + name: Inspection1 + required: 1 + checkboxes: + - {text: "Is the board bent?", value: False, requirement: False} + - {text: "Is the board visibly broken?", value: False, requirement: False} + - {text: "Are any components missing?", value: False, requirement: False} + - {text: "Are any components visibly broken?", value: False, requirement: False} + comments: No Comment diff --git a/CheckInGUI/Configs/Flex_cfg.yaml b/CheckInGUI/Configs/Flex_cfg.yaml new file mode 100644 index 00000000..3ba55dcf --- /dev/null +++ b/CheckInGUI/Configs/Flex_cfg.yaml @@ -0,0 +1,32 @@ +--- +DBInfo: + baseURL: http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB + inserter: WagonDBInserter + name: WagonDB + reader: WagonDBReadUser + use_database: false +GUIType: FlexCable +SerialCheckSafe: false +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: cmstester8, username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/FlexTest/FlexCableTesting + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: ID Resistor Measurement + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/FlexTest/FlexCableTesting + TestScript: run_bert_tmp.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Bit Error Rate Test + required: 1 +UsingScanner: true + +People: [Lauren, Billy] +... diff --git a/CheckInGUI/Configs/Skeleton_cfg.yaml b/CheckInGUI/Configs/Skeleton_cfg.yaml new file mode 100644 index 00000000..2968c97c --- /dev/null +++ b/CheckInGUI/Configs/Skeleton_cfg.yaml @@ -0,0 +1,67 @@ +--- + +# Specify which board you want to test here +GUIType: Wagon + +# Specify if a scanner is used for barcodes +UsingScanner: true + +# How the tests will be run (local, ssh, or ZMQ) +TestHandler: ZMQ + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: +- name: Resistance Measurement + required: true + desc_short: Measure resistance of analog lines + desc_long: Test must be completed before attempting to measure ID resistor + TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py +- name: ID Resistor Measurement + required: true + desc_short: Measure resistance of ID resistor + desc_long: Must be completed after the general resistance measurement + TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py +- name: I2C Read/Write + required: true + desc_short: Check I2C read/write along wagon + desc_long: Test must be completed before BERT for wagon wheel configuration + TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw + TestScript: run_iic_check.py +- name: Bit Error Rate Test + required: true + desc_short: Determine quality of data transmission + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + TestClass: BERT + TestPath: /home/HGCAL_dev/sw + TestScript: run_bert.py + + +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: true + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/CheckInGUI/Configs/Wagon_cfg.py b/CheckInGUI/Configs/Wagon_cfg.py new file mode 100644 index 00000000..4099544d --- /dev/null +++ b/CheckInGUI/Configs/Wagon_cfg.py @@ -0,0 +1,85 @@ +from dumpToYaml import dump_to_yaml + +masterCfg = { + + "GUIType": "Wagon", + + "UsingScanner": True, + + # Order of tests matters here + # This should be the same order that you want the tests to be run in + # Number of test will also be decide by this list so don't miss any + # TestClass, TestScript, and TestPath fields will be used to write the REPserver script + # TestPath should be in reference to the testing home directory + "Test": [ + { + "name": "Resistance Measurement", + "required": 1, + "desc_short": "Measure resistance of analog lines", + "desc_long": "Test must be completed before attempting to measure ID resistor", + "TestClass": "gen_resist_test", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "wagon_rtd.py" + }, + + { + "name": "ID Resistor Measurement", + "required": 1, + "desc_short": "Measure resistance of ID resistor", + "desc_long": "Must be completed after the general resistance measurement", + "TestClass": "id_resist_test", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "wagon_rtd.py" + }, + + { + "name": "I2C Read/Write", + "required": 1, + "desc_short": "Check I2C read/write along wagon", + "desc_long": "Test must be completed before BERT for wagon wheel configuration", + "TestClass": "IIC_Check", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "run_iic_check.py" + }, + + { + "name": "Bit Error Rate Test", + "required": 1, + "desc_short": "Determine quality of data transmission", + "desc_long": "Needs to be completed after I2C check in order to set up wagon wheel", + "TestClass": "BERT", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "run_bert.py" + }, + ], + + + "PhysicalTest": [ + #{ + # "name": "SAMPLE test", + # "required": 1, + # "desc_short": "Some short description", + # "desc_long": "Really long description for later purposes.", + # "criteria": { + # "first testing criteria", + # "second testing criteria", + # "third testing criteria", + # }, + + #}, + + ], + + + # Information for sending and receiving data to/from the database + # Needs to be different based on board type + "DBInfo": { + "use_database": True, + "name": "WagonDB", + "reader": "WagonDBReadUser", + "inserter": "WagonDBInserter", + "baseURL": "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" + } + } + +dump_to_yaml(masterCfg) diff --git a/CheckInGUI/Configs/Wagon_cfg.yaml b/CheckInGUI/Configs/Wagon_cfg.yaml new file mode 100644 index 00000000..cb1873c9 --- /dev/null +++ b/CheckInGUI/Configs/Wagon_cfg.yaml @@ -0,0 +1,62 @@ +--- +DBInfo: + #baseURL: http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: FactoryInserter + name: WagonDB_PRO + reader: FactoryReadUser + use_database: true +GUIType: Wagon +SerialCheckSafe: true +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: 192.168.140.22, username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: ADC + TestPath: /home/HGCAL_dev/sw + TestScript: run_adc_self_test.py + desc_long: Checking all ADCs to see if they are running as expected before running any tests + desc_short: ADC interal test + name: ADC Self Test + required: 1 +- TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py + desc_long: Test must be completed before attempting to measure ID resistor + desc_short: Measure resistance of analog lines + name: Resistance Measurement + required: 1 +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: ID Resistor Measurement + required: 1 +- TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw + TestScript: run_iic_check.py + desc_long: Test must be completed before BERT for wagon wheel configuration + desc_short: Check I2C read/write along wagon + name: I2C Read/Write + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/sw + TestScript: run_bert.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Bit Error Rate Test + required: 1 +UsingScanner: true +InspectionTest: +- TestClass: Inspection + name: Inspection1 + required: 1 + checkboxes: + - {text: "Is the board bent?", value: False, requirement: False} + - {text: "Is the board visibly broken?", value: False, requirement: False} + - {text: "Are any components missing?", value: False, requirement: False} + - {text: "Are any components visibly broken?", value: False, requirement: False} + comments: No Comment diff --git a/PREV-VERSION/gui/utils/__init__.py b/CheckInGUI/Configs/__init__.py similarity index 100% rename from PREV-VERSION/gui/utils/__init__.py rename to CheckInGUI/Configs/__init__.py diff --git a/CheckInGUI/Configs/convert_to_yaml.py b/CheckInGUI/Configs/convert_to_yaml.py new file mode 100644 index 00000000..819dc9aa --- /dev/null +++ b/CheckInGUI/Configs/convert_to_yaml.py @@ -0,0 +1,26 @@ +#This script will convert any python dictionaries you want into yaml +#visit dumptoyaml.py for doing this within another python script + +import yaml + +#Import any dictionary files you want to convert to yaml here +from Engine_cfg import masterCfg as master_engine +from Wagon_cfg import masterCfg as master_wagon + +#Create a dictionary with the names of the yaml files you want to create as keys +#and the dictionary files as the values +#Only put alphanumeric characters into the files name, no spaces +py_files = { + 'Engine_cfg': master_engine, + 'Wagon_cfg': master_wagon, + } + +write_yaml(py_files) + +#This function can be passed a dictionary from other functions and will write a yaml file with that dictionary +def write_yaml(dictionary): + #This for loop iterates over all entries in the dictionary and creates a corresponding yaml file + for i in dictionary: + file = open(i + '.yaml', 'w') + yaml.dump(dictionary[i], file) + file.close() diff --git a/CheckInGUI/Configs/dumpToYaml.py b/CheckInGUI/Configs/dumpToYaml.py new file mode 100644 index 00000000..67ceab7d --- /dev/null +++ b/CheckInGUI/Configs/dumpToYaml.py @@ -0,0 +1,17 @@ +import yaml + +from glob import glob + +def dump_to_yaml(mastercfg): + + yaml_string = yaml.dump(mastercfg) + + + with open("temp.yaml", "w") as f: + + f.write(yaml_string) + + f.close() + + + diff --git a/CheckInGUI/MainFunctionVI.py b/CheckInGUI/MainFunctionVI.py new file mode 100644 index 00000000..da2c0939 --- /dev/null +++ b/CheckInGUI/MainFunctionVI.py @@ -0,0 +1,99 @@ +# Need to make the log file path before any imports +import os +guiLogPath = "/home/{}/GUILogs/".format(os.getlogin()) + +if not os.path.exists(guiLogPath): + os.makedirs(guiLogPath) + +import sys +sys.path.append("..") + +# Imports the GUIWindow +from PythonFiles.GUIWindow import GUIWindow +import socket +import logging +import logging.handlers +import yaml +from pathlib import Path + +logger = logging.getLogger("HGCAL_VI") +logger.setLevel(logging.DEBUG) + +if not logger.handlers: + fh = logging.handlers.TimedRotatingFileHandler(guiLogPath + "checkin_gui.log", when="midnight", interval=1) + fh.setLevel(logging.DEBUG) + + ch = logging.StreamHandler() + ch.setLevel(logging.ERROR) + + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + logger.addHandler(fh) + logger.addHandler(ch) + +class StreamToLogger(object): + def __init__(self, logger, level): + self.logger = logger + self.level = level + self.buffer = '' + + def write(self, message): + if message != '\n': + self.logger.log(self.level, message.strip()) + + def flush(self): + pass + +sys.stdout = StreamToLogger(logger, logging.DEBUG) +#sys.stderr = StreamToLogger(logger, logging.ERROR) + +def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + +sys.excepthook = handle_exception + + +# Creates a main function to initialize the GUI +def main(): + logger.info('Creating new instance of HGCAL_VI') + + filepath = os.path.dirname(os.path.abspath(__file__)) + logger.info("Current path is: %s" % filepath) + + node = socket.gethostname() + logger.info("Node is: %s" % node) + + wagon_GUI_computers = [ + "cmsfactory1.cmsfactorynet", + "cmsfactory2.cmsfactorynet", + "cmsfactory4.cmsfactorynet", + "cmsfactory5.cmsfactorynet", + "cmslab4.umncmslab", + "127.0.1.1", + ] + engine_GUI_computers = [ + + ] + + try: + config_path = sys.argv[1] + except: + config_path = "{}/Configs/Wagon_cfg.yaml".format(filepath) + + masterCfg = import_yaml(config_path) + + main_window = GUIWindow(masterCfg, filepath) + + +def import_yaml(config_path): + + return yaml.safe_load(open(config_path,"r")) + + +if __name__ == "__main__": + main() diff --git a/CheckInGUI/PythonFiles/Data/DBSender.py b/CheckInGUI/PythonFiles/Data/DBSender.py new file mode 100644 index 00000000..66e1f64c --- /dev/null +++ b/CheckInGUI/PythonFiles/Data/DBSender.py @@ -0,0 +1,314 @@ +import requests +import logging +import json +import socket +# from read_barcode import read_barcode + +logger = logging.getLogger('HGCAL_VI.PythonFiles.Data.DBSender') + +class DBSender(): + + def __init__(self, gui_cfg): + self.gui_cfg = gui_cfg + + # Predefined URL for the database + self.db_url = self.gui_cfg.getDBInfo("baseURL") + + # If True, use database + # If False, run in "offline" mode + self.use_database = self.gui_cfg.get_if_use_DB() + + + + # Since we will have the tester in a separate room, we need to do modify the http requests + # This proxy will be used to make http requests directly to cmslab3 via an ssh tunnel + def getProxies(self): + if (self.use_database): + if "umncmslab" in socket.gethostname(): + return None + + return {"http": "http://127.0.0.1:8080"} + + # If not using the database, then... + else: + pass + + def add_new_user_ID(self, user_ID, passwd): + + if (self.use_database): + + try: + r = requests.post('{}/add_tester2.py'.format(self.db_url), data= {'person_name':user_ID, 'password': passwd}) + except Exception as e: + logger.error("Unable to add the user to the database. Username: {}. Check to see if your password is correct.".format(user_ID)) + logger.debug(r.text) + + + # If not using the database, use this... + else: + pass + + def decode_label(self, full_id): + + if len(full_id) != 15: + logger.warning("Invalid label scanned") + label_info = None + else: + r = requests.post('{}/decode_label.py'.format(self.db_url), data={'label': full_id}) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `decode_label.py`. Check that the label library has been updated for the web API.") + logger.debug(r.text) + + temp = [] + + for i in range(begin, end): + temp.append(lines[i]) + + label_info = {'Major Type': temp[0], 'Subtype': temp[1], 'SN': temp[2]} + + return label_info + + + + # Returns an acceptable list of usernames from the database + def get_usernames(self): + if (self.use_database): + url = '{}/get_usernames.py'.format(self.db_url) + r = requests.get(url) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `get_usernames.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + usernames = [] + + for i in range(begin, end): + temp = lines[i] + usernames.append(temp) + + return usernames + + # If not using database... + else: + + return ['User1', 'User2', 'User3'] + + + + # Returns a list of booleans + # Whether or not DB has passing results + def get_previous_test_results(self, full_id): + + r = requests.post('{}/get_previous_test_results.py'.format(self.db_url), data={'full_id': str(full_id)}) + lines = r.text.split('\n') + + try: + begin1 = lines.index("Begin1") + 1 + end1 = lines.index("End1") + begin2 = lines.index("Begin2") + 1 + end2 = lines.index("End2") + begin3 = lines.index("Begin3") + 1 + end3 = lines.index("End3") + except: + logger.error("There was an issue with the web API script `get_previous_test_results.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + tests_run = [] + outcomes = [] + poss_tests = [] + + for i in range(begin1, end1): + tests_run.append(lines[i]) + for i in range(begin2, end2): + outcomes.append(lines[i]) + for i in range(begin3, end3): + poss_tests.append(lines[i]) + + tests_passed = [] + for i in range(len(tests_run)): + tests_passed.append([tests_run[i], outcomes[i]]) + + return tests_passed, poss_tests + + + + # Posts a new board with passed in full id + def add_new_board(self, full, user_id, comments, manufacturer): + r = requests.post('{}/add_module2.py'.format(self.db_url), data={"full_id": str(full), 'manufacturer': manufacturer, "location" :"UMN"}) + try: + lines = r.text.split('\n') + + begin = lines.index("Begin") + 1 + end = lines.index("End") + + for i in range(begin, end): + logger.debug(lines[i]) + + except: + logger.error("There was an issue with the web API script `add_module2.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + + r = requests.post('{}/board_checkin2.py'.format(self.db_url), data={"full_id": str(full), 'person_id': str(user_id), 'comments': str(comments)}) + + try: + lines = r.text.split('\n') + + begin = lines.index("Begin") + 1 + end = lines.index("End") + + in_id = None + + for i in range(begin, end): + in_id = lines[i] + except: + logger.error("There was an issue with the web API script `board_checkin2.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + in_id = None + + return in_id + + + def is_new_board(self, full): + r = requests.post('{}/is_new_board.py'.format(self.db_url), data={"full_id": str(full)}) + + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `is_new_board.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + in_id = lines[end+1][1:lines[end+1].find(",")] + + + for i in range(begin, end): + + if lines[i] == "True": + return True, None + elif lines[i] == "False": + return False, in_id + + def check_for_ldo(self, engine): + r = requests.post('{}/check_for_ldo.py'.format(self.db_url), data={"full_id": str(engine)}) + + try: + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `check_for_ldo`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + + for i in range(begin, end): + got_code = lines[i] + + except: + got_code = None + + return got_code + + + def get_manufacturers(self): + r = requests.post('{}/get_manufacturers.py'.format(self.db_url)) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `get_manufacturers.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + manufacturers = [] + for i in range(begin, end): + manufacturers.append(lines[i]) + + return manufacturers + + def add_component(self, barcode, full_id): + r = requests.post('{}/add_component.py'.format(self.db_url), data = {'barcode': barcode, 'full_id': full_id}) + + def get_manufacturer_from_code(self, code): + r = requests.post('{}/get_manufacturer_from_code.py'.format(self.db_url), data = {'code': code}) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `get_manufacturer_from_code.py`. Check that the database contains this manufacturer code.") + logger.debug(r.text) + + for i in range(begin, end): + return lines[i] + + + + def add_test_json(self, json_file, datafile_name, full_id): + load_file = open(json_file) + results = json.load(load_file) + load_file.close() + + datafile = open(datafile_name, "rb") + + attach_data = {'attach1': datafile} + + r = requests.post('{}/add_test_json.py'.format(self.db_url), data = results, files = attach_data) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + + for i in range(begin, end): + return lines[i] + except: + logger.error("There was an issue uploading the test.") + logger.debug(r.text) + + return None + + + # Returns a list of all different types of tests + def get_test_list(self): + if (self.use_database): + r = requests.get('{}/get_test_types.py'.format(self.db_url)) + + lines = r.text.split('\n') + + begin = lines.index("Begin") + 1 + end = lines.index("End") + + tests = [] + + for i in range(begin, end): + temp = lines[i][1:-1].split(",") + temp[0] = str(temp[0][1:-1]) + temp[1] = int(temp[1]) + tests.append(temp) + + return tests + + else: + + blank_tests = [] + for i in enumerate(self.gui_cfg.getNumTest()): + blank_tests.append("Test{}".format(i)) + + return blank_tests + diff --git a/CheckInGUI/PythonFiles/Data/DataHolder.py b/CheckInGUI/PythonFiles/Data/DataHolder.py new file mode 100644 index 00000000..1dee008f --- /dev/null +++ b/CheckInGUI/PythonFiles/Data/DataHolder.py @@ -0,0 +1,322 @@ +################################################################################ +import json, logging, socket, PythonFiles, copy, os +from PythonFiles.Data.DBSender import DBSender +from PythonFiles.update_config import update_config +from collections import OrderedDict +import yaml +from pathlib import Path + +logger = logging.getLogger('HGCAL_VI.PythonFiles.Data.DataHolder') + +class DataHolder(): + + ################################################# + + # List of the variables being held by data holder + def __init__(self, gui_cfg): + + # Object for taking care of instantiation for different test types + self.gui_cfg = gui_cfg + + # Object that sends information to the database + self.data_sender = DBSender(gui_cfg) + + self.data_dict = { + 'user_ID': "_", + 'test_stand': str(socket.gethostname()), + 'current_full_ID': "-1BAD", + 'comments': "_", + 'in_id': None, + 'is_new_board': False, + 'test_names': '', + 'prev_results': '', + 'manufacturer': 'None', + } + + # for the visual inspection component + self.inspection_data = OrderedDict({ + 'board_bent': False, + 'board_broken': False, + 'component_missing': False, + 'component_broken': False, + 'inspection_comments': '_' + }) + + # All of the checkbox logic + # Dictionaries stored by inspection index + self.all_checkboxes = [] + + self.label_info = None + + for index in range(self.gui_cfg.getNumInspections()): + self.all_checkboxes.append(self.gui_cfg.getCheckDict(index)) + + # All of the comments logic + # Dictionaries stored by inspection index + self.all_comments = [] + + for index in range(self.gui_cfg.getNumInspections()): + self.all_comments.append(self.gui_cfg.getCommentDict(index)) + + ################################################# + + def import_yaml(self, filename): + return yaml.safe_load(filename) + + def get_check_dict(self, idx): + return self.all_checkboxes[idx] + + def set_check_dict(self, idx, value): + self.all_checkboxes[0][idx]['value'] = value + self.inspection_data[list(self.inspection_data.keys())[idx]] = value + + def get_comment_dict(self, idx): + return self.all_comments[idx] + + def set_comment_dict(self, idx, val): + self.all_comments[idx] = val + + def add_new_user_name(self, user_ID, passwd): + self.data_dict['user_ID'] = user_ID + + is_new_user_ID = True + + for item in self.get_all_users(): + if self.data_dict['user_ID'] == item: + is_new_user_ID = False + + if is_new_user_ID: + self.data_sender.add_new_user_ID(self.data_dict['user_ID'], passwd) + + def get_manufacturers(self): + return self.data_sender.get_manufacturers() + + def get_manufacturer_from_batch(self, major, batch, code=None): + if major == 'LD-Engine' or major == 'HD-Engine' or major == 'Zipper Board': + x = self.import_yaml(open(Path(__file__).parent / "Engine_batches.yaml")) + elif major == 'HD-Wagon': + x = self.import_yaml(open(Path(__file__).parent / "HD_Wagon_batches.yaml")) + + return x[code][int(batch)] + + def get_manufacturer_from_code(self, code): + return self.data_sender.get_manufacturer_from_code(code) + + def set_manufacturer_id(self, manufacturer): + self.data_dict['manufacturer'] = manufacturer + + def add_component(self, barcode): + self.data_sender.add_component(barcode, self.get_full_ID()) + + + # when a board gets entered, this function checks if it's new + def check_if_new_board(self): + logger.info("Checking if full id is a new board...") + + full = self.get_full_ID() + #returns true if the board is new, false if not + is_new_board, in_id = self.data_sender.is_new_board(full) + + comments = self.data_dict['comments'] + self.data_dict['is_new_board'] = is_new_board + + if is_new_board == True: + logger.info('Board is new, checking it in.') + user = self.data_dict['user_ID'] + # data sender's add new board function returns the check in id + self.data_dict['in_id'] = self.data_sender.add_new_board(full, user, comments, self.data_dict['manufacturer']) + + else: + # if the board is not new, this returns the previous testing information on the board + logger.info('Board has already been checked in, getting previous results.') + prev_results, test_names = self.data_sender.get_previous_test_results(full) + if prev_results: + self.data_dict['test_names'] = test_names + self.data_dict['prev_results'] = prev_results + else: + self.data_dict['test_names'] = None + self.data_dict['prev_results'] = 'No tests have been run on this board.' + self.data_dict['in_id'] = in_id + + return self.data_dict['in_id'] + + def decode_label(self, full_id): + self.label_info = self.data_sender.decode_label(full_id) + + def check_for_ldo(self): + got_code = self.data_sender.check_for_ldo(self.get_full_ID()) + return got_code + + ################################################# + + + def set_user_ID(self, user_ID): + + self.data_dict['user_ID'] = user_ID + logger.info("User ID set to %s" % user_ID) + + ################################################## + + def set_comments(self, comments): + + self.data_dict['comments'] = comments + logger.info("Comments have been entered.") + + def set_inspection_comments(self, comments): + + self.inspection_data['inspection_comments'] = comments + logger.info('Comments entered for VI test') + + ################################################## + + def set_full_ID(self, full): + if full[3] != self.data_dict['current_full_ID'][3]: + new_cfg = update_config(full) + self.gui_cfg = new_cfg + + self.data_sender = DBSender(self.gui_cfg) + + self.data_dict['current_full_ID'] = full + self.data_holder_new_test() + logging.info("Full ID set to %s" % full) + + def send_image(self, img_idx=0): + self.data_sender.add_board_image(self.data_dict["current_full_ID"], open(self.image_data[img_idx], "rb")) + + ################################################# + + # current method to send to the database + def send_to_DB(self): + test_name = "Visual Inspection" + test_type_id = 0 + + logger.info('Uploading visual inspection results...') + info_dict = {"full_id":self.get_full_ID(),"tester": self.data_dict['user_ID'], "test_type": test_name, "successful": self.data_dict["inspection_pass"], "comments": self.data_dict['comments']} + logger.debug(info_dict) + json_dir = Path.home()/ "JSONFiles" + json_dir.mkdir(exist_ok=True, parents=True) + json_dir = str(json_dir.parent.absolute()) + with open("{}/JSONFiles/storage.json".format(json_dir), "w") as outfile: + json.dump(info_dict, outfile) + + with open("{}/JSONFiles/data.json".format(json_dir), "w") as outfile: + json.dump(self.inspection_data, outfile) + + test_id = self.data_sender.add_test_json("{}/JSONFiles/storage.json".format(json_dir), "{}/JSONFiles/data.json".format(json_dir), self.get_full_ID()) + + logging.info("Test results sent to database.") + + return test_id + + ################################################# + + # sends the visual inspection json comments to the database + def update_from_json_string(self): + + test_type = "Visual Inspection" + test_type_id = 0 + + passed = not any([x for x in self.inspection_data.values()][:-1]) + + self.data_dict['inspection_completed'] = True + self.data_dict['inspection_pass'] = int(passed) + fid = self.get_full_ID() + if "320EL" in fid or "320EH" in fid: + got_code = self.check_for_ldo() + if got_code is None or got_code == "None": + self.data_dict['inspection_pass'] = 0 + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " no LDO scanned." + + + #self.add_inspection_to_comments() + self.data_dict['data'] = self.inspection_data + + ################################################ + + def add_inspection_to_comments(self): + if self.inspection_data['board_bent']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Board is chipped or bent." + if self.inspection_data['board_broken']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Board is visibly broken" + if self.inspection_data['component_missing']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Board is missing components." + if self.inspection_data['component_broken']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Board component is broken." + #if self.inspection_data['inspection_comments'] != "_": + # if self.data_dict['comments'] == "_": + # self.data_dict['comments'] = "" + # self.data_dict['comments'] = self.data_dict['comments'] + " User comments: " + self.inspection_data['inspection_comments'] + # self.data_dict['inspection_comments'] = self.inspection_data['inspection_comments'] + + + ################################################## + + def get_full_ID(self): + return self.data_dict['current_full_ID'] + + ################################################# + + def get_all_users(self): + users_list = self.data_sender.get_usernames() + return users_list + + ################################################# + + # Tracking the test index in another place and propagating to the config + def setTestIdx(self, test_idx): + + self.current_test_idx = test_idx + self.gui_cfg.setTestIndex(self.current_test_idx) + + def getTestNames(self): + return self.gui_cfg.getTestNames() + + ################################################ + + # Keeps the login information stored + def data_holder_new_test(self): + + self.data_dict = { + 'user_ID': self.data_dict['user_ID'], + 'test_stand': str(socket.gethostname()), + 'current_full_ID': self.get_full_ID(), + 'comments': "_", + 'is_new_board': False, + 'test_names': '', + 'prev_results': '', + 'in_id': None, + 'manufacturer': self.data_dict['manufacturer'], + } + + self.gui_cfg.setTestIndex(1) + + self.current_test_idx = self.gui_cfg.getTestIndex() + + self.data_dict["inspection_completed"] = False + self.data_dict["inspection_pass"] = False + + self.inspection_data = { + 'board_bent': False, + 'board_broken': False, + 'component_missing': False, + 'component_broken': False, + 'inspection_comments': "_" + } + + logger.info("DataHolder Information has been reset for a new test.") + + ################################################ + +################################################################################# + + diff --git a/CheckInGUI/PythonFiles/Data/Engine_batches.yaml b/CheckInGUI/PythonFiles/Data/Engine_batches.yaml new file mode 100755 index 00000000..a16405af --- /dev/null +++ b/CheckInGUI/PythonFiles/Data/Engine_batches.yaml @@ -0,0 +1,28 @@ +--- +EL0QE: + 1: Sunshine-Caltronics +EL0QW: + 1: Sunshine-Caltronics +EL10E: + 1: TTM-Caltronics + 2: TTM-Caltronics + 3: TTM-Piranha +EL10W: + 1: TTM-Caltronics + 2: TTM-Caltronics + 3: TTM-Piranha +EH0QF: + 1: APCT-Caltronics +EH0QH: + 1: APCT-Caltronics +EH10F: + 1: TTM-Caltronics + 2: TTM-Caltronics +EH10H: + 1: TTM-Piranha + 2: TTM-Piranha +ZPLMEZ: + 1: TTM-Piranha +ZPLMZ2: + 2: Sunshine-Caltronics + diff --git a/CheckInGUI/PythonFiles/Data/HD_Wagon_batches.yaml b/CheckInGUI/PythonFiles/Data/HD_Wagon_batches.yaml new file mode 100755 index 00000000..4d6b79ca --- /dev/null +++ b/CheckInGUI/PythonFiles/Data/HD_Wagon_batches.yaml @@ -0,0 +1,34 @@ +--- +WH20A0: + 1: JLCPCB-Evotronics +WH21A0: + 1: JLCPCB-Evotronics +WH30A0: + 2: JLCPCB-Caltronics +WH30B0: + 1: JLCPCB-Evotronics +WH30C0: + 1: JLCPCB-Evotronics +WH30D0: + 1: JLCPCB-Evotronics +WH31A0: + 1: JLCPCB-Evotronics +WH31B0: + 1: JLCPCB-Evotronics +WH-21AT: + 2: Sunshine-Evotronics +WH-21AD: + 2: Sunshine-Evotronics +WH-31AT: + 2: Sunshine-Evotronics +WH-31AD: + 2: Sunshine-Evotronics +WH-30BT: + 2: Sunshine-Evotronics +WH-30BD: + 2: Sunshine-Evotronics +WH-30DT: + 2: Sunshine-Evotronics +WH-30DD: + 2: Sunshine-Evotronics + diff --git a/CheckInGUI/PythonFiles/Data/__init__.py b/CheckInGUI/PythonFiles/Data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CheckInGUI/PythonFiles/GUIConfig.py b/CheckInGUI/PythonFiles/GUIConfig.py new file mode 100644 index 00000000..2a23aaa7 --- /dev/null +++ b/CheckInGUI/PythonFiles/GUIConfig.py @@ -0,0 +1,90 @@ +import logging + +logger = logging.getLogger('HGCAL_VI') +# Class to handle creation of different types of GUIs based on which board we want to test +# This class will hold all of the frame information and order them accordingly + + +# Responsible for interfacing with the configuration file +class GUIConfig(): + + # Loads in a config file with board type name + # Information about board tests and database are stored within the config + def __init__(self, board_cfg): + self.board_cfg = board_cfg + self.current_idx = 1 + + self.configure() + + + # Create the GUI instance based off testing information + def configure(self): + + # Possibly do something special here if need be + + logger.info("Instance of {} GUI created.".format(self.getGUIType())) + + + # Get serial check safe attribute + def getSerialCheckSafe(self): + return self.board_cfg["SerialCheckSafe"] + + def getUseScanner(self): + return self.board_cfg["UsingScanner"] + + def getNumInspections(self): + return len(self.board_cfg['InspectionTest']) + + def getCheckDict(self, inspect_num): + return self.board_cfg["InspectionTest"][inspect_num]['checkboxes'] + + def getCommentDict(self, inspect_num): + return self.board_cfg["InspectionTest"][inspect_num]['comments'] + + + # Get database info for getting and posting test results + def getDBInfo(self, key=None): + if key is None: + return self.board_cfg["DBInfo"] + else: + return self.board_cfg["DBInfo"][key] + + # Returns true if the database should be used + def get_if_use_DB(self): + return self.board_cfg['DBInfo']['use_database'] + + def getGUIType(self): + return self.board_cfg["GUIType"] + + def getTestHandler(self): + return self.board_cfg["TestHandler"] + + def getUsers(self): + return self.board_cfg["People"] + + ################################ + + # Necessary to be stored for the current test + # This allows for more advanced test navigation + + def setTestIndex(self, idx): + self.current_idx = idx + + def getTestIndex(self): + return self.current_idx + + ################################ + + # Returns the names for the physical tests from config + def getPhysicalNames(self): + try: + return [test["name"] for test in self.board_cfg["PhysicalTest"]] + except: + return [] + + def getTestNames(self): + try: + return [test["name"] for test in self.board_cfg["Test"]] + except: + logger.debug("Unable to return test names. Check to see if test['name'] is empty") + return [] diff --git a/CheckInGUI/PythonFiles/GUIWindow.py b/CheckInGUI/PythonFiles/GUIWindow.py new file mode 100644 index 00000000..13428c41 --- /dev/null +++ b/CheckInGUI/PythonFiles/GUIWindow.py @@ -0,0 +1,437 @@ +################################################################################# + +# Importing all neccessary modules +from pickle import NONE +import tkinter as tk +from tkinter import ttk +from turtle import bgcolor +from PythonFiles.GUIConfig import GUIConfig +from PythonFiles.Data.DataHolder import DataHolder +from PythonFiles.Scenes.LoginScene import LoginScene +from PythonFiles.Scenes.ScanScene import ScanScene +from PythonFiles.Scenes.SplashScene import SplashScene +from PythonFiles.Scenes.SummaryScene import SummaryScene +from PythonFiles.Scenes.AddUserScene import AddUserScene +from PythonFiles.Scenes.PostScanScene import PostScanScene +from PythonFiles.Scenes.ComponentScanScene import ComponentScanScene +from PythonFiles.Scenes.InspectionScenes.Inspection1 import Inspection1 +from PythonFiles.update_config import update_config +import logging +import os +import PythonFiles + +logger = logging.getLogger('HGCAL_VI.PythonFiles.GUIWindow') + + +################################################################################# + + +# Create a class for creating the basic GUI Window to be called by the main function to +# instantiate the actual object +class GUIWindow(): + + ################################################# + + def __init__(self, board_cfg, main_path): + # Create the window named "master_window" + # global makes master_window global and therefore accessible outside the function + global master_window + master_window = tk.Tk() + self.master_window = master_window + self.master_window.report_callback_exception = self.log_callback_exception + + master_window.title("Board Check In and Inspection") + + # Creates the size of the window and disables resizing + master_window.geometry("1350x850+25+100") + master_window.pack_propagate(1) + + #resizing master_frame, keeping sidebar same width + master_window.grid_columnconfigure(0, weight=0) # Make the sidebar resizable + master_window.grid_columnconfigure(1, weight=1) # Make the master frame resizable + master_window.grid_rowconfigure(0, weight=1) + + # Variables necessary for the help popup + self.all_text = "No help available for this scene." + self.label_text = tk.StringVar() + + self.main_path = main_path + + # Following line prevents the window from being resizable + # master_window.resizable(0,0) + + # Removes the tkinter logo from the window + # master_window.wm_attributes('-toolwindow', 'True') + + # Creates and packs a frame that exists on top of the master_frame + master_frame = tk.Frame(master_window, width = 1350, height = 850) + master_frame.grid(column = 0, row = 0, columnspan = 4, sticky="nsew") + + # Object for taking care of instantiation of different test types + # don't need the update config for this GUI + # DBSender takes care of differentiating between boards + self.gui_cfg = GUIConfig(board_cfg) + + # Creates the "Storage System" for the data during testing + self.data_holder = DataHolder(self.gui_cfg) + + ################################################# + # Creates all the different frames in layers # + ################################################# + + # At top so it can be referenced by other frames' code... Order of creation matters + self.summary_frame = SummaryScene(self, master_frame, self.data_holder) + self.summary_frame.grid(row=0, column=0, sticky="nsew") + + self.login_frame = LoginScene(self, master_frame, self.data_holder) + self.login_frame.grid(row=0, column=0, sticky='nsew') + + self.scan_frame = ScanScene(self, master_frame, self.data_holder) + self.scan_frame.grid(row=0, column=0, sticky='nsew') + + self.add_user_frame = AddUserScene(self, master_frame, self.data_holder) + self.add_user_frame.grid(row=0,column=0, sticky='nsew') + + self.inspection_frame = Inspection1(self, master_frame, self.data_holder) + self.inspection_frame.grid(row=0,column=0, sticky='nsew') + + self.component_scan_frame = ComponentScanScene(self, master_frame, self.data_holder) + self.component_scan_frame.grid(row=0, column=0, sticky='nsew') + + self.post_scan_frame = PostScanScene(self, master_frame, self.data_holder) + self.post_scan_frame.grid(row=0, column=0, sticky='nsew') + + # Near bottom so it can reference other frames with its code + self.splash_frame = SplashScene(self, master_frame) + self.splash_frame.grid(row=0, column=0, sticky='nsew') + + logger.info('All frames have been created.') + + ################################################# + # End Frame Creation # + ################################################# + + # Tells the master window that its exit window button is being given a new function + master_window.protocol('WM_DELETE_WINDOW', self.exit_function) + + # Sets the current frame to the splash frame + self.set_frame_splash_frame() + + master_frame.after(500, self.set_frame_login_frame) + + master_window.mainloop() + + def log_callback_exception(self, exc_type, exc_value, exc_traceback): + logger.error("Exception in Tkinter callback", exc_info=(exc_type, exc_value, exc_traceback)) + + + + ################################################# + + def update_config(self): + #switch between Wagon and Engine config depending on the full id entered + full = self.data_holder.get_full_ID() + if not self.gui_cfg.getSerialCheckSafe(): + return + new_cfg = update_config(full) + self.gui_cfg = new_cfg + + ################################################ + + def set_frame_login_frame(self): + logger.info('Setting frame to login_frame') + self.login_frame.update_frame(self) + self.set_frame(self.login_frame) + + + ################################################# + + def set_frame_component_frame(self): + logger.info('Setting frame to component_scan_frame') + self.component_scan_frame.is_current_scene = True + self.component_scan_frame.update() + self.component_scan_frame.start() + self.set_frame(self.component_scan_frame) + + ################################################# + + def set_frame_scan_frame(self): + logger.info('Setting frame to scan_frame') + self.scan_frame.is_current_scene = True + self.scan_frame.update() + self.set_frame(self.scan_frame) + self.scan_frame.scan_QR_code(master_window) + + ################################################# + + def set_frame_inspection_frame(self): + logger.info('Setting frame to inspection_frame') + self.inspection_frame.update_frame(self) + self.set_frame(self.inspection_frame) + + ################################################# + + def set_frame_postscan(self): + logger.info('Setting frame to postscan_frame') + + self.post_scan_frame.update_frame() + self.set_frame(self.post_scan_frame) + + ################################################# + + def set_frame_splash_frame(self): + logger.info('Setting frame to splash_frame') + + self.set_frame(self.splash_frame) + + ################################################# + + def set_frame_summary(self): + logger.info('Setting frame to summary_frame') + self.summary_frame.update_frame() + self.set_frame(self.summary_frame) + + def set_frame_add_user_frame(self): + logger.info('Setting frame to add_user_frame') + self.set_frame(self.add_user_frame) + + ################################################# + + # Called to change the frame to the argument _frame + def set_frame(self, _frame): + + ############################################################################# + # The Following Code Determines What Buttons Are Visible On The Side Bar # + ############################################################################# + + #Binding return button to next frame + try: + bind_func = _frame.get_submit_action() + _frame.bind_all("", lambda event: bind_func(_frame.get_parent())) + except: + logger.warning("No bind function for " + str(_frame)) + + + # Hides the submit button on scan frame until an entry is given to the computer + if (_frame is not self.scan_frame): + self.scan_frame.is_current_scene = False + self.scan_frame.hide_submit_button() + + self.set_help_text(_frame) + ############################################################################# + # End Button Visibility Code # + ############################################################################# + + # Raises the passed in frame to be the current frame + _frame.tkraise() + + ################################################# + + + # New function for clicking on the exit button + def exit_function(self): + + # Creates a popup to confirm whether or not to exit out of the window + self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') + self.popup.title("Exit Window") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + + # Creates frame in the new window + frm_popup = tk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = tk.Label( + frm_popup, + text = "Are you sure you would like to exit?", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) + + # Creates yes and no buttons for exiting + btn_yes = tk.Button( + frm_popup, + width = 12, + height = 2, + text = "Yes", + relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.destroy_function() + ) + btn_yes.grid(column = 0, row = 1) + + btn_no = tk.Button( + frm_popup, + width = 12, + height = 2, + text = "No", + relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.destroy_popup() + ) + btn_no.grid(column = 1, row = 1) + + + + + ################################################# + + # Called when the no button is pressed to destroy popup and return you to the main window + def destroy_popup(self): + try: + self.popup.destroy() + except: + logger.error("GUIWindow: Unable to close the popup") + ################################################# + + def remove_all_widgets(self): + self.add_user_frame.remove_widgets(self) + + ################################################# + + + def help_popup(self, current_window): + + logger.info("The user has requested a help window") + logger.info("Opening a help menu for {}".format(type(current_window))) + + # Creates a popup to confirm whether or not to exit out of the window + self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') + self.popup.title("Help Window") + self.popup.geometry("650x650+500+300") + #self.popup.grab_set() + + self.mycanvas = tk.Canvas(self.popup, background="#808080", width=630, height =650) + self.viewingFrame = tk.Frame(self.mycanvas, width = 200, height = 200) + self.scroller = ttk.Scrollbar(self.popup, orient="vertical", command=self.mycanvas.yview) + self.mycanvas.configure(yscrollcommand=self.scroller.set) + + + + self.canvas_window = self.mycanvas.create_window((4,4), window=self.viewingFrame, anchor='nw', tags="self.viewingFrame") + + + self.viewingFrame.bind("", self.onFrameConfigure) + self.mycanvas.bind("", self.onCanvasConfigure) + + self.viewingFrame.bind('', self.onEnter) + self.viewingFrame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + + self.set_help_text(current_window) + + # Creates frame in the new window + #frm_popup = tk.Frame(self.mycanvas) + #frm_popup.pack() + + + # Creates label in the frame + lbl_popup = tk.Label( + self.viewingFrame, + textvariable = self.label_text, + font = ('Arial', 11) + ) + lbl_popup.grid(column = 0, row = 0, pady = 5, padx = 50) + + + self.mycanvas.pack(side="right") + self.scroller.pack(side="left", fill="both", expand=True) + + + #btn_ok = tk.Button( + # frm_popup, + # width = 8, + # height = 2, + # text = "OK", + # font = ('Arial', 8), + # relief = tk.RAISED, + # command = lambda: self.destroy_popup() + #) + #btn_ok.grid(column = 0, row = 0) + + + ############################################# + + def set_help_text(self, current_window): + + # Help text from file + file = open("{}/HGCAL_Help/{}_help.txt".format(PythonFiles.__path__[0], type(current_window).__name__)) + self.all_text = file.read() + + + self.label_text.set(self.all_text) + + + + ################################################# + + def onFrameConfigure(self, event): + '''Reset the scroll region to encompass the inner frame''' + self.mycanvas.configure(scrollregion=self.mycanvas.bbox("all")) #whenever the size of the frame changes, alter the scroll region respectively. + + def onCanvasConfigure(self, event): + '''Reset the canvas window to encompass inner frame when required''' + canvas_width = event.width + self.mycanvas.itemconfig(self, width = canvas_width) #whenever the size of the canvas changes alter the window region respectively. + + + ################################################# + + + def onMouseWheel(self, event): # cross platform scroll wheel event + if event.num == 4: + self.mycanvas.yview_scroll( -1, "units" ) + elif event.num == 5: + self.mycanvas.yview_scroll( 1, "units" ) + + def onEnter(self, event): # bind wheel events when the cursor enters the control + self.mycanvas.bind_all("", self.onMouseWheel) + self.mycanvas.bind_all("", self.onMouseWheel) + + def onLeave(self, event): # unbind wheel events when the cursorl leaves the control + self.mycanvas.unbind_all("") + self.mycanvas.unbind_all("") + + + + ################################################# + + # Called when the yes button is pressed to destroy both windows + def destroy_function(self, event=None): + try: + logging.info("GUIWindow: Exiting the GUI.") + + master_window.update() + self.popup.update() + + self.scan_frame.kill_processes() + + # Destroys the popup and master window + self.popup.destroy() + self.popup.quit() + + master_window.destroy() + master_window.quit() + + + logging.info("The application has exited successfully.") + except Exception as e: + logging.exception(e) + logging.error("The application has failed to close.") + + exit() + + + + + + ################################################ + + +################################################################################# diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/AddUserScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/AddUserScene_help.txt new file mode 100644 index 00000000..a306b4cc --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/AddUserScene_help.txt @@ -0,0 +1,40 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Add User Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Add User Scene can only be accessed via the Login Scene's "Add User" + button. This scene is responsible for adding another user's name into the + database so that the Login Scene's next call to the database will have the + new user's information. + + To add a user, type the user's name into the top text field. Be careful, + this is a case-sensitive field. Then, type in the administrative password + below. This is also a case-sensitive field. After the information has been + entered, press the "Submit" button. This will bring you back to the Login + Scene. + + + Note: If you add a user name and it does not show up on the Login Scene, + try adding the user again. There is a chance that you have entered + the password incorrectly. If you are sure your credentials are + correct, there might be an issue setting/accessing the data stored + in the database. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/ComponentScanScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/ComponentScanScene_help.txt new file mode 100644 index 00000000..34384fc4 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/ComponentScanScene_help.txt @@ -0,0 +1,62 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Scan Scene is displayed after the Login Scene. It also marks the + beginning of testing a new board (either wagon or engine). This scene + is responsible for gathering the serial identification number, which + is often displayed directly on the board via a QR code. + + The submit button (which leads to the next scene) is grayed out until + the serial id box has been filled. This box can be filled in two + different ways. + + The most efficient way to fill the text field is to + scan the box using an EventListener scanner attached to the computer. + The scanner should be attached to the computer that is actively + running the GUI. After successfully scanning a QR code, the scanner + should beep, and the text should be filled into the field. Notice that + the text field will become grayed out after information has been + scanned into it. After scanning, if you would like to change the + serial number entered, you will need to press the "Rescan" button to + reactivate the scanner. + + The scanner runs on a completely separate process from the GUI, which + can cause issues with sections of code not completing correctly. In + some cases, the scanner may be stuck in a constant state of searching + for QR code information. If this is the case, be sure to press the + button that says "Report Bug" and report the issue. + + If the physical scanner is not available, you are also able to manually + enter the serial identification into the box. Note that this will not + require pressing the "Rescan" button nor will it gray out the text field. + + After the entry of the serial ID matches the board that you would like + to test, select the "Submit" button in order to move onto the next scene. + Before moving to the next scene, the GUI will request more information + about your board by sending the serial ID to the database. If the serial + ID is recognized as invalid, errors may occur within the GUI. + + + Note: If the serial ID is recognized and the board has been tested + before, the GUI may skip some of the previously passed tests + in order to save time. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/Inspection1_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/Inspection1_help.txt new file mode 100644 index 00000000..578210e6 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/Inspection1_help.txt @@ -0,0 +1,27 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Inspection Scene 1 + + Date Updated: 7/14/23 + + +---------------------------------------------------------------------------------------- + + This help file has not been written yet. Edit it in the following + text file: + + Inspection1_help.txt + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/LoginScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/LoginScene_help.txt new file mode 100644 index 00000000..27559a93 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/LoginScene_help.txt @@ -0,0 +1,36 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Login Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Login Scene is displayed directly after the Splash Scene as well as + via the "logout" buttons on many scenes. The purpose of this scene is to + have the user log into the system using the credentials stored on the + web database. When this scene is shown, there is a call to the web + database to grab the login options. + + Users must select an option from the list of users provided in the drop- + down box. After selecting the desired user's name, the submit button will + bring you to the next scene, which is the Scan Scene. + + If there is not an option for a specific user, you can select + the "Add User" button to bring you to the Add User Scene. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/PhotoScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/PhotoScene_help.txt new file mode 100644 index 00000000..8d6f1d05 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/PhotoScene_help.txt @@ -0,0 +1,38 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Photo Scene + + Date Updated: 7/17/23 + + +---------------------------------------------------------------------------------------- + + + The Photo Scene is viewed after the Camera Scene on the Visual + Inspection GUI. Its sole purpose is to allow the user to confirm the + picture taken by the previous scene. + + You are able to submit the photo that is displayed on the Photo Scene + to the database by pressing the submit button. If you are not satisfied + with the quality of the photo, you can press "Try Again" to bring you + back to the Camera Scene. + + Note: If the Photo Scene displays a picture that looks nothing like + what was shown on the Camera Scene preview, please make sure that you + press the "Snapshot" button on the previous sceen. If you have done so, + then there is most likely an issue with the saving process behind the + "Snapshot" button. Please reach out for assistance if that is the case. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/PostScanScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/PostScanScene_help.txt new file mode 100644 index 00000000..f6d1ab51 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/PostScanScene_help.txt @@ -0,0 +1,25 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Summary Scene + + Date Updated: 9/26/23 + + +---------------------------------------------------------------------------------------- + + The Scan Summary Scene gives information about the board: whether it has + been checked in, whether it has had tests run on it, etc. Once you are sure + the information displayed is correct, click Proceed. + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/ScanScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/ScanScene_help.txt new file mode 100644 index 00000000..34384fc4 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/ScanScene_help.txt @@ -0,0 +1,62 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Scan Scene is displayed after the Login Scene. It also marks the + beginning of testing a new board (either wagon or engine). This scene + is responsible for gathering the serial identification number, which + is often displayed directly on the board via a QR code. + + The submit button (which leads to the next scene) is grayed out until + the serial id box has been filled. This box can be filled in two + different ways. + + The most efficient way to fill the text field is to + scan the box using an EventListener scanner attached to the computer. + The scanner should be attached to the computer that is actively + running the GUI. After successfully scanning a QR code, the scanner + should beep, and the text should be filled into the field. Notice that + the text field will become grayed out after information has been + scanned into it. After scanning, if you would like to change the + serial number entered, you will need to press the "Rescan" button to + reactivate the scanner. + + The scanner runs on a completely separate process from the GUI, which + can cause issues with sections of code not completing correctly. In + some cases, the scanner may be stuck in a constant state of searching + for QR code information. If this is the case, be sure to press the + button that says "Report Bug" and report the issue. + + If the physical scanner is not available, you are also able to manually + enter the serial identification into the box. Note that this will not + require pressing the "Rescan" button nor will it gray out the text field. + + After the entry of the serial ID matches the board that you would like + to test, select the "Submit" button in order to move onto the next scene. + Before moving to the next scene, the GUI will request more information + about your board by sending the serial ID to the database. If the serial + ID is recognized as invalid, errors may occur within the GUI. + + + Note: If the serial ID is recognized and the board has been tested + before, the GUI may skip some of the previously passed tests + in order to save time. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/SelectScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/SelectScene_help.txt new file mode 100644 index 00000000..63816295 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/SelectScene_help.txt @@ -0,0 +1,26 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Select Check Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Select Check Scene is displayed after the Login Scene. The purpose of this + scene is to select whether boards are being checked in or out of the lab. + Users must select an option provided in the drop-down box. After selecting the + correct option, the submit button will bring you to the next scene, which is the + Scan Scene. + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/SplashScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/SplashScene_help.txt new file mode 100644 index 00000000..6a3c91a7 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/SplashScene_help.txt @@ -0,0 +1,36 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Splash Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Splash Scene is displayed when the graphical user interface (GUI) is first + ran. The information provides credit to the students largely responsible + for the creation of this interface. The three students listed, along with + their home institutions are as follows: + + - Bryan Crossman (University of Minnesota) + - Andrew Kirzeder (Bethel University) + - Garrett Schindler (Bethel University) + + This scene should only be displayed for a short period of time before + moving on to the next scene. The next scene should be the Login Scene. + No buttons are required to be pressed on this screen. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/SummaryScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/SummaryScene_help.txt new file mode 100644 index 00000000..04cf6d44 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/SummaryScene_help.txt @@ -0,0 +1,50 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test Summary Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Test Summary Scene is responsible for showing all of the results that + the GUI has gathered. The information is stored in a DataHolder object + while it is gathered throughout the process. The Test Summary Scene + displays the attributes of the DataHolder object to the user to ensure + that all of the information has been stored correctly. + + The top of the screen should display the tester's name as well as the + serial identification number for the board that is being tested. + + Each of the tests that are listed for the board are displayed below. The + GUI keeps track of whether or not certain tests have been completed. + The information displayed for each test is the "Test Name", "Test Status", + and "Pass/Fail". These display which type of test, whether or not it has + been run, and whether or not it has passed the test. Notice that a pass + is signified by a green checkmark and a fail is signified by a red x. + + To the right of the displayed information are two buttons; "More Info" and + "Retest". The "More Info" button will open a popup window with JSON + information that the tester has sent back to the GUI. If you want to know + more about the specific test results, press this button. The "Retest" + button will send you back to the Test Scene for the designated test. + Upon finishing this retest, the GUI will send you back to the Test + Summary Scene. + + Upon reaching the Test Summary Scene, the DataHolder object is uploaded to + the database. When you complete a retest for a specified board, the + information stored in the database for that board will be updated. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt new file mode 100644 index 00000000..7b454595 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt @@ -0,0 +1,53 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test In Progress Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Test In Progress Scene is displayed while you are running a test on + the testing machine. When working correctly, the test machine will send + dialog back to the GUI describing the current testing process. This type + of logging is run through a connection queue. + + If the GUI does not receive an initial response from the test stand after + 10 seconds, the GUI will throw a timeout error (popup window). This error + message will say "TestInProgressScene: Process timed out after 10 seconds". + Upon pressing the "OK" button, you will be brought back to the Login Scene + where you can restart the process. + + If the previous error persists for more than one occurance, you will need + to check the status of the REPServer that has been launched on the test + stand. If the server is not running on the test stand, the GUI will never + be able to receive a response, therefore always throwing this error. + + The GUI cannot be closed while the Test In Progress Scene is on the + screen. If you gry to quit the application by clicking the "X" in the top, + right corner, a popup window will show saying that "You cannot quit the + application during a test!". This is to prevent corruption of test data. + + There is a "Stop" button in the middle of the application that tries to + stop the test. If able to successfully stop the test, the GUI will go to + the next available test. If there are no more available tests, then + the next scene will be the Test Summary Scene. + + If the test has been completed successfully and the results have been + received by the GUI, then the GUI will automatically go to the next scene. + The next scene will either be the next Test Scene (if available) or will + be the Test Summary Scene (if the last test has been finished). + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/CheckInGUI/PythonFiles/HGCAL_Help/TestScene_help.txt b/CheckInGUI/PythonFiles/HGCAL_Help/TestScene_help.txt new file mode 100644 index 00000000..35f67ad9 --- /dev/null +++ b/CheckInGUI/PythonFiles/HGCAL_Help/TestScene_help.txt @@ -0,0 +1,45 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Test Scene is a very important scene, as it is the last scene you will + see before a test is run. It is important to ensure that all of the + information shown on this screen is correct before proceeding. + + There are three boxes of information displayed on this screen: "Tester", + "Serial Number", and "Current Test". The first should display your name, + the second should display the serial ID of the board that you would like + to test (make sure that it is plugged into the test stand at this point), + and the third should display the name of the test that is going to be run. + + If any of this information is incorrect, then you will need to proceed to + the previous scenes in order to change this information. If your username + is incorrect, you will need to press the "Logout" button to return to the + Login Scene. If the board serial number is incorrect or if you would like + to switch which board you are testing, you will need to press the "Change + Board" button to go back to the Scan Scene. + + If all of the information looks correct, pressing the "Confirm" button + will send you to the Test In Progress Scene. This button will also tell + the testing machine to begin the test (which test is specified by the name + listed under the "Current Test"). + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/VisualInspectionGUI/PythonFiles/Images/Bethel_Logo.png b/CheckInGUI/PythonFiles/Images/Bethel_Logo.png similarity index 100% rename from VisualInspectionGUI/PythonFiles/Images/Bethel_Logo.png rename to CheckInGUI/PythonFiles/Images/Bethel_Logo.png diff --git a/CheckInGUI/PythonFiles/Images/EngineExample.png b/CheckInGUI/PythonFiles/Images/EngineExample.png new file mode 100644 index 00000000..a6e5ce9c Binary files /dev/null and b/CheckInGUI/PythonFiles/Images/EngineExample.png differ diff --git a/CheckInGUI/PythonFiles/Images/EnginePhoto.png b/CheckInGUI/PythonFiles/Images/EnginePhoto.png new file mode 100644 index 00000000..af544ad6 Binary files /dev/null and b/CheckInGUI/PythonFiles/Images/EnginePhoto.png differ diff --git a/VisualInspectionGUI/PythonFiles/Images/GreenCheckMark.png b/CheckInGUI/PythonFiles/Images/GreenCheckMark.png similarity index 100% rename from VisualInspectionGUI/PythonFiles/Images/GreenCheckMark.png rename to CheckInGUI/PythonFiles/Images/GreenCheckMark.png diff --git a/CheckInGUI/PythonFiles/Images/LDO_QRcode.png b/CheckInGUI/PythonFiles/Images/LDO_QRcode.png new file mode 100644 index 00000000..fca74666 Binary files /dev/null and b/CheckInGUI/PythonFiles/Images/LDO_QRcode.png differ diff --git a/VisualInspectionGUI/PythonFiles/Images/QRimage.png b/CheckInGUI/PythonFiles/Images/QRimage.png similarity index 100% rename from VisualInspectionGUI/PythonFiles/Images/QRimage.png rename to CheckInGUI/PythonFiles/Images/QRimage.png diff --git a/VisualInspectionGUI/PythonFiles/Images/RedX.png b/CheckInGUI/PythonFiles/Images/RedX.png similarity index 100% rename from VisualInspectionGUI/PythonFiles/Images/RedX.png rename to CheckInGUI/PythonFiles/Images/RedX.png diff --git a/VisualInspectionGUI/PythonFiles/Images/UMN_Logo.png b/CheckInGUI/PythonFiles/Images/UMN_Logo.png similarity index 100% rename from VisualInspectionGUI/PythonFiles/Images/UMN_Logo.png rename to CheckInGUI/PythonFiles/Images/UMN_Logo.png diff --git a/CheckInGUI/PythonFiles/Images/WagonExample.png b/CheckInGUI/PythonFiles/Images/WagonExample.png new file mode 100644 index 00000000..393cfbd8 Binary files /dev/null and b/CheckInGUI/PythonFiles/Images/WagonExample.png differ diff --git a/CheckInGUI/PythonFiles/Images/captured_image0.png b/CheckInGUI/PythonFiles/Images/captured_image0.png new file mode 100644 index 00000000..05fd5392 Binary files /dev/null and b/CheckInGUI/PythonFiles/Images/captured_image0.png differ diff --git a/CheckInGUI/PythonFiles/Images/captured_image1.png b/CheckInGUI/PythonFiles/Images/captured_image1.png new file mode 100644 index 00000000..1ad8b1b3 Binary files /dev/null and b/CheckInGUI/PythonFiles/Images/captured_image1.png differ diff --git a/CheckInGUI/PythonFiles/Scanner/Makefile b/CheckInGUI/PythonFiles/Scanner/Makefile new file mode 100644 index 00000000..d9158415 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scanner/Makefile @@ -0,0 +1,30 @@ +# +# ©2015 Symbol Technologies LLC. All rights reserved. +# + +CC = g++ +CXXFLAGS = -Wall -g -I/usr/include/zebra-scanner -I./include/ +LFLAGS = -L/usr/lib/zebra-scanner/corescanner -I./include/ +LIBS = -lcs-client -lcs-common +SRCS = ./src/main.cpp ./src/EventListener.cpp +OBJS = $(SRCS:.cpp=.o) +MAIN = ./bin/runScanner + +.PHONY: depend clean + +all: $(MAIN) + +$(MAIN): $(OBJS) + $(CC) $(CXXFLAGS) $(INCLUDES) -o $(MAIN) $(OBJS) $(LFLAGS) $(LIBS) + +.c.o: + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +.cpp.o: + $(CC) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +clean: + $(RM) ./src/*.o *~ $(MAIN) + +depend: $(SRC) + makedepend $(INCLUDES) $^ diff --git a/CheckInGUI/PythonFiles/Scanner/__init__.py b/CheckInGUI/PythonFiles/Scanner/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CheckInGUI/PythonFiles/Scanner/bin/__init__.py b/CheckInGUI/PythonFiles/Scanner/bin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CheckInGUI/PythonFiles/Scanner/bin/runScanner b/CheckInGUI/PythonFiles/Scanner/bin/runScanner new file mode 100755 index 00000000..7d495a34 Binary files /dev/null and b/CheckInGUI/PythonFiles/Scanner/bin/runScanner differ diff --git a/CheckInGUI/PythonFiles/Scanner/include/EventListener.h b/CheckInGUI/PythonFiles/Scanner/include/EventListener.h new file mode 100644 index 00000000..bd753b39 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scanner/include/EventListener.h @@ -0,0 +1,48 @@ +#ifndef EVENTLISTENER_H_ +#define EVENTLISTENER_H_ + +#include "zebra-scanner/CsIEventListenerXml.h" +#include "zebra-scanner/CsUserDefs.h" +#include "zebra-scanner/CsBarcodeTypes.h" +#include "zebra-scanner/Cslibcorescanner_xml.h" +#include + +class EventListener : public IEventListenerXml +{ + public: + explicit EventListener(); + virtual ~EventListener(); + + virtual void OnVideoEvent( short eventType, + int size, char* sfvideoData, + int dataLength, + std::string& pScannerData + ); + + virtual void OnImageEvent( short eventType, + int size, short imageFormat, + char* sfimageData, + int dataLength, + std::string& pScannerData + ); + + virtual void OnBinaryDataEvent( short eventType, int size, short dataFormat, + unsigned char* sfBinaryData, std::string& pScannerData); + virtual void OnBarcodeEvent( short eventType, std::string& pscanData ); + virtual void OnPNPEvent( short eventType, std::string ppnpData ); + virtual void OnCommandResponseEvent( short status, std::string& prspData ); + virtual void OnScannerNotification( short notificationType, std::string& pScannerData); + virtual void OnIOEvent( short type, unsigned char data ); + virtual void OnScanRMDEvent( short eventType, std::string& prmdData ); + virtual void OnDisconnect(); + + void Open(); + void GetScanners(); + void Close(); + bool GetToClose(); + + private: + bool toClose_; +}; + +#endif diff --git a/CheckInGUI/PythonFiles/Scanner/include/main.h b/CheckInGUI/PythonFiles/Scanner/include/main.h new file mode 100644 index 00000000..8159206e --- /dev/null +++ b/CheckInGUI/PythonFiles/Scanner/include/main.h @@ -0,0 +1,4 @@ +#ifndef MAIN_H_ +#define MAIN_H_ + +#endif diff --git a/CheckInGUI/PythonFiles/Scanner/python/__init__.py b/CheckInGUI/PythonFiles/Scanner/python/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CheckInGUI/PythonFiles/Scanner/python/get_barcodes.py b/CheckInGUI/PythonFiles/Scanner/python/get_barcodes.py new file mode 100644 index 00000000..e63d5ab2 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scanner/python/get_barcodes.py @@ -0,0 +1,68 @@ +import subprocess +import time +import signal +import ctypes +#import PythonFiles +libc = ctypes.CDLL("libc.so.6") + +from multiprocessing import Process, Manager, Pipe +import logging +logger = logging.getLogger("HGCAL_VI.PythonFiles.Scanner.python.get_barcodes") + +def decode(hex_str): + serial = "" + for h in hex_str.split(" "): + serial += bytes.fromhex(h).decode("ASCII") + + return serial + +def parse_xml(inXML): + if "<" not in inXML: + return + else: + splitting = inXML.split("datalabel") + return decode(splitting[1][1:-2]) + +def set_pdeathsig(sig = signal.SIGTERM): + def callable(): + return libc.prctl(1, sig) + return callable + +def scan(path): + proc = subprocess.Popen('{}/PythonFiles/Scanner/bin/runScanner'.format(path), stdout=subprocess.PIPE, preexec_fn=set_pdeathsig(signal.SIGTERM)) + logger.info("Starting scanner") + return proc + #for line in proc.stdout: + # if line is not None: + # conn.send(line.strip().decode('utf-8')) + # return + +def listen(serial, proc): + for line in proc.stdout: + if line is not None: + serial.append(line.strip().decode('utf-8')) + return + + #while not output_found: + # output = conn.recv() + # if output is not None: + # serial.append(output) + # output_found = True + # else: + # print('Still waiting') + +def run_scanner(): + manager = Manager() + serial = manager.list() + + proc = scan() + listener = Process(target=listen, args=(serial, proc)) + + listener.start() + + listener.join() + + logger.info("Scanner: %s" %parse_xml(serial[0])) + +if __name__=="__main__": + run_scanner() diff --git a/CheckInGUI/PythonFiles/Scanner/src/EventListener.cpp b/CheckInGUI/PythonFiles/Scanner/src/EventListener.cpp new file mode 100644 index 00000000..29172248 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scanner/src/EventListener.cpp @@ -0,0 +1,107 @@ +#include "../include/EventListener.h" +#include + +using namespace std; + +EventListener::EventListener() +{ + this->toClose_ = false; +} + +EventListener::~EventListener() +{ + Close(); +} + +void EventListener::Open() +{ + StatusID status; + + ::Open(this, SCANNER_TYPE_ALL, &status); + + std::string inXML = + "11"; + std::string outXML = ""; + + ::ExecCommand(CMD_REGISTER_FOR_EVENTS, inXML, outXML, &status); + +} + +void EventListener::Close() +{ + StatusID status; + ::Close(0, &status); +} + +void EventListener::GetScanners() +{ + unsigned short number; + std::vector scannerIDs; + std::string outXML; + StatusID status; + + std::cout << "Getting total number of scanners connected..." << std::endl; + + ::GetScanners(&number, &scannerIDs, outXML, &status); + + std::cout << "Total number of scanners connected: " << number << std::endl; + std::cout << outXML << std::endl; +} + +void EventListener::OnBarcodeEvent( short eventType, std::string& pscanData ) +{ + //std::cout << "Barcode Detected" << std::endl; + //std::cout << "Out XML:" << std::endl; + std::cout << pscanData << std::endl; + toClose_ = true; + std::cout << toClose_ << std::endl; +} + +bool EventListener::GetToClose() +{ + return toClose_; +} + +void EventListener::OnDisconnect() +{ + cout << "OnDisconnect" << endl; +} + +void EventListener::OnImageEvent( short eventType, int size, short imageFormat, + char* sfimageData, int dataLength, std::string& pScannerData ) +{ + cout << "OnImageEvent" << endl; +} +void EventListener::OnVideoEvent( short eventType, int size, char* sfvideoData, int + dataLength, std::string& pScannerData ) +{ + cout << "OnVideoEvent" << endl; +} +void EventListener::OnPNPEvent( short eventType, std::string ppnpData ) +{ +} +void EventListener::OnCommandResponseEvent( short status, std::string& prspData ) +{ + cout << endl << "Scanner data: " << prspData << endl; + cout << "OnCommandResponseEvent" << endl; + cout << prspData << endl; +} +void EventListener::OnScannerNotification( short notificationType, std::string& + pScannerData ) +{ + cout << endl << "Scanner event data: " << pScannerData << endl; + cout << "OnScannerNotification" << endl; +} +void EventListener::OnIOEvent( short type, unsigned char data ) +{ + cout << "OnIOEvent" << endl; +} +void EventListener::OnScanRMDEvent( short eventType, std::string& prmdData ) +{ + cout << "OnScanRMDEvent" << endl; + cout << "Out XML " << endl; + cout << prmdData << endl; +} +void EventListener::OnBinaryDataEvent( short eventType, int size, short dataFormat, unsigned char* sfBinaryData, std::string& pScannerData) +{ +} diff --git a/CheckInGUI/PythonFiles/Scanner/src/main.cpp b/CheckInGUI/PythonFiles/Scanner/src/main.cpp new file mode 100644 index 00000000..6a52ca4a --- /dev/null +++ b/CheckInGUI/PythonFiles/Scanner/src/main.cpp @@ -0,0 +1,28 @@ +#include "../include/EventListener.h" +#include "../include/main.h" + +#include +#include + +int main(void) +{ + + EventListener el; + el.Open(); + + //el.GetScanners(); + + //std::cout << "Checking if you have a scanner plugged in..." << std::endl; + + //std::cout << "Please scan barcode or type 'q' to quit: " << std::endl; + + //std::cout << "33 32 30 35 39 39 39 39 39 39 30 30 30 30 31" << std::endl; + + while(!el.GetToClose()) + { + //std::cout << "Still running..." << std::endl; + //usleep(1000000); + } + + return 0; +} diff --git a/CheckInGUI/PythonFiles/Scenes/AddUserScene.py b/CheckInGUI/PythonFiles/Scenes/AddUserScene.py new file mode 100644 index 00000000..acba030c --- /dev/null +++ b/CheckInGUI/PythonFiles/Scenes/AddUserScene.py @@ -0,0 +1,233 @@ + +################################################################################# + +# importing necessary modules +import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os + +################################################################################# + +logger = logging.getLogger('HGCAL_VI.PythonFiles.Scenes.AddUserScene') + +# Creates a class that is called by the GUIWindow. +# GUIWindow instantiates an object called add_user_scene. +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. + +class AddUserScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + super().__init__(master_frame, width=850, height=500) + self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + self.create_style(parent) + self.parent = parent + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + # Creating the title for the window + lbl_title = ttk.Label( + self, + text="Add User", + font=('Arial', '42') + ) + lbl_title.pack(pady=(50,0)) + + # Creating entry box for new user's name + self.new_user_name = "" + self.user_entry = tk.Entry( + self, + textvariable= self.new_user_name, + font=('Arial', '15') + ) + self.user_entry.pack(pady=30) + + # Creating the title for the window + password_label = ttk.Label( + self, + text="Enter Admin Password", + font=('Arial', '36') + ) + password_label.pack(pady=(10,0)) + + # Creating entry box for new user's name + self.password = "" + self.user_password = tk.Entry( + self, + textvariable= self.password, + font=('Arial', '15'), + show = "*" + ) + self.user_password.pack(pady=30) + + # Creating the submit button + self.btn_submit = ttk.Button( + self, + text="Submit", + #padx = 50, + #pady = 10, + #relief=tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.pack(pady = 15) + + # Creating the cancel button + self.btn_submit = ttk.Button( + self, + text="Cancel", + #padx = 50, + #pady = 10, + #relief=tk.RAISED, + command= lambda: self.btn_cancel_action(parent) + ) + self.btn_submit.pack() + + # Forces frame to stay the size of the main_window + # rather than adjusting to the size of the widgets + self.pack_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + + + + ################################################# + + # Creates the function for the submit button command + # @params "_parent" is also a parent like "parent", but it is a different "parent", + # passes in GUIWindow + def btn_submit_action(self, _parent): + + self.new_user_name = self.user_entry.get() + self.password = self.user_password.get() + + # Popup to confirm that a new user is added into the DB + cnfm_pop = ConfirmPopup(_parent, self.data_holder, self.new_user_name, self.password) + + def remove_widgets(self, _parent): + for widget in self.winfo_children(): + widget.destroy() + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + def btn_cancel_action(self, _parent): + + _parent.set_frame_login_frame() + + ################################################# + +################################################################################# + + +class ConfirmPopup(): + + ################################################# + + def __init__(self, parent, data_holder, new_user_name, password): + self.confirm_popup(data_holder, new_user_name, password) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder, new_user_name, password): + self.data_holder = data_holder + self.new_user_name = new_user_name + self.password = password + logger.info("Confirming that the user wants to add {}".format(self.new_user_name)) + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("New User Name") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = " You are about to add {} as a user \n Are you sure? ".format(self.new_user_name), + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) + + # Creates retry and continue buttons + btn_retry = ttk.Button( + frm_popup, + width = 8, + #height = 2, + text = "Cancel", + #relief = tk.RAISED, + # font = ('Arial', 12), + command = lambda: self.cancel_function() + ) + btn_retry.grid(column = 0, row = 1) + + btn_continue = ttk.Button( + frm_popup, + width = 8, + #height = 2, + text = "Confirm", + #relief = tk.RAISED, + # font = ('Arial', 12), + command = lambda: self.continue_function(self.parent) + ) + btn_continue.grid(column = 1, row = 1) + + + ################################################# + + # Called when the "cancel" button is selected + def cancel_function(self): + self.popup.destroy() + + ################################################# + + # Called to continue on in the testing procedure + def continue_function(self, _parent): + self.popup.destroy() + + # Adding a new user name to data_holder/DB + self.data_holder.add_new_user_name(self.new_user_name, self.password) + # Changes frame to scan_frame + _parent.set_frame_login_frame() + + +################################################################################# diff --git a/CheckInGUI/PythonFiles/Scenes/ComponentScanScene.py b/CheckInGUI/PythonFiles/Scenes/ComponentScanScene.py new file mode 100644 index 00000000..c3d101a3 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scenes/ComponentScanScene.py @@ -0,0 +1,297 @@ +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter as tk +import tkinter.ttk as ttk +import sys, time +from tkinter import * +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + + +################################################################################# + +logger = logging.getLogger('HGCAL_VI.PythonFiles.Scenes.ComponentScanScene') + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class ComponentScanScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + self.data_holder = data_holder + self.is_current_scene = False + + self.create_style(parent) + + self.EXIT_CODE = 0 + + self.is_checking = False + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + # can see more scanner documentation in the Visual Inspection GUI + def check_for_ldo(self): + if not self.is_checking: + return + + got_code = self.data_holder.check_for_ldo() + if got_code is not None and got_code != "None": + logger.info("LDO uploaded successfully.") + self.stop() + else: + self.parent.master_window.after(1000, self.check_for_ldo) + + + + def start(self): + self.is_checking = True + self.parent.master_window.after(1000, self.check_for_ldo) + + + def stop(self): + self.is_checking = False + self.btn_submit_action(self.parent) + + + + # Creates the GUI itself + def initialize_GUI(self, parent, master_frame): + + self.master_frame = master_frame + self.parent = parent + + super().__init__(self.master_frame, width=1105, height=850) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + # Create a photoimage object of the QR Code + QR_image = Image.open("{}/Images/EngineExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label = ttk.Label(self, image=QR_PhotoImage) + QR_label.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label.grid(column=3, row = 0, sticky= 'ne', pady = (250,0)) + + # Create a photoimage object of the QR Code + QR_image = Image.open("{}/Images/WagonExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label2 = ttk.Label(self, image=QR_PhotoImage) + QR_label2.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label2.grid(column=3, row = 0, sticky= 'ne', pady =(100, 0), padx = (75,0)) + + Scan_Board_Prompt_Frame = ttk.Frame(self,) + Scan_Board_Prompt_Frame.grid(column=0, row = 0) + + # creates a Label Variable, different customization options + self.lbl_check = ttk.Label( + master = Scan_Board_Prompt_Frame, + text = 'Scan the LDO', + font = ('Arial', 40) + ) + self.lbl_check.pack(padx = 50, pady = 50) + + self.lbl_scan = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Scan QR Code to open Webapp", + font = ('Arial', 24) + ) + self.lbl_scan.pack(padx = 50, pady = 25) + + qr_code = Image.open("{}/Images/LDO_QRcode.png".format(PythonFiles.__path__[0])) + qr_code = iTK.PhotoImage(qr_code) + # Create a label to label the entry box + lbl_full = ttk.Label( + Scan_Board_Prompt_Frame, + image = qr_code + ) + lbl_full.image = qr_code + lbl_full.pack(padx = 20) + + # Submit button creation + self.btn_submit = ttk.Button( + Scan_Board_Prompt_Frame, + text="Cancel", + #padx = 20, + #pady = 10, + #relief = tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.pack() + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=0, row = 1) + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 3, row = 0, sticky= 'se') + + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.pack(anchor = 'se', padx = 10, pady = 20) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 's', padx = 10, pady = 20) + + + # Locks frame size to the master_frame size + self.grid_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + ################################################# + + # Function for the submit button + def btn_submit_action(self, _parent): + + self.EXIT_CODE = 1 + + #self.data_holder.add_component(self.ent_full.get()) + + _parent.set_frame_inspection_frame() + + self.EXIT_CODE = 0 + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + # Function for the log out button + def btn_logout_action(self, _parent): + + self.EXIT_CODE = 1 + self.listener.terminate() + self.scanner.terminate() + + + # Send user back to login frame + _parent.set_frame_login_frame() + + ################################################# + + # Function to activate the submit button + def show_submit_button(self): + self.data_holder.decode_label(self.ent_full.get()) + self.btn_submit["state"] = "active" + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to disable to the submit button + def hide_submit_button(self): + self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to activate the rescan button + def show_rescan_button(self): + self.btn_rescan["state"] = "active" + + ################################################# + + # Function to disable to the rescan button + def hide_rescan_button(self): + self.btn_rescan["state"] = "disabled" + + ################################################# + + def kill_processes(self): + try: + self.scanner.kill() + self.listener.terminate() + self.EXIT_CODE = 1 + except: + pass diff --git a/CheckInGUI/PythonFiles/Scenes/InspectionScenes/Inspection1.py b/CheckInGUI/PythonFiles/Scenes/InspectionScenes/Inspection1.py new file mode 100644 index 00000000..53d58b38 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scenes/InspectionScenes/Inspection1.py @@ -0,0 +1,261 @@ +################################################################################# + +# Importing Necessary Modules +import tkinter as tk +import tkinter.ttk as ttk +import tkinter.font as font +import logging + +logger = logging.getLogger("HGCAL_VI.PythonFiles.Scenes.InspectionScene") + + +################################################################################# + + +# Creating class for the window +class Inspection1(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + super().__init__(master_frame, width = 1105, height = 850) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.test_name = "SOMETHING STRING" + self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + self.parent = parent + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + + # Creates a font to be more easily referenced later in the code + font_scene = ('Arial', 12) + font_scene_14 = ('Arial', 14) + + # Create a centralized window for information + frm_window = ttk.Frame(self, width = 1105, height = 850) + frm_window.grid(column=0, row=0, sticky = 'n') + + frm_window.grid_rowconfigure(0, weight=1) + frm_window.grid_columnconfigure(0, weight=1) + + # Create a label for the tester's name + lbl_tester = ttk.Label( + frm_window, + text = "Tester: ", + font = ('Arial', '26') + ) + lbl_tester.grid(row=0, column=1,sticky = 'nw', pady=15 , padx = 20) + + # Create an entry for the tester's name + ent_tester = tk.Entry( + frm_window, + #font = font_scene + ) + ent_tester.insert(0, self.data_holder.data_dict['user_ID']) + ent_tester.grid(row=0, column=2, sticky = 'nw', pady=25 ) + ent_tester.config(state = "disabled") + + # Create a label for the full id box + lbl_full = ttk.Label( + frm_window, + text = "Full ID: ", + font = ('Arial', '26') + ) + lbl_full.grid(row=0, column=3, pady=10, padx = 20, sticky = 'nw') + + # Create a entry for the full id box + ent_full = tk.Entry( + frm_window, + #font = font_scene + ) + ent_full.insert(0, self.data_holder.data_dict['current_full_ID']) + ent_full.grid(pady=25, row = 0, column = 4, sticky = 'nw') + ent_full.config(state = "disabled") + + # Create a centralized window for information + frm_Q = ttk.Frame(self) + frm_Q.grid(column=0, row=0, sticky = 'n', pady = (200,0)) + frm_Q.grid_rowconfigure(0, weight=1) + frm_Q.grid_columnconfigure(0, weight=1) + + + inspection_index = 0 + check_dictionary = self.data_holder.get_check_dict(inspection_index) + + self.tk_bools = [] + self.s.configure('TCheckbutton', font = ('Arial','36')) + + if len(check_dictionary) > 0: + for idx, item in enumerate(self.data_holder.get_check_dict(0)): + + new_bool = tk.BooleanVar() + self.tk_bools.append(new_bool) + + # Checkbutton1 + c1 = ttk.Checkbutton( + frm_Q, + text=item['text'], + variable= new_bool, + onvalue= True, + offvalue= False, + style = 'TCheckbutton' + # command=print_selection + ) + c1.grid( row = 1 + idx, column= 2, sticky = 'nw', columnspan=2) + + lbl_comm = ttk.Label( + frm_window, + text = "Comments:", + #font = font_scene + ) + lbl_comm.grid(row=0, column=1, pady= (75,0) ) + lbl_comm.config(font = ('Arial', '24')) + + # Comment Box + self.comment_box = tk.Entry( + frm_window, + #font = font_scene, + state= 'normal', + width= 75, + ) + self.comment_box.grid(row = 1, column =1, columnspan=5) + + # Create a button for confirming test + btn_confirm = ttk.Button( + frm_window, + text = "Confirm", + #relief = tk.RAISED, + command = lambda:self.btn_confirm_action(parent) + ) + btn_confirm.grid(row = 5, column= 2, pady= 35, sticky = 's') + #btn_confirm['font'] = font.Font(family = 'Arial', size = 13) + + # Create frame for logout button + nav_frame = ttk.Frame(self) + nav_frame.grid(column = 1, row = 0, sticky = 'se', padx =5) + + # Create a rescan button + btn_rescan = ttk.Button( + nav_frame, + text = "Change Boards", + #relief = tk.RAISED, + command = lambda: self.btn_rescan_action(parent)) + btn_rescan.pack(anchor = 'se', pady=15) + + # Create a logout button + btn_logout = ttk.Button( + nav_frame, + text = "Logout", + #relief = tk.RAISED, + command = lambda: self.btn_logout_action(parent)) + btn_logout.pack(anchor = 'se') + + # Creating the help button + btn_help = ttk.Button( + nav_frame, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'se', pady = 10) + + + + # # # # # # # # # + + + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_columnconfigure(2, weight=1) + self.grid_columnconfigure(3, weight=1) + self.grid_columnconfigure(4, weight=1) + + frm_window.grid_columnconfigure(0, weight=1) + frm_window.grid_columnconfigure(1, weight=1) + frm_window.grid_columnconfigure(2, weight=1) + frm_window.grid_columnconfigure(3, weight=1) + + self.grid_rowconfigure(0, weight=1) + + + frm_window.grid_propagate(0) + self.grid_propagate(0) + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + ################################################# + + # Rescan button takes the user back to scanning in a new board + def btn_rescan_action(self, _parent): + _parent.set_frame_scan_frame() + + ################################################# + + # Back button action takes the user back to the scanning device + def btn_back_action(self, _parent): + pass + + ################################################# + + # adds the visual inspection info to the data holder + def update_data_holder(self): + for i, item in enumerate(self.tk_bools): + self.data_holder.set_check_dict(i, item.get()) + + self.data_holder.set_comment_dict(0, self.comment_box.get()) + self.data_holder.update_from_json_string() + self.data_holder.set_inspection_comments(self.comment_box.get()) + self.data_holder.set_comments(self.comment_box.get()) + self.data_holder.add_inspection_to_comments() + + + ################################################# + + def btn_confirm_action(self, _parent): + + self.update_data_holder() + _parent.set_frame_summary() + + def get_submit_action(self): + return self.btn_confirm_action + + def get_parent(self): + return self.parent + + ################################################# + + # functionality for the logout button + def btn_logout_action(self, _parent): + _parent.set_frame_login_frame() + + ################################################# + + + def remove_widgets(self, _parent): + for widget in self.winfo_children(): + widget.destroy() + + + diff --git a/VisualInspectionGUI/PythonFiles/Scenes/LoginScene.py b/CheckInGUI/PythonFiles/Scenes/LoginScene.py similarity index 58% rename from VisualInspectionGUI/PythonFiles/Scenes/LoginScene.py rename to CheckInGUI/PythonFiles/Scenes/LoginScene.py index c2541658..e7779b2e 100644 --- a/VisualInspectionGUI/PythonFiles/Scenes/LoginScene.py +++ b/CheckInGUI/PythonFiles/Scenes/LoginScene.py @@ -2,9 +2,14 @@ # importing necessary modules import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os ################################################################################# +logger = logging.getLogger("HGCAL_VI.PythonFiles.Scenes.LoginScene") # Creates a class that is called by the GUIWindow. # GUIWindow instantiates an object called login_frame. @@ -12,45 +17,62 @@ # @param master_frame -> passes master_frame as the container for everything in the class. # @param data_holder -> passes data_holder into the class so the data_holder functions can # be accessed within the class. -class LoginScene(tk.Frame): +class LoginScene(ttk.Frame): ################################################# def __init__(self, parent, master_frame, data_holder): - super().__init__(master_frame, width=850, height=500) + super().__init__(master_frame, width = 1105, height = 850) self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + self.parent = parent + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + + logging.info("LoginScene: Frame has been created.") # Creating a list of users for dropdown menu - # Eventually need to add a way for a database to have control over this list - User_List = [ - "Bob Johnson", - "Spencer Higgins", - "Amanda Holmes" - ] + User_List = self.data_holder.get_all_users() # Creating the title for the window - lbl_title = tk.Label( + lbl_title = ttk.Label( self, text="Please Select Your Name", - font=('Arial', '24') + font=('Arial', '62') ) lbl_title.pack(pady=75) # Creating intial value in dropdown menu self.user_selected = tk.StringVar(self) - self.user_selected.set("") # default value is empty # Creating the dropdown menu itself - self.opt_user_dropdown = tk.OptionMenu( + self.opt_user_dropdown = ttk.OptionMenu( self, - self.user_selected, # Tells option menu to use the created initial value + self.user_selected, + User_List[0], + *User_List # Tells the dropdown menu to use every index in the User_List list ) self.opt_user_dropdown.pack(pady=15) - self.opt_user_dropdown.config(width = 20, font = ('Arial', 13)) - self.opt_user_dropdown['menu'].configure(font = ('Arial', 12)) + #self.opt_user_dropdown.config(width = 20, font = ('Arial', 13)) + #self.opt_user_dropdown['menu'].configure(font = ('Arial', 12)) # Traces when the user selects an option in the dropdown menu # When an option is selected, it calls the show_submit_button function @@ -61,12 +83,12 @@ def __init__(self, parent, master_frame, data_holder): # Creating the submit button # It does not get enabled until the user selects an option menu option - self.btn_submit = tk.Button( + self.btn_submit = ttk.Button( self, text="Submit", - padx = 50, - pady = 10, - relief=tk.RAISED, + #padx = 50, + #pady = 10, + #relief=tk.RAISED, command= lambda: self.btn_submit_action(parent) ) self.btn_submit.pack() @@ -74,20 +96,46 @@ def __init__(self, parent, master_frame, data_holder): # Creating the add user button - self.btn_add_user = tk.Button( + self.btn_add_user = ttk.Button( self, text="Add User", - padx = 20, - pady = 5, - relief=tk.RAISED, + #padx = 20, + #pady = 5, + #relief=tk.RAISED, command= lambda: self.btn_add_user_action(parent) ) self.btn_add_user.pack(pady=70) + # Creating the help button + self.btn_help = ttk.Button( + self, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + self.btn_help.pack(anchor = 's', padx = 10, pady = 20) + + # Forces frame to stay the size of the main_window # rather than adjusting to the size of the widgets self.pack_propagate(0) + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + ################################################# # Creates the function for the submit button command @@ -95,18 +143,15 @@ def __init__(self, parent, master_frame, data_holder): # passes in GUIWindow def btn_submit_action(self, _parent): # Sets the user_ID in the data_holder to the selected user - self.data_holder.user_ID = self.user_selected.get() + self.data_holder.set_user_ID(self.user_selected.get()) # Changes frame to scan_frame _parent.set_frame_scan_frame() - - self.data_holder.print() - ################################################# # To be given commands later, for now it is a dummy function def btn_add_user_action(self, _parent): - pass + _parent.set_frame_add_user_frame() ################################################# @@ -116,5 +161,5 @@ def show_submit_button(self): ################################################# - -################################################################################# \ No newline at end of file + +################################################################################# diff --git a/CheckInGUI/PythonFiles/Scenes/PostScanScene.py b/CheckInGUI/PythonFiles/Scenes/PostScanScene.py new file mode 100644 index 00000000..69bf08ff --- /dev/null +++ b/CheckInGUI/PythonFiles/Scenes/PostScanScene.py @@ -0,0 +1,271 @@ +################################################################################# + +import PythonFiles +import json, logging +import tkinter as tk +import tkinter.ttk as ttk +from PIL import ImageTk as iTK +from PIL import Image +from matplotlib.pyplot import table +from pyparsing import col +import PythonFiles +import os +import datetime + +################################################################################# + +logger = logging.getLogger("HGCAL_VI.PythonFiles.Scenes.PostScanScene") + +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + +# Frame that shows up after board has been entered with info about the board +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data + +class PostScanScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + + # Call to the super class's constructor + # Super class is the tk.Frame class + self.data_holder = data_holder + + self.master_frame = master_frame + + super().__init__(self.master_frame, width = 1105, height = 850) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.parent = parent + + self.create_style(parent) + + self.create_frame(parent) + + # Fits the frame to set size rather than interior widgets + self.grid_propagate(0) + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def create_frame(self, parent): + + try: + for widget in self.winfo_children(): + widget.destroy() + except: + logger.warning("PostScanScene: Widgets could not be found and/or destroyed (making room for new widgets.") + else: + logger.info("PostScanScene: Widgets destroyed successfully (making room for new widgets).") + + self.canvas = tk.Canvas(self, width = 1105, height = 850, bg = "#33393b") + self.frame = ttk.Frame(self.canvas, width = 1105, height = 850) + + self.scroller = ttk.Scrollbar(self, orient='vertical', command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.scroller.set) + self.canvas.grid(row = 0, column = 0) + self.scroller.grid(row=0, column=1, sticky='NSEW') + self.window = self.canvas.create_window((4,4), window=self.frame, anchor='n', tags='self.frame') + + self.frame.bind('', self.onFrameConfigure) + self.frame.bind('', self.onEnter) + self.frame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + + # Adds the title to the Summary Frame + self.title = ttk.Label( + self.frame, + #fg='#0d0d0d', + text = "This Board has already been Checked In", + font=('Arial',35,'bold') + ) + self.title.grid(row= 0, column= 1, pady = 20) + + + # Adds Board full id to the SummaryFrame + self.id = ttk.Label( + self.frame, + #fg='#0d0d0d', + text = "Full ID: " + str(self.data_holder.data_dict['current_full_ID']), + font=('Arial',24,'bold') + ) + self.id.grid(row= 1, column= 1, pady = 20) + + green_check = Image.open("{}/Images/GreenCheckMark.png".format(PythonFiles.__path__[0])) + green_check = green_check.resize((75, 75), Image.LANCZOS) + green_check = iTK.PhotoImage(green_check) + + redx = Image.open('{}//Images/RedX.png'.format(PythonFiles.__path__[0])) + redx = redx.resize((75, 75), Image.LANCZOS) + redx = iTK.PhotoImage(redx) + + row_offset = 2 + try: + if self.data_holder.data_dict['test_names']: + res_dict = {} + for n in self.data_holder.data_dict['test_names']: + res_dict[n] = [] + for idx,el in enumerate(self.data_holder.data_dict['prev_results']): + res_dict[el[0]] = el[1] + + for idx,el in enumerate(res_dict.keys()): + self.lbl_res = ttk.Label( + self.frame, + text = str(el) + ': ', + font=('Arial',14) + ) + self.lbl_res.grid(row=idx+2, column=1) + if res_dict[el] == 'Passed': + self.lbl_img = ttk.Label( + self.frame, + image = green_check, + width=75, + #height=75, + font=('Arial',14) + ) + self.lbl_img.image=green_check + self.lbl_img.grid(row=idx+2, column=2) + else: + self.lbl_img = ttk.Label( + self.frame, + image = redx, + width=75, + #height=75, + font=('Arial',14) + ) + self.lbl_img.image=redx + self.lbl_img.grid(row=idx+2, column=2) + row_offset += idx+2 + else: + self.lbl_res = ttk.Label( + self.frame, + text = str(self.data_holder.data_dict['prev_results']), + font=('Arial',14) + ) + self.lbl_res.grid(row=2, column=1) + + except Exception as e: + logger.exception(e) + self.lbl_err = ttk.Label( + self, + text = "Some extraneous error occured and Board was not entered. See logs for more info.", + font=('Arial', 14) + ) + self.lbl_err.grid(column = 1, row = 2, pady = 10) + + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 3, row = 0, sticky= 'se') + + # Creating the proceed button + proceed_button = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Proceed", + command = lambda: self.btn_proceed_action(parent) + ) + proceed_button.grid(row = 3, column = 0, padx = 10, pady = 25, sticky = 's') + + #creating the next board button + next_board_button = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Change Boards", + command = lambda: self.btn_NextBoard_action(parent) + ) + next_board_button.grid(row = 4,column =0 ,padx = 10, pady = 25, sticky = 's') + + #creating the component scan board button + scan_components = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Check In LDO", + command = lambda: self.btn_components_action(parent) + ) + scan_components.grid(row = 5,column =0 ,padx = 10, pady = 25, sticky = 's') + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.grid(sticky = 's', padx = 10, pady = 0) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.grid(sticky = 's', padx = 10, pady = 10) + + + ################################################# + + def btn_proceed_action(self, _parent): + _parent.set_frame_inspection_frame() + + def btn_NextBoard_action(self, parent): + parent.set_frame_scan_frame() + + def btn_components_action(self, parent): + parent.set_frame_component_frame() + + def btn_logout_action(self, parent): + parent.set_frame_login_frame() + + def get_submit_action(self): + return self.btn_proceed_action + + def get_parent(self): + return self.parent + + + + ################################################# + + def update_frame(self): + self.create_frame(self.parent) + + ################################################# + + def onFrameConfigure(self, event): + self.canvas.configure(scrollregion=self.canvas.bbox('all')) + + def onMouseWheel(self, event): + if event.num == 4: + self.canvas.yview_scroll(-1, 'units') + elif event.num == 5: + self.canvas.yview_scroll(1, 'units') + + def onEnter(self, event): + self.canvas.bind_all('', self.onMouseWheel) + self.canvas.bind_all('', self.onMouseWheel) + + def onLeave(self, event): + self.canvas.unbind_all('') + self.canvas.unbind_all('') + + + +################################################################################# diff --git a/CheckInGUI/PythonFiles/Scenes/ScanScene.py b/CheckInGUI/PythonFiles/Scenes/ScanScene.py new file mode 100644 index 00000000..c8a31a42 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scenes/ScanScene.py @@ -0,0 +1,500 @@ +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter as tk +import tkinter.ttk as ttk +import sys, time +from tkinter import * +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + + +################################################################################# + +logger = logging.getLogger('HGCAL_VI.PythonFiles.Scenes.ScanScene') + + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class ScanScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + self.data_holder = data_holder + self.is_current_scene = False + + self.create_style(parent) + + self.EXIT_CODE = 0 + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + # can see more scanner documentation in the Visual Inspection GUI + def scan_QR_code(self, master_window): + self.EXIT_CODE = 0 + + self.ent_full.config(state = 'normal') + self.ent_full.delete(0,END) + self.master_window = master_window + self.hide_rescan_button() + sys.path.insert(1,'/home/hgcal/WagonTest/Scanner/python') + + from ..Scanner.python.get_barcodes import scan, listen, parse_xml + + manager = mp.Manager() + full_id = manager.list() + + self.ent_full.config(state = 'normal') + + self.scanner = scan(self.parent.main_path) + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + while 1 > 0: + + try: + self.master_window.update() + except: + pass + if not len(full_id) == 0: + label = parse_xml(full_id[0]) + + self.listener.terminate() + self.scanner.terminate() + + self.ent_full.delete(0,END) + self.ent_full.insert(0, str(label)) + self.ent_full.config(state = 'disabled') + self.show_rescan_button() + break + + elif self.EXIT_CODE != 0: + logger.info("Exit code received on the Scan Scene. Terminating processes.") + self.listener.terminate() + self.scanner.terminate() + logger.info("ScanScene processes terminated successfully.") + break + else: + time.sleep(.01) + + # Creates the GUI itself + def initialize_GUI(self, parent, master_frame): + + self.master_frame = master_frame + self.parent = parent + + super().__init__(self.master_frame, width = 1105, height = 850) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + # Create a photoimage object of the QR Code + QR_image = Image.open("{}/Images/EngineExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label = ttk.Label(self, image=QR_PhotoImage) + QR_label.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label.grid(column=3, row = 0, sticky= 'ne', pady = (250,0)) + + # Create a photoimage object of the QR Code + QR_image = Image.open("{}/Images/WagonExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label2 = ttk.Label(self, image=QR_PhotoImage) + QR_label2.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label2.grid(column=3, row = 0, sticky= 'ne', pady =(100, 0), padx = (75,0)) + + Scan_Board_Prompt_Frame = ttk.Frame(self,) + Scan_Board_Prompt_Frame.grid(column=0, row = 0, rowspan=2) + + # creates a Label Variable, different customization options + self.lbl_check = ttk.Label( + master = Scan_Board_Prompt_Frame, + text = 'Check In', + font = ('Arial', 40) + ) + self.lbl_check.pack(padx = 50, pady = 50) + + lbl_scan = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Scan the QR Code on the Board", + font = ('Arial', 24) + ) + lbl_scan.pack(padx = 50, pady = 25) + + # Create a label to label the entry box + lbl_full = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Full ID:", + font = ('Arial', 24) + ) + lbl_full.pack(padx = 20) + + # Entry for the full id to be displayed. Upon Scan, update and disable? + global ent_full + + # Creating intial value in entry box + self.user_text = tk.StringVar(self) + + # Creates an entry box + self.ent_full = tk.Entry( + Scan_Board_Prompt_Frame, + font = ('Arial', 16), + textvariable= self.user_text, + ) + self.ent_full.pack(padx = 50, pady = 25) + + manufacturers_list = ['None'] + self.data_holder.get_manufacturers() + self.manuf_selected = tk.StringVar(self) + + lbl_full = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Select Manufacturer:", + font = ('Arial', 24) + ) + lbl_full.pack(padx = 20) + + self.manufacturer_dropdown = ttk.OptionMenu( + Scan_Board_Prompt_Frame, + self.manuf_selected, + self.data_holder.data_dict['manufacturer'], + *manufacturers_list # Tells the dropdown menu to use every index in the manufacturers_list list + ) + self.manufacturer_dropdown.pack(pady=15) + + # Create a label to label the comments box + lbl_com = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Comments:", + font = ('Arial', 32), + ) + lbl_com.pack(padx = 20) + + com_text = '' + #place to enter comments + self.ent_com = tk.Text( + master = Scan_Board_Prompt_Frame, + font = ('Arial', 16), + height = 5, + width = 20 + ) + self.ent_com.pack(padx = 50) + + # Traces an input to show the submit button once text is inside the entry box + self.user_text.trace( + "w", + lambda name, + index, + mode, + sv=self.user_text: self.show_submit_button() + ) + + self.manuf_selected.trace( + "w", + lambda name, + index, + mode, + sv=self.manuf_selected: self.show_submit_button_manu() + ) + + # Rescan button creation + self.btn_rescan = ttk.Button( + Scan_Board_Prompt_Frame, + text="Rescan", + #padx = 20, + #pady =10, + #relief = tk.RAISED, + command = lambda: self.scan_QR_code(self.master_window) + ) + self.btn_rescan.pack(pady=30) + + # Submit button creation + self.btn_submit = ttk.Button( + Scan_Board_Prompt_Frame, + text="Submit", + #padx = 20, + #pady = 10, + #relief = tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.pack() + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=3, row = 1, sticky='ne') + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 3, row = 1, sticky= 'se') + + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.pack(anchor = 'se', padx = 10, pady = 20) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 's', padx = 10, pady = 20) + + + + + # Locks frame size to the master_frame size + self.grid_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + ################################################# + + # Function for the submit button + def btn_submit_action(self, _parent): + + self.EXIT_CODE = 1 + + self.data_holder.set_full_ID(self.ent_full.get()) + self.data_holder.set_comments(self.ent_com.get(1.0, 'end-1c')) + + self.data_holder.set_manufacturer_id(self.manuf_selected.get()) + + in_id = self.data_holder.check_if_new_board() + + if in_id == None: + self.label_major['text'] = 'Could not upload board' + self.label_sub['text'] = 'Have an expert check the logs' + self.label_sn['text'] = '' + self.label_sub.update() + self.label_sn.update() + self.label_major.update() + self.btn_submit["state"] = "disabled" + + if self.data_holder.data_dict['prev_results'] != '': + _parent.set_frame_postscan() + + else: + if self.ent_full.get()[3] in ('W', 'Z', 'S'): + _parent.set_frame_inspection_frame() + elif self.ent_full.get()[3] == 'E': + _parent.set_frame_component_frame() + else: + # TODO make this a popup + logger.warning('Error: Please scan a Wagon, Zipper, Engine, or Flex Cable.') + + + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + # Function for the log out button + def btn_logout_action(self, _parent): + + self.EXIT_CODE = 1 + self.listener.terminate() + self.scanner.terminate() + + + # Send user back to login frame + _parent.set_frame_login_frame() + + ################################################# + + # Function to activate the submit button + def show_submit_button(self): + barcode = self.ent_full.get() + self.data_holder.decode_label(barcode) + major = self.data_holder.label_info['Major Type'] + sn = self.data_holder.label_info['SN'] + if major: + if major == 'LD-Engine' or major == 'HD-Engine': + self.manuf_selected.set(self.data_holder.get_manufacturer_from_batch(major, sn[2], barcode[3:8])) + elif major == 'HD-Wagon': + self.manuf_selected.set(self.data_holder.get_manufacturer_from_batch(major, sn[2], barcode[3:9])) + elif major == 'LD-Wagon-West' or major == 'LD-Wagon-East': + self.manuf_selected.set(self.data_holder.get_manufacturer_from_code(sn[0])) + elif major == 'Zipper Board' or major == 'Scintillator Cables': + if barcode[3:9] in ("ZPLMEZ", "ZPLMZ2"): + self.manuf_selected.set(self.data_holder.get_manufacturer_from_batch(major, sn[1], barcode[3:9])) + else: + self.manuf_selected.set("PCBWay-PCBWay") + + if self.manuf_selected.get() == 'None': + self.label_major['text'] = 'Please select manufacturer to continue' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_sub.update() + self.label_sn.update() + self.label_major.update() + self.btn_submit["state"] = "disabled" + return + elif self.user_text.get() == '': + self.btn_submit['state'] = 'disabled' + self.label_major['text'] = 'Please scan barcode to continue' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_sub.update() + self.label_sn.update() + self.label_major.update() + return + + else: + self.btn_submit["state"] = "active" + + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + def show_submit_button_manu(self): + self.data_holder.decode_label(self.ent_full.get()) + + if self.manuf_selected.get() == 'None': + self.label_major['text'] = 'Please select manufacturer to continue' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_sub.update() + self.label_sn.update() + self.label_major.update() + self.btn_submit["state"] = "disabled" + return + + elif self.user_text.get() == '': + self.btn_submit['state'] = 'disabled' + self.label_major['text'] = 'Please scan barcode to continue' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_sub.update() + self.label_sn.update() + self.label_major.update() + return + else: + self.btn_submit["state"] = "active" + + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to disable to the submit button + def hide_submit_button(self): + self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to activate the rescan button + def show_rescan_button(self): + self.btn_rescan["state"] = "active" + + ################################################# + + # Function to disable to the rescan button + def hide_rescan_button(self): + self.btn_rescan["state"] = "disabled" + + ################################################# + + def kill_processes(self): + logger.info("Terminating scanner proceses.") + try: + self.scanner.kill() + self.listener.terminate() + self.EXIT_CODE = 1 + except: + logger.warning("Processes could not be terminated.") diff --git a/VisualInspectionGUI/PythonFiles/Scenes/SplashScene.py b/CheckInGUI/PythonFiles/Scenes/SplashScene.py similarity index 50% rename from VisualInspectionGUI/PythonFiles/Scenes/SplashScene.py rename to CheckInGUI/PythonFiles/Scenes/SplashScene.py index fb188de5..514abc86 100644 --- a/VisualInspectionGUI/PythonFiles/Scenes/SplashScene.py +++ b/CheckInGUI/PythonFiles/Scenes/SplashScene.py @@ -1,15 +1,19 @@ -################################################################################# +################################################################################ # Importing necessary modules import tkinter as tk -import tkinter.font as font +import tkinter.ttk as ttk from PIL import ImageTk as iTK from PIL import Image +import logging +import PythonFiles +import os ################################################################################# +logger = logging.getLogger('HGCAL_VI.PythonFiles.Scenes.SplashScene') -class SplashScene(tk.Frame): +class SplashScene(ttk.Frame): ################################################# @@ -18,31 +22,43 @@ def __init__(self, parent, master_frame): ################################################# + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + def initialize_GUI(self, parent, master_frame): - super().__init__(master_frame, width = 850, height = 500) + super().__init__(master_frame, width = 1105, height = 850) + + self.create_style(parent) # Creating Bethel Logo - img_bethel_logo = Image.open("./PythonFiles/Images/Bethel_Logo.png") - img_bethel_logo = img_bethel_logo.resize((250,100), Image.ANTIALIAS) + img_bethel_logo = Image.open("{}/Images/Bethel_Logo.png".format(PythonFiles.__path__[0])) + img_bethel_logo = img_bethel_logo.resize((250,100), Image.LANCZOS) phimg_bethel_logo = iTK.PhotoImage(img_bethel_logo) - lbl_bethel_logo = tk.Label(self, image=phimg_bethel_logo, width=250, height=100) + lbl_bethel_logo = ttk.Label(self, image=phimg_bethel_logo, width=250) lbl_bethel_logo.image = phimg_bethel_logo lbl_bethel_logo.grid(row=0, column= 0, padx = 50, pady = 100) # Creating UMN Logo - img_umn_logo = Image.open("./PythonFiles/Images/UMN_Logo.png") - img_umn_logo = img_umn_logo.resize((250,100), Image.ANTIALIAS) + img_umn_logo = Image.open('{}/Images/UMN_Logo.png'.format(PythonFiles.__path__[0])) + img_umn_logo = img_umn_logo.resize((250,100), Image.LANCZOS) phimg_umn_logo = iTK.PhotoImage(img_umn_logo) - lbl_umn_logo = tk.Label(self, image=phimg_umn_logo, width=250, height=100) + lbl_umn_logo = ttk.Label(self, image=phimg_umn_logo, width=250) lbl_umn_logo.image = phimg_umn_logo lbl_umn_logo.grid(row = 0 , column = 2, padx = 50, pady = 100) # Creating label for names - lbl_names = tk.Label( + lbl_names = ttk.Label( self, - text = ' Created by:\n \n Bryan Crosman, \n Andrew Kirzeder, \n & \n Garrett Schindler', + text = ' Created by:\n \n Bryan Crosman, \n Andrew Kirzeder, \n Garrett Schindler, \n & \n Rand Bovard', font = ('Arial', 15) ) lbl_names.grid(row = 1, column = 1) @@ -52,4 +68,12 @@ def initialize_GUI(self, parent, master_frame): ################################################# -################################################################################# \ No newline at end of file + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + +################################################################################# diff --git a/CheckInGUI/PythonFiles/Scenes/SummaryScene.py b/CheckInGUI/PythonFiles/Scenes/SummaryScene.py new file mode 100644 index 00000000..266565e0 --- /dev/null +++ b/CheckInGUI/PythonFiles/Scenes/SummaryScene.py @@ -0,0 +1,285 @@ +################################################################################# + +import PythonFiles +import json, logging +import tkinter as tk +import tkinter.ttk as ttk +from PIL import ImageTk as iTK +from PIL import Image +from matplotlib.pyplot import table +from pyparsing import col +import PythonFiles +import os +import datetime + +################################################################################# + +logger = logging.getLogger('HGCAL_VI.PythonFiles.Scenes.SummaryScene') + +# Frame that shows all of the final results +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data + +class SummaryScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + + # Call to the super class's constructor + # Super class is the tk.Frame class + super().__init__(master_frame, width = 1105, height = 850) + + self.create_style(parent) + + self.data_holder = data_holder + + self.parent = parent + + self.create_frame(parent) + + # Fits the frame to set size rather than interior widgets + self.grid_propagate(0) + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def create_frame(self, parent): + try: + for widget in self.winfo_children(): + widget.destroy() + except: + #logger.warning("SummaryScene: Widgets could not be found and/or destroyed (making room for new widgets.") + pass + else: + #logger.info("SummaryScene: Widgets destroyed successfully (making room for new widgets).") + pass + + self.canvas = tk.Canvas(self, width = 1105, height = 850, background = '#33393b') + self.frame = ttk.Frame(self.canvas, width=1105, height=850) + self.scroller = ttk.Scrollbar(self, orient='vertical', command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.scroller.set) + self.canvas.grid(row = 0, column = 0) + self.scroller.grid(row=0, column=1, sticky='NSEW') + self.window = self.canvas.create_window((4,4), window=self.frame, anchor='n', tags='self.frame') + + self.frame.bind('', self.onFrameConfigure) + self.frame.bind('', self.onEnter) + self.frame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + if self.data_holder.data_dict['is_new_board'] == True: + # Adds the title to the Summary Frame + self.title = ttk.Label( + self.frame, + #fg='#0d0d0d', + text = "Board Checked in and Inspection Completed!", + font=('Arial',32,'bold') + ) + self.title.grid(row= 0, column= 1, pady = 50) + + self.id = ttk.Label( + self.frame, + #fg='#0d0d0d', + text = "Check In ID:" + str(self.data_holder.data_dict['in_id']), + font=('Arial',24,'bold') + ) + self.id.grid(row= 1, column= 1, pady = 20) + + else: + # Adds the title to the Summary Frame + self.title = ttk.Label( + self.frame, + #fg='#0d0d0d', + text = "Inspection Completed!", + font=('Arial',36,'bold') + ) + self.title.grid(row= 0, column= 1, pady = 20) + + + # Adds Board full id to the SummaryFrame + self.lbl_full = ttk.Label( + self.frame, + text = "Full ID: " + str(self.data_holder.data_dict['current_full_ID']), + font=('Arial', 18) + ) + self.lbl_full.grid(column = 1, row = 2, pady = 10) + + + # Adds Tester Name to the Summary Frame + self.lbl_tester = ttk.Label( + self.frame, + text = "User: " + self.data_holder.data_dict['user_ID'], + font=('Arial', 18) + ) + self.lbl_tester.grid(column = 1, row = 3, pady = 10) + + # Adds comments to the Summary Frame + self.lbl_com = ttk.Label( + self.frame, + text = "Comments: " + self.data_holder.data_dict['comments'], + font=('Arial', 18) + ) + self.lbl_com.grid(column = 1, row = 4, pady = 10) + + # Adds time to the Summary Frame + self.lbl_time = ttk.Label( + self.frame, + text = str(datetime.datetime.now()), + font=('Arial', 18) + ) + self.lbl_time.grid(column = 1, row = 5, pady = (15, 150)) + + # Creates the "table" as a frame object + self.frm_table = ttk.Frame(self.frame) + self.frm_table.grid(row = 8, column = 1, sticky = 's') + + # Where to start putting the JSON information + starting_row = 4 + # Number of keys the data_holder.inspection_data dictionary + key_count = 0 + + # Loop through all of the keys in the data_holder.inspection_data dictionary + for index,box in enumerate(self.data_holder.all_checkboxes[0]): + key_count = key_count + 1 + + key_label = ttk.Label( + self.frm_table, + text = box['text'], + #relief = 'ridge', + width=40, + #height=1, + font=('Arial', 30, "bold") + ) + key_label.grid(row = key_count , sticky = 'sw', column=1, padx = 15) + + # Correctly displays the booleans + # If not a string, show as a boolean true/false + l_text = "UNDEFINED" + if not isinstance(box['value'], str): + if (box['value']): + l_text = "True" + else: + l_text = "False" + else: + l_text = value['value'] + + result_label = ttk.Label( + self.frm_table, + text = l_text, + #relief = 'ridge', + width=40, + #height=1, + font=('Arial', 30, "bold") + ) + result_label.grid(row=key_count, column=3) + + comment_index = 0 + comment_title_text = "Comments:" + comment_title = ttk.Label( + self.frm_table, + text = comment_title_text, + #relief = 'ridge', + width=40, + #height=2, + font=('Arial', 24, "bold") + ) + comment_title.grid(row=key_count + 1, column=0) + + comment_text = str(self.data_holder.get_comment_dict(comment_index)) + comment_label = ttk.Label( + self.frm_table, + text = comment_text, + #relief = 'ridge', + width=40, + #height=2, + font=('Arial', 18, "bold") + ) + comment_label.grid(row=key_count + 1, column=1) + + # Create frame for logout button + nav_frame = ttk.Frame(self) + nav_frame.grid(column = 3, row = 0, sticky = 'se', padx =5) + + #creating the next board button + next_board_button = ttk.Button( + nav_frame, + #relief = tk.RAISED, + text = "Submit and go to Next Board", + command = lambda: self.btn_NextBoard_action(parent) + ) + next_board_button.grid(pady = 5,row=key_count+2, column=1, padx = 5) + + #creating redo check in button + redo_checkin_button = ttk.Button( + nav_frame, + text = "Cancel and Start Over", + command = lambda: self.btn_redo_action(parent) + ) + redo_checkin_button.grid(pady = 5,row=key_count+3, column=1, padx = 5) + + # Creating the logout button + btn_logout = ttk.Button( + nav_frame, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.grid(row=key_count+4,pady = 5, column=1, padx = 5) + + + + ################################################# + + def btn_NextBoard_action(self, parent): + test_id = self.data_holder.send_to_DB() + if test_id: + parent.set_frame_scan_frame() + + def btn_logout_action(self, parent): + parent.set_frame_login_frame() + + def btn_redo_action(self, parent): + parent.set_frame_scan_frame() + + def get_submit_action(self): + return self.btn_NextBoard_action + + def get_parent(self): + return self.parent + + def onFrameConfigure(self, event): + self.canvas.configure(scrollregion=self.canvas.bbox('all')) + + def onMouseWheel(self, event): + if event.num == 4: + self.canvas.yview_scroll(-1, 'units') + elif event.num == 5: + self.canvas.yview_scroll(1, 'units') + def onEnter(self, event): + self.canvas.bind_all('', self.onMouseWheel) + self.canvas.bind_all('', self.onMouseWheel) + + def onLeave(self, event): + self.canvas.unbind_all('') + self.canvas.unbind_all('') + + ################################################# + + def update_frame(self): + self.create_frame(self.parent) + + + +################################################################################# diff --git a/CheckInGUI/PythonFiles/__init__.py b/CheckInGUI/PythonFiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CheckInGUI/PythonFiles/update_config.py b/CheckInGUI/PythonFiles/update_config.py new file mode 100644 index 00000000..a5a52e92 --- /dev/null +++ b/CheckInGUI/PythonFiles/update_config.py @@ -0,0 +1,32 @@ +from PythonFiles.GUIConfig import GUIConfig +import yaml +from pathlib import Path + + +def update_config(full_id): + + # sets the config to wagon or engine based on the 4th character of the board's full id + if full_id[3] in ('W', 'S'): + #from TestConfigs.Wagon_cfg import masterCfg + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Wagon_cfg.yaml")) + board_cfg = masterCfg + + elif full_id[3] == 'E': + #from TestConfigs.Engine_cfg import masterCfg + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Engine_cfg.yaml")) + board_cfg = masterCfg + + elif full_id[3:9] in ('ZPLMEZ','ZPLMZ2'): + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Engine_cfg.yaml")) + board_cfg = masterCfg + + else: + #from TestConfigs.Wagon_cfg import masterCfg + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Wagon_cfg.yaml")) + board_cfg = masterCfg + + return GUIConfig(board_cfg) + +def import_yaml(filename): + + return yaml.safe_load(filename) diff --git a/CheckInGUI/README.md b/CheckInGUI/README.md new file mode 100644 index 00000000..275ddea2 --- /dev/null +++ b/CheckInGUI/README.md @@ -0,0 +1,35 @@ +# Visual Inspection and Check In GUI + +Graphical user interface responsible for running the visual board inspections of both wagons and engines. The interface is largely based on the [HGCAL Tester GUI](https://github.com/UMN-CMS/HGCALTestGUI). Instead of running tests on a wagon/engine test stand, the Visual Inspection GUI utilizes a camera to take a photo of the specified electrical board. This information is then stored in a database for data monitoring. This software is specifically built for use on [Raspberry Pi OS](https://www.raspberrypi.com/software/) but all packages work across Windows and Linux devices with some tweaks. + +## Package Installation +_You must use python3_. To install all of the dependancies for this project, you will need to run the following installation commands. Notice that the versioning for the opencv is important (this is to prevent extensive installation times). +``` +# If permissions denied, add a 'sudo' before each line +pip3 install tkinter +apt-get install python3-pil python3-pil.imagetk +apt install python3-matplotlib python3-tk +pip3 install pyzmq +pip3 install -U numpy +apt install -y python3-picamera2 + +# Version type matters here +pip3 install opencv-contrib-python==4.5.3.56 +``` + +### Extra Camera Installation +If you are using a Raspberry Pi Camera Module 3, you will need to make extra installations in order to run the code. Documentation links are found below: + +- [The Picamera2 Library](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf?_gl=1*seefj*_ga*MTQ0NTI3MzQ3OC4xNjg5ODYwNjkw*_ga_22FD70LWDS*MTY4OTg2MjM2Ny4xLjEuMTY4OTg2MzMyOS4wLjAuMA..) +- [libcamera Documentation](https://www.raspberrypi.com/documentation/computers/camera_software.html#getting-started) +- [Updating Raspberry Pi OS](https://www.raspberrypi.com/documentation/computers/os.html#using-apt) + + + + +## Running the GUI +After all of the packages have been installed and the camera is plugged into the Raspberry Pi, you will be able to run the program. +``` +cd ~/path/to/CheckInGUI +python3 MainFunctionVI.py + diff --git a/Configs/Demo_Local_cfg.yaml b/Configs/Demo_Local_cfg.yaml new file mode 100644 index 00000000..5acab778 --- /dev/null +++ b/Configs/Demo_Local_cfg.yaml @@ -0,0 +1,88 @@ +--- + +# Specify which board you want to test here +GUIType: Demo + +# Specify if a scanner is used for barcodes +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# Uncomment the test handler which you would like to use +# +# Note that for SSH, the username and hostname must be specified. +# It is assumed that you have set up SSH key access for this username +# On the specified host. +# +# ZMQ will use the built-in request server and client. +# The IP address of the GUI node and testing node must be specified separately +# Additionally, SSH key access removes the need to start the server +# on the tester by hand. Path and server file name needed for remote +# server start up +# +TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +#TestHandler: {name: ZMQ, localip: , remoteip: , username: , serverpath: , serverscript: } + + +# Let the GUI know if you want to check serial numbers for multiple board types at a single testing location +SerialCheckSafe: false + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py +- name: Counting2 + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting2 + TestPath: Tests + TestScript: demo_count.py + +######################################## +# NOTE TO SELF: Add in test sequence as # +# a separate entry in config # +# All tests in one area, not physical # +# vs. automatic and add a test type # +# # +######################################## + + +# Tests which require a physical measurement +# The pass fail criteria for these tests are specified here +PhysicalTest: + - name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + ################################# + # REMOVE FOR DEMO # + ################################# + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: false + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" + +People: + - Caleb + +... diff --git a/Configs/Demo_SSH_cfg.yaml b/Configs/Demo_SSH_cfg.yaml new file mode 100644 index 00000000..42026ecf --- /dev/null +++ b/Configs/Demo_SSH_cfg.yaml @@ -0,0 +1,71 @@ +--- +#----------------------------------------------- +# Note: Demo will not work out of the box +# Need to follow installation procedure in +# README.md +#----------------------------------------------- + +# Specify which board you want to test here +# this doesn't actually matter much, the GUI type will update based on the board type entered in scan scene +GUIType: Demo + +# Specify if a scanner is used for barcodes +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# Uncomment the test handler which you would like to use +# +# Note that for SSH, the username and hostname must be specified. +# It is assumed that you have set up SSH key access for this username on the specified host. +# +# ZMQ will use the built-in request server and client. +# The IP address of the GUI node and testing node must be specified separately +# Additionally, SSH key access removes the need to start the server +# on the tester by hand. Path and server file name needed for remote +# server start up +#TestHandler: {name: Local, remoteip: localhost} +TestHandler: {name: SSH, username: bovar008, hostname: cmsfactory2, remoteip: } +#TestHandler: {name: ZMQ, localip: , remoteip: , username: , serverpath: , serverscript: } + +# Let the GUI know if you want to check serial numbers for multiple board types at a single testing location +SerialCheckSafe: false + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestPath and TestScript aren't used in SSH +# TestCommand field is the command run in SSH, -u is needed for realtime output from a python script +# TestPath should be in reference to the testing home directory +Test: +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py + TestCommand: python3 -u HGCALTestGUI/Tests/demo_count.py + TestConfig: HGCALTestGUI/Tests/test_configs/counting.yaml + +# Tests which require a physical measurement +# The pass fail criteria for these tests are specified here +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: false + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/Configs/Engine_cfg.py b/Configs/Engine_cfg.py new file mode 100644 index 00000000..0afac8a1 --- /dev/null +++ b/Configs/Engine_cfg.py @@ -0,0 +1,223 @@ +base_path = "/home/HGCAL_dev/test_scripts" + +from dumpToYaml import dump_to_yaml + +masterCfg = { + + "GUIType": "Engine", + + "UsingScanner": True, + + + # Order of tests matters here + # This should be the same order that you want the tests to be run in + # Number of test will also be decide by this list so don't miss any + "Test": [ + { + "name": "Power-Ground Resistance", + "required": 1, + "desc_short": "Measure resistance between power and ground", + "desc_long": "Check that the power and grounds are not shorted at the terminal, or between the inputs.", + "TestClass" : "TestPowerGround", + }, + { + "name": "1.5V Input Check", + "required": 1, + "desc_short": "Check that the 1.5V input is not shorted.", + "desc_long": "Check that resistance between across C906 or C908 is non-zero.", + "TestClass" : "Test1.5VInput", + }, + { + "name": "10V Input Check", + "required": 1, + "desc_short": "Check that the 10V input is not shorted.", + "desc_long": "Check that resistance between across C907 or C909 is non-zero.", + "TestClass" : "Test10VInput", + }, + { + "name": "1.2V Output Check", + "required": 1, + "desc_short": "Check that the 1.2V output is not shorted.", + "desc_long": "Check that resistance between across C904 or C904 or TP901 is non-zero.", + "TestClass" : "Test1.2VOutput", + }, + { + "name": "RX 2.5V Output Check", + "required": 1, + "desc_short": "Check that the RX 2.5V output is not shorted.", + "desc_long": "Check that resistance across C902 is non-zero.", + "TestClass" : "TestRX2.5VOutput", + }, + { + "name": "TX 2.5V Output Check", + "required": 1, + "desc_short": "Check that the TX 2.5V output is not shorted.", + "desc_long": "Check that resistance across either C903 or TP902 is non-zero.", + "TestClass" : "TestTX2.5VOutput", + }, + # Power on Tests + { + "name": "LDO Output", + "required": 1, + "desc_short": "Check that the LDO output voltage is around 1.2V", + "desc_long": "Measure the votlage across either R911 or TP901 and verify that it is appropriate.", + "TestClass" : "TestLDOOutput", + }, + { + "name": "LinPol RX Check", + "required": 1, + "desc_short": "Check that the RX voltage from the linppol is operating correctly", + "desc_long": "Check that voltages across either R905 or R902 is 2.5V.", + "TestClass" : "TestLinPolRX", + }, + { + "name": "LinPol TX Check", + "required": 1, + "desc_short": "Check that the TX voltage from the linppol is operating correctly", + "desc_long": "Measure the voltage across either TP902 or R906 or C903 is 2.5V.", + "TestClass" : "TestLinPolTX", + }, + + #Operations Tests + { + "name": "X_PWR", + "required": 1, + "desc_short": "Check the the X_PWR voltage is correct.", + "desc_long": "Measure using the tester, and should find approximately 1.2V.", + "TestClass" : "TestXPWR", + }, + { + "name": "lpGBT setup", + "required": 1, + "desc_short": "Ensure setup can be performed", + "desc_long": "Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz", + "TestClass" : "TestlpGBTsetup", + }, + { + "name": "EClock Rates", + "required": 1, + "desc_short": "Ensure EClock rates are correct", + "desc_long": "Check that all EClocks are running at 320MHz.", + "TestClass" : "TestEClock", + }, + { + "name": "lpGBT IC/EC communication", + "required": 1, + "desc_short": "Check operability of lpGBT IC/EC communication", + "desc_long": "Read and write to lpBGT registers via ICEC. Check DAQ lpGBT read of registers via IC. Check Trigger lpGBTs: successful read registers via EC. Ensure write and readback to user ID registers (0x004 - 0x007)", + "TestClass" : "TestlpGBTcom", + }, + { + "name": "I2C", + "required": 1, + "desc_short": "Engine can use I2C master", + "desc_long": "Check that engine can communicate as an I2C master", + #"TestScript": "engine_test_suite.py", + "TestClass" : "TestI2C", + + }, + { + "name": "GPIO functionality", + "required": 1, + "desc_short": "Check the quality of the GPIOs", + "desc_long": "Read and write to all GPIO channels and verify levels. Write nominal configuration and then toggle each line one-by-one and verify change in both lpGBT status and voltage level", + "TestClass" : "TestGpio" + }, + { + "name": "ADC functionality", + "required": 1, + "desc_short": "Check quality of the ADCs", + "desc_long": "Measure known voltages/resistances. Check measured values for all 4 gains within tolerances, (only need to do all 4 gains for one measurement).", + "TestClass" : "TestAdc" + }, + { + "name": "Uplink quality", + "required": 1, + "desc_short": "Check the quality of the uplinks", + "desc_long": "PRBS validation from lpGBTs. Check bit error rate below threshold.", + "TestClass" : "TestUplink" + }, + { + "name": "Downlink quality", + "required": 1, + "desc_short": "Check the quality of the downlinks", + "desc_long": "Eye opening test. Check eye opening width and height below threshold.", + "TestClass" : "TestDownlink", + }, + { + "name": "Fast Command quality", + "required": 1, + "desc_short": "Check the quality of the Fast Command path", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestFC" + }, + { + "name": "Elink quality", + "required": 1, + "desc_short": "Check the quality of the elinks", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestElinkUp" + }, + { + "name": "Crossover link quality", + "required": 1, + "desc_short": "Check the quality of the crossover links", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestCrossover", + }, + ], + + + "PhysicalTest": [ + #{ + # "name": "SAMPLE test", + # "required": 1, + # "desc_short": "Some short description", + # "desc_long": "Really long description for later purposes.", + # "criteria": { + # "first testing criteria", + # "second testing criteria", + # "third testing criteria", + # }, + + #}, + + ], + + "Board_type": [ + { + "name": "Engine V3 Right", + "type_sn": "100300", + "requiredTests": [0, 1, 2, 3, 4], + }, + { + "name": "Engine V3 Left", + "type_sn": "100310", + "requiredTests": [0, 1, 2, 3, 4], + }, + ], + # People who you would like to add as testers by default + # HGCAL_dev can be used for debug testing in the beginning + # The GUI will require everyone to have their own "account" + "People": [ + "Nadja", + "Charlie", + "Bryan", + "Devin", + "HGCAL_dev", + ], + # Information for sending and receiving data to/from the database + # Needs to be different based on board type + "DBInfo": { + "use_database": True, + "name": "EngineDB", + "reader": "EngineDBReadUser", + "inserter": "EngineDBInserter", + "admin": "EngineDBInserter", + "baseURL": "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/EngineDB", + }, +} + +masterCfg["Test"] = [dict(**x, TestPath=base_path, TestScript= "engine_test_suite.py") for x in masterCfg["Test"] if "TestClass" in x] + +dump_to_yaml(masterCfg) diff --git a/Configs/Flex_cfg.yaml b/Configs/Flex_cfg.yaml new file mode 100644 index 00000000..625d74f7 --- /dev/null +++ b/Configs/Flex_cfg.yaml @@ -0,0 +1,32 @@ +--- +DBInfo: + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: WagonDBInserter + name: WagonDB + reader: WagonDBReadUser + use_database: True +GUIType: FlexCable +SerialCheckSafe: false +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: "TEST_SERVER_IP_ADDRESS", username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/FlexTest/FlexCableTesting + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: Flex Cable Resistance Test + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/FlexTest/FlexCableTesting + TestScript: run_bert_tmp.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Flex Cable Bit Error Rate Test + required: 1 +UsingScanner: true + +People: [Lauren, Billy] +... diff --git a/Configs/HD_Engine_cfg.yaml b/Configs/HD_Engine_cfg.yaml new file mode 100644 index 00000000..705fe0d4 --- /dev/null +++ b/Configs/HD_Engine_cfg.yaml @@ -0,0 +1,133 @@ +GUIType: Engine + +People: [Nadja, Charlie, Bryan, Devin, HGCAL_dev] + +Board_type: + - name: Engine V3 Right + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100300' + - name: Engine V3 Left + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100310' + +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/EngineDB + inserter: FactoryInserter + name: EngineDB_PRO + reader: FactoryReadUser + use_database: true + + +SerialCheckSafe: true +UsingScanner: true + +# TestHandler: {name: Local, remoteip: localhost} +# TestHandler: {name: SSH, username: HGCAL_dev, hostname: cmstester5, remoteip: } +TestHandler: {name: ZMQ, localip: "localhost", remoteip: "TEST_SERVER_IP_ADDRESS", username: "test_server", serverpath: , serverscript: } + + +PhysicalTest: [] +Test: + - TestClass: TestXPWR + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Measure using the tester, and should find approximately 1.2V. + desc_short: Check the the X_PWR voltage is correct. + name: X_PWR + required: 1 + + - TestClass: TestSetupLpgbt + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus + (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All + 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz + desc_short: Ensure setup can be performed + name: lpGBT setup + required: 1 + + - TestClass: TestCurrent + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Measure the current drawn on the engine. + desc_short: Measure the current drawn on the engine. + name: Current Draw + required: 1 + + - TestClass: TestLpgbtId + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Read lpgbt ids + desc_short: Check ids + name: LPGBT ID + required: 1 + + - TestClass: TestI2C + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_i2c.py + desc_long: Check that engine can communicate as an I2C master + desc_short: Engine can use I2C master + name: I2C + required: 1 + + - TestClass: TestGpio + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_gpio.py + desc_long: Read and write to all GPIO channels and verify levels. Write nominal + configuration and then toggle each line one-by-one and verify change in both + lpGBT status and voltage level + desc_short: Check the quality of the GPIOs + name: GPIO functionality + required: 1 + + - TestClass: TestAdc + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_adc.py + desc_long: Measure known voltages/resistances. Check measured values for all 4 + gains within tolerances, (only need to do all 4 gains for one measurement). + desc_short: Check quality of the ADCs + name: ADC functionality + required: 1 + + - TestClass: TestUplink + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from lpGBTs. Check bit error rate below threshold. + desc_short: Check the quality of the uplinks + name: Uplink quality + required: 1 + + + - TestClass: TestFC + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the Fast Command path + name: Fast Command quality + required: 1 + + - TestClass: TestEyeOpening + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: Test the size of the eye opening to determine if it sufficiently large + desc_short: Check size of eye opening + name: Eye Opening + required: 1 + + - TestClass: TestElinkUp + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the elinks + name: Elink quality + required: 1 + + - TestClass: TestEClockRates + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: Check that all EClocks are running at 320MHz. + desc_short: Ensure EClock rates are correct + name: EClock Rates + required: 1 + diff --git a/Configs/HD_Wagon_cfg.yaml b/Configs/HD_Wagon_cfg.yaml new file mode 100644 index 00000000..5bb7097f --- /dev/null +++ b/Configs/HD_Wagon_cfg.yaml @@ -0,0 +1,123 @@ +--- +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: FactoryInserter + name: WagonDB_PRO + reader: FactoryReadUser + use_database: true +GUIType: Wagon +UsingScanner: true +SerialCheckSafe: false +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: HGCAL_dev, hostname: "TEST_SERVER_IP_ADDRESS", remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: umn-zcu102-d, username: , serverpath: , serverscript: } + +PhysicalTest: [] +Test: +- TestClass: GPIOtest + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: gpio_test.py + desc_long: Tests that the GPIO pins behave as expected + desc_short: Tests the GPIO pins + name: GPIO Test + required: 1 +- TestClass: Analogtest + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: analog_test.py + desc_long: Tests that the Analog pins behave as expected + desc_short: Tests the Analog pins + name: Analog Test + required: 1 +- TestClass: StartupPowerCheck + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: startup_power_check.py + desc_long: Resets ECONs and checks the power draw of each ECON + desc_short: Check power draw of wagon + name: Startup Power Check + required: 1 +- TestClass: I2CToECON + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: i2c_to_econ.py + desc_long: Check that each ECON on the wagon can be communicated with via i2c + desc_short: Test i2c to each ECON + name: I2C to ECON +- TestClass: ResetLineTest + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: resets_test.py + desc_long: Test hard and soft resets of each ECON and then setup the ECONs + desc_short: Test ECON resets and setup. + name: Reset Lines Test +- TestClass: ConfigPowerCheck + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: config_power_check.py + desc_long: Checks the power draw of each ECON post config (no resetting) + desc_short: Check power draw of wagon without resetting + name: Config Power Check + required: 1 +- TestClass: I2CToKRIA + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: i2c_to_kria.py + desc_long: Check that each KRIA can be communicated with via i2c + desc_short: Test i2c to each KRIA + name: I2C to KRIA + required: 1 +- TestClass: FCECONtest + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: fc_to_econs.py + desc_long: Check that each of the ECONs is receiving fast control + desc_short: Check ECON FC + name: Fast Control to ECONs Test + required: 1 +- TestClass: FCMODtest + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: fc_to_modules.py + desc_long: Check that each of the KRIAs is receiving fast control + desc_short: Check KRIA FC + name: Fast Control to Modules Test + required: 1 +- TestClass: ErrorPinTest + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: error_pin.py + desc_long: Tests that the ECON-D is receiving the ECON-T error pin + desc_short: Tests ECON-T error pin sent to ECON-D + name: Error Pin Test + required: 1 +- TestClass: TestEconTtoEngine + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: test_econ_links.py + desc_long: + desc_short: + name: ECON-T Elinks To Engine + required: 1 +- TestClass: TestEconDtoEngine + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: test_econ_links.py + desc_long: + desc_short: + name: ECON-D Elinks To Engine + required: 1 +- TestClass: TestEconDFromModule + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: test_econ_links.py + desc_long: + desc_short: + name: ECON-D Elinks From Module + required: 1 +- TestClass: TestEconTFromModule + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: test_econ_links.py + desc_long: + desc_short: + name: ECON-T Elinks From Module + required: 1 +InspectionTest: +- TestClass: Inspection + name: Inspection1 + required: 1 + checkboxes: + - {text: "Is the board bent?", value: False, requirement: False} + - {text: "Is the board visibly broken?", value: False, requirement: False} + - {text: "Are any components missing?", value: False, requirement: False} + - {text: "Are any components visibly broken?", value: False, requirement: False} + comments: No Comment diff --git a/Configs/LD_4Mod_Wagon_cfg.yaml b/Configs/LD_4Mod_Wagon_cfg.yaml new file mode 100644 index 00000000..6a248983 --- /dev/null +++ b/Configs/LD_4Mod_Wagon_cfg.yaml @@ -0,0 +1,92 @@ +--- +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: FactoryInserter + name: WagonDB_PRO + reader: FactoryReadUser + use_database: true +GUIType: Wagon +SerialCheckSafe: false +UsingScanner: true +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: HGCAL_dev, hostname: "TEST_SERVER_IP_ADDRESS", remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: "TEST_SERVER_IP_ADDRESS", username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: ADC + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_adc_self_test.py + desc_long: Checking all ADCs to see if they are running as expected before running any tests + desc_short: ADC interal test + name: ADC Self Test + required: 1 +- TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: wagon_rtd.py + desc_long: Test must be completed before attempting to measure ID resistor + desc_short: Measure resistance of analog lines + name: Resistance Measurement + required: 1 +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: ID Resistor Measurement + required: 1 +- TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_iic_check.py + desc_long: Test must be completed before BERT for wagon wheel configuration + desc_short: Check I2C read/write along wagon + name: I2C Read/Write + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_bert.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Bit Error Rate Test + required: 1 +- TestClass: Mod4LMezzComm + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_mod4_tests.py + desc_long: "LMezz LPGBT Comm" + desc_short: "LMezz LPGBT Comm" + name: "Mod4 LMezz Comm" + required: 1 +- TestClass: Mod4LMezzId + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_mod4_tests.py + desc_long: "LMezz LPGBT Id" + desc_short: "LMezz LPGBT Id" + name: "Mod4 LMezz Id" + required: 1 +- TestClass: Mod4Resistance + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_mod4_tests.py + desc_long: "Mod4 Resistance" + desc_short: "Mod4 Resistance" + name: "Mod4 Resistance" + required: 1 +- TestClass: Mod4Reset + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_mod4_tests.py + desc_long: "Mod4 Reset" + desc_short: "Mod4 Reset" + name: "Mod4 Reset" + required: 1 + + +UsingScanner: true +InspectionTest: +- TestClass: Inspection + name: Inspection1 + required: 1 + checkboxes: + - {text: "Is the board bent?", value: False, requirement: False} + - {text: "Is the board visibly broken?", value: False, requirement: False} + - {text: "Are any components missing?", value: False, requirement: False} + - {text: "Are any components visibly broken?", value: False, requirement: False} + comments: No Comment diff --git a/Configs/LD_Engine_cfg.yaml b/Configs/LD_Engine_cfg.yaml new file mode 100644 index 00000000..0c626bb1 --- /dev/null +++ b/Configs/LD_Engine_cfg.yaml @@ -0,0 +1,140 @@ +GUIType: Engine + +People: [Nadja, Charlie, Bryan, Devin, HGCAL_dev] + +Board_type: + - name: Engine V3 Right + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100300' + - name: Engine V3 Left + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100310' + +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/EngineDB + inserter: FactoryInserter + name: EngineDB_PRO + reader: FactoryReadUser + use_database: true + + +SerialCheckSafe: true +UsingScanner: true + +# TestHandler: {name: Local, remoteip: localhost} +# TestHandler: {name: SSH, username: HGCAL_dev, hostname: cmstester5, remoteip: } +TestHandler: {name: ZMQ, localip: "localhost", remoteip: "TEST_SERVER_IP_ADDRESS", username: "test_server", serverpath: , serverscript: } + + +PhysicalTest: [] +Test: + - TestClass: TestXPWR + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Measure using the tester, and should find approximately 1.2V. + desc_short: Check the the X_PWR voltage is correct. + name: X_PWR + required: 1 + + - TestClass: TestSetupLpgbt + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus + (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All + 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz + desc_short: Ensure setup can be performed + name: lpGBT setup + required: 1 + + - TestClass: TestCurrent + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Measure the current drawn on the engine. + desc_short: Measure the current drawn on the engine. + name: Current Draw + required: 1 + + - TestClass: TestLpgbtId + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Read lpgbt ids + desc_short: Check ids + name: LPGBT ID + required: 1 + + - TestClass: TestI2C + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_i2c.py + desc_long: Check that engine can communicate as an I2C master + desc_short: Engine can use I2C master + name: I2C + required: 1 + + - TestClass: TestGpio + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_gpio.py + desc_long: Read and write to all GPIO channels and verify levels. Write nominal + configuration and then toggle each line one-by-one and verify change in both + lpGBT status and voltage level + desc_short: Check the quality of the GPIOs + name: GPIO functionality + required: 1 + + - TestClass: TestAdc + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_adc.py + desc_long: Measure known voltages/resistances. Check measured values for all 4 + gains within tolerances, (only need to do all 4 gains for one measurement). + desc_short: Check quality of the ADCs + name: ADC functionality + required: 1 + + - TestClass: TestUplink + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from lpGBTs. Check bit error rate below threshold. + desc_short: Check the quality of the uplinks + name: Uplink quality + required: 1 + + + - TestClass: TestFC + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the Fast Command path + name: Fast Command quality + required: 1 + + - TestClass: TestEyeOpening + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: Test the size of the eye opening to determine if it sufficiently large + desc_short: Check size of eye opening + name: Eye Opening + required: 1 + + - TestClass: TestElinkUp + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the elinks + name: Elink quality + required: 1 + + - TestClass: TestEClockRates + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: Check that all EClocks are running at 320MHz. + desc_short: Ensure EClock rates are correct + name: EClock Rates + required: 1 + + - TestClass: CrossoverLink + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the crossover links + name: Crossover link quality + required: 1 diff --git a/Configs/LD_Wagon_cfg.yaml b/Configs/LD_Wagon_cfg.yaml new file mode 100644 index 00000000..afe7312e --- /dev/null +++ b/Configs/LD_Wagon_cfg.yaml @@ -0,0 +1,61 @@ +--- +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: FactoryInserter + name: WagonDB_PRO + reader: FactoryReadUser + use_database: true +GUIType: Wagon +SerialCheckSafe: true +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: HGCAL_dev, hostname: "TEST_SERVER_IP_ADDRESS", remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: "TEST_SERVER_IP_ADDRESS", username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: ADC + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_adc_self_test.py + desc_long: Checking all ADCs to see if they are running as expected before running any tests + desc_short: ADC interal test + name: ADC Self Test + required: 1 +- TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: wagon_rtd.py + desc_long: Test must be completed before attempting to measure ID resistor + desc_short: Measure resistance of analog lines + name: Resistance Measurement + required: 1 +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: ID Resistor Measurement + required: 1 +- TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_iic_check.py + desc_long: Test must be completed before BERT for wagon wheel configuration + desc_short: Check I2C read/write along wagon + name: I2C Read/Write + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/sw/WagonTesting + TestScript: run_bert.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Bit Error Rate Test + required: 1 +UsingScanner: true +InspectionTest: +- TestClass: Inspection + name: Inspection1 + required: 1 + checkboxes: + - {text: "Is the board bent?", value: False, requirement: False} + - {text: "Is the board visibly broken?", value: False, requirement: False} + - {text: "Are any components missing?", value: False, requirement: False} + - {text: "Are any components visibly broken?", value: False, requirement: False} + comments: No Comment diff --git a/Configs/Mezz_cfg.yaml b/Configs/Mezz_cfg.yaml new file mode 100644 index 00000000..4f009c24 --- /dev/null +++ b/Configs/Mezz_cfg.yaml @@ -0,0 +1,71 @@ +GUIType: Engine + +People: [Sam, HGCAL_dev] + +Board_type: + - name: LPGBT Mezzanine + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100300' + +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/EngineDB + inserter: FactoryInserter + name: EngineDB_PRO + reader: FactoryReadUser + use_database: true + + +SerialCheckSafe: true +UsingScanner: true + +# TestHandler: {name: Local, remoteip: localhost} +# TestHandler: {name: SSH, username: HGCAL_dev, hostname: cmstester5, remoteip: } +TestHandler: {name: ZMQ, localip: "localhost", remoteip: "TEST_SERVER_IP_ADDRESS", username: "test_server", serverpath: , serverscript: } + + +PhysicalTest: [] +Test: + - TestClass: lpGBT Mezz Setup + TestPath: /home/test_server/test_code/mezz_tests + TestScript: setup_test_sam.py + desc_long: Perform Read, Write operations on Tester and Mezzanine. Check that the lpGBT can be setup and read back correctly. + desc_short: Check Mezz and Tester Setup. + name: lpGBT Mezz Setup + required: 1 + + - TestClass: lpGBT Mezz ID + TestPath: /home/test_server/test_code/mezz_tests + TestScript: id_test_sam.py + desc_long: Read lpgbt id + desc_short: Check id + name: lpGBT Mezz ID + required: 1 + + - TestClass: lpGBT Mezz Power + TestPath: /home/test_server/test_code/mezz_tests + TestScript: power_test_sam.py + desc_long: Measure the power drawn on the Mezzanine. + desc_short: Measure the power drawn on the Mezzanine. + name: lpGBT Mezz Power + required: 1 + + - TestClass: lpGBT Mezz GPIO + TestPath: /home/test_server/test_code/mezz_tests + TestScript: gpio_test_sam.py + desc_long: Read and write to all GPIO channels and verify levels. Write nominal + configuration and then toggle each line one-by-one and verify change in both + lpGBT status and voltage level + desc_short: Check the quality of the GPIOs + name: lpGBT Mezz GPIO + required: 1 + + - TestClass: lpGBT Mezz ADC + TestPath: /home/test_server/test_code/mezz_tests + TestScript: adc_test_sam.py + desc_long: Measure known voltages/resistances. Check measured values for all 8 + gains within tolerances, (only need to do all 8 gains for one measurement). + desc_short: Check quality of the ADCs + name: lpGBT Mezz ADC + required: 1 + diff --git a/Configs/Skeleton_cfg.yaml b/Configs/Skeleton_cfg.yaml new file mode 100644 index 00000000..2968c97c --- /dev/null +++ b/Configs/Skeleton_cfg.yaml @@ -0,0 +1,67 @@ +--- + +# Specify which board you want to test here +GUIType: Wagon + +# Specify if a scanner is used for barcodes +UsingScanner: true + +# How the tests will be run (local, ssh, or ZMQ) +TestHandler: ZMQ + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: +- name: Resistance Measurement + required: true + desc_short: Measure resistance of analog lines + desc_long: Test must be completed before attempting to measure ID resistor + TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py +- name: ID Resistor Measurement + required: true + desc_short: Measure resistance of ID resistor + desc_long: Must be completed after the general resistance measurement + TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py +- name: I2C Read/Write + required: true + desc_short: Check I2C read/write along wagon + desc_long: Test must be completed before BERT for wagon wheel configuration + TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw + TestScript: run_iic_check.py +- name: Bit Error Rate Test + required: true + desc_short: Determine quality of data transmission + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + TestClass: BERT + TestPath: /home/HGCAL_dev/sw + TestScript: run_bert.py + + +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: true + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/Configs/Thermal_Cy_cfg.yaml b/Configs/Thermal_Cy_cfg.yaml new file mode 100644 index 00000000..316b3a83 --- /dev/null +++ b/Configs/Thermal_Cy_cfg.yaml @@ -0,0 +1,64 @@ +--- + +# Specify which board you want to test here +GUIType: Thermal + +# Specify if a scanner is used for barcodes +SerialCheckSafe: false +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# TestHandler: {name: Local, remoteip: localhost} + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: [] + + +PhysicalTest: [] +# PhysicalTest: +# - name: Blank Test +# required: false +# desc_short: Some short description of the test +# desc_long: Long description of the test +# criteria: +# - first testing criteria +# - second testing criteria +# - third testing criteria + +# TestHandler: {name: Local, remoteip: localhost} +# TestHandler: {name: SSH, username: , hostname: , remoteip: } +# TODO ZMQ update to accurate hosts and IP addresses + +# # For testing purposes only +# TestHandler: { +# name: Demo +# } + +TestHandler: { + name: ThermalZMQ, + localip: "localhost", + remoteip: "zcu102b", + username: "test_server", + serverpath: , + serverscript: + } + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type + +# TODO To be updated with new DB info +DBInfo: + use_database: true + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab1.spa.umn.edu/Factory/EngineDB" + +People: + - Garrett +... diff --git a/Configs/Wagon_cfg.py b/Configs/Wagon_cfg.py new file mode 100644 index 00000000..4099544d --- /dev/null +++ b/Configs/Wagon_cfg.py @@ -0,0 +1,85 @@ +from dumpToYaml import dump_to_yaml + +masterCfg = { + + "GUIType": "Wagon", + + "UsingScanner": True, + + # Order of tests matters here + # This should be the same order that you want the tests to be run in + # Number of test will also be decide by this list so don't miss any + # TestClass, TestScript, and TestPath fields will be used to write the REPserver script + # TestPath should be in reference to the testing home directory + "Test": [ + { + "name": "Resistance Measurement", + "required": 1, + "desc_short": "Measure resistance of analog lines", + "desc_long": "Test must be completed before attempting to measure ID resistor", + "TestClass": "gen_resist_test", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "wagon_rtd.py" + }, + + { + "name": "ID Resistor Measurement", + "required": 1, + "desc_short": "Measure resistance of ID resistor", + "desc_long": "Must be completed after the general resistance measurement", + "TestClass": "id_resist_test", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "wagon_rtd.py" + }, + + { + "name": "I2C Read/Write", + "required": 1, + "desc_short": "Check I2C read/write along wagon", + "desc_long": "Test must be completed before BERT for wagon wheel configuration", + "TestClass": "IIC_Check", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "run_iic_check.py" + }, + + { + "name": "Bit Error Rate Test", + "required": 1, + "desc_short": "Determine quality of data transmission", + "desc_long": "Needs to be completed after I2C check in order to set up wagon wheel", + "TestClass": "BERT", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "run_bert.py" + }, + ], + + + "PhysicalTest": [ + #{ + # "name": "SAMPLE test", + # "required": 1, + # "desc_short": "Some short description", + # "desc_long": "Really long description for later purposes.", + # "criteria": { + # "first testing criteria", + # "second testing criteria", + # "third testing criteria", + # }, + + #}, + + ], + + + # Information for sending and receiving data to/from the database + # Needs to be different based on board type + "DBInfo": { + "use_database": True, + "name": "WagonDB", + "reader": "WagonDBReadUser", + "inserter": "WagonDBInserter", + "baseURL": "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" + } + } + +dump_to_yaml(masterCfg) diff --git a/Configs/Zipper_cfg.yaml b/Configs/Zipper_cfg.yaml new file mode 100644 index 00000000..94812996 --- /dev/null +++ b/Configs/Zipper_cfg.yaml @@ -0,0 +1,34 @@ +--- +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: FactoryInserter + name: WagonDB_PRO + reader: FactoryReadUser + use_database: true + +GUIType: Zipper +SerialCheckSafe: false +UsingScanner: True + +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: HGCAL_dev, hostname: "TEST_SERVER_IP_ADDRESS", remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: "TEST_SERVER_IP_ADDRESS", username: , serverpath: , serverscript: } + +PhysicalTest: [] + +Test: +- TestClass: ZipResTest + TestPath: /home/HGCAL_dev/sw/WagonTesting/ZipperTest + TestScript: zip_res_test.py + desc_long: Connection check for zippers using ID resistor ADC + desc_short: Check zipper connections + name: Zipper Resistance Measurement + required: 1 +- TestClass: ZipBERT + TestPath: /home/HGCAL_dev/sw/WagonTesting/ZipperTest + TestScript: zip_bert_test.py + desc_long: Measure quality of fast data transmission through zippers using CLK lines from Kria + desc_short: BERT for zippers + name: Zipper Bit Error Rate Test + required: 1 diff --git a/Configs/__init__.py b/Configs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Configs/convert_to_yaml.py b/Configs/convert_to_yaml.py new file mode 100644 index 00000000..819dc9aa --- /dev/null +++ b/Configs/convert_to_yaml.py @@ -0,0 +1,26 @@ +#This script will convert any python dictionaries you want into yaml +#visit dumptoyaml.py for doing this within another python script + +import yaml + +#Import any dictionary files you want to convert to yaml here +from Engine_cfg import masterCfg as master_engine +from Wagon_cfg import masterCfg as master_wagon + +#Create a dictionary with the names of the yaml files you want to create as keys +#and the dictionary files as the values +#Only put alphanumeric characters into the files name, no spaces +py_files = { + 'Engine_cfg': master_engine, + 'Wagon_cfg': master_wagon, + } + +write_yaml(py_files) + +#This function can be passed a dictionary from other functions and will write a yaml file with that dictionary +def write_yaml(dictionary): + #This for loop iterates over all entries in the dictionary and creates a corresponding yaml file + for i in dictionary: + file = open(i + '.yaml', 'w') + yaml.dump(dictionary[i], file) + file.close() diff --git a/Configs/dumpToYaml.py b/Configs/dumpToYaml.py new file mode 100644 index 00000000..67ceab7d --- /dev/null +++ b/Configs/dumpToYaml.py @@ -0,0 +1,17 @@ +import yaml + +from glob import glob + +def dump_to_yaml(mastercfg): + + yaml_string = yaml.dump(mastercfg) + + + with open("temp.yaml", "w") as f: + + f.write(yaml_string) + + f.close() + + + diff --git a/MainFunction.py b/MainFunction.py deleted file mode 100755 index 60c954fa..00000000 --- a/MainFunction.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/python3 - -# Importing necessary modules -from audioop import mul -import multiprocessing as mp -import socket -# Imports the GUIWindow -from PythonFiles.GUIWindow import GUIWindow -from PythonFiles.utils.SUBClient import SUBClient - - -# Creates a task of creating the GUIWindow -def task_GUI(conn, queue): - # creates the main_window as an instantiation of GUIWindow - main_window = GUIWindow(conn, queue) - -# Creates a task of creating the SUBClient -def task_SUBClient(conn, queue): - # Creates the SUBSCRIBE Socket Client - sub_client = SUBClient(conn, queue) - -def run(): - # Creates a Pipe for the SUBClient to talk to the GUI Window - conn_SUB, conn_GUI = mp.Pipe() - - queue = mp.Queue() - - # Turns creating the GUI and creating the SUBClient tasks into processes - process_GUI = mp.Process(target = task_GUI, args=(conn_GUI, queue,)) - process_SUBClient = mp.Process(target = task_SUBClient, args = (conn_SUB, queue,)) - - - # Starts the processes - process_GUI.start() - process_SUBClient.start() - - # Should hold the code at this line until the GUI process ends - process_GUI.join() - - try: - conn_SUB.close() - conn_GUI.close() - except: - print("Pipe close is unnecessary.") - - try: - # Cleans up the SUBClient process - process_SUBClient.terminate() - except: - print("Terminate is unnecessary.") - pass - -if __name__ == "__main__": - print(socket.gethostname()) - ###### Example code to branch between the different GUIS ##### - # visual_GUI_computers = [ - # computer_1, - # computer_2, - # etc. - # ] - # wagon_GUI_computers = [ - # computer_3, - # computer_4, - # etc. - # ] - # engine_GUI_computers = [ - # computer_5, - # computer_6, - # etc. - # ] - # current_computer = socket.gethostname() - # for computer in visual_GUI_computers: - # if current_computer == computer: - # run_visual_GUI() - # else: - # pass - # for computer in wagon_GUI_computers: - # if current_computer == computer: - # run_wagon_GUI() - # else: - # pass - # for computer in engine_GUI_computers: - # if current_computer == computer: - # run_engine_GUI() - # else: - # pass - ###### END EXAMPLE CODE ###### - run() diff --git a/PREV-VERSION/PrevBethelFiles/AsyncioThread.py b/PREV-VERSION/PrevBethelFiles/AsyncioThread.py deleted file mode 100644 index 21f1bf96..00000000 --- a/PREV-VERSION/PrevBethelFiles/AsyncioThread.py +++ /dev/null @@ -1,75 +0,0 @@ -import time -from tkinter import * -import asyncio -import threading -import random -import queue - -from PythonFiles.TestScriptEx import TestScriptEx - - -# Fancy child of a thread that can send data to and from the tkinter stuff -# Code works -# Must be commented and reformatted to work within our program -class AsyncioThread(threading.Thread): - - # Constructor - # Parameters: the queue that is storing the "updates", the maximum amount of data that can go in - def __init__(self, the_queue, max_data): - - # SPECIAL - # asyncio.get_event_loop() is a special object from the asyncio import - self.asyncio_loop = asyncio.get_event_loop() - - # Initializes the class attributes - self.the_queue = the_queue - self.max_data = max_data - - # Calls the thread (parent class) constructor - threading.Thread.__init__(self, daemon= True) - - - # Method that is called when the thread is started - # Ran on the command "thread.start()" - def run(self): - - # run_until_complete is a method for "asyncio.get_event_loop()" objects - self.asyncio_loop.run_until_complete(self.do_data()) - - - # This is an "async def" function - # I don't know why it's special, but it is - async def do_data(self): - """ Creating and starting 'maxData' asyncio-tasks. """ - - # List of tasks that should be completed - tasks = [ - self.run_test_script(key) - for key in range(self.max_data) - ] - - - # Wait until this is done - # I don't really know what this does - await asyncio.wait(tasks) - - - - # Creates the random numbers that are shown as "data" - # Randomly does tasks - async def run_test_script(self, key): - """ Create data and store it in the queue. """ - - test_script_object = TestScriptEx("test1") - await asyncio.sleep(1) - test_script_object.run_inc_thread() - - - while self.is_alive : - - time.sleep(0.75) - - data = test_script_object.get_current_status() - - self.the_queue.put((key, data)) - \ No newline at end of file diff --git a/PREV-VERSION/PrevBethelFiles/ClientRunner.py b/PREV-VERSION/PrevBethelFiles/ClientRunner.py deleted file mode 100644 index 9aade3d9..00000000 --- a/PREV-VERSION/PrevBethelFiles/ClientRunner.py +++ /dev/null @@ -1,16 +0,0 @@ -# File to quickly start a client. Unnecessary file to be deleted at final stages of development - -from utils.REQClient import REQClient -from utils.SUBClient import SUBClient - -# Creates a main function to initialize the GUI -def main(): - client2 = SUBClient() - client1 = REQClient() - client1.run_test_thread(b"test1") - - - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/PREV-VERSION/PrevBethelFiles/ConsoleOutput.py b/PREV-VERSION/PrevBethelFiles/ConsoleOutput.py deleted file mode 100644 index 321ac10a..00000000 --- a/PREV-VERSION/PrevBethelFiles/ConsoleOutput.py +++ /dev/null @@ -1,36 +0,0 @@ -################################################################################# - -# Importing all the necessary modules -import tkinter as tk -import sys - -################################################################################# - - -# Creates a class to be instantiated for writing, needs the entry box it is writing into passed in -class ConsoleOutput(): - - ################################################# - - def __init__(self, entry): - self.entry = entry - - ################################################# - - # The write command adds the text into the entry box - def write(self, string): - - # Inserts text into the console entry box - self.entry.insert(tk.END, string) - self.entry.see('end') # Scrolls to the bottom of the text in the entry box - - ################################################# - - # Necessary for use - def flush(self): - pass - - ################################################# - - -################################################################################# \ No newline at end of file diff --git a/PREV-VERSION/PrevBethelFiles/QueueAttempt.py b/PREV-VERSION/PrevBethelFiles/QueueAttempt.py deleted file mode 100644 index 4ae58eb5..00000000 --- a/PREV-VERSION/PrevBethelFiles/QueueAttempt.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# based on - -from tkinter import * -import asyncio -import threading -import random -import queue - - -# Fancy child of a thread that can send data to and from the tkinter stuff -# Code works -# Must be commented and reformatted to work within our program -class AsyncioThread(threading.Thread): - - # Constructor - # Parameters: the queue that is storing the "updates", the maximum amount of data that can go in - def __init__(self, the_queue, max_data): - - # SPECIAL - # asyncio.get_event_loop() is a special object from the asyncio import - self.asyncio_loop = asyncio.get_event_loop() - - # Initializes the class attributes - self.the_queue = the_queue - self.max_data = max_data - - # Calls the thread (parent class) constructor - threading.Thread.__init__(self) - - - # Method that is called when the thread is started - # Ran on the command "thread.start()" - def run(self): - - # run_until_complete is a method for "asyncio.get_event_loop()" objects - self.asyncio_loop.run_until_complete(self.do_data()) - - - # This is an "async def" function - # I don't know why it's special, but it is - async def do_data(self): - """ Creating and starting 'maxData' asyncio-tasks. """ - - # List of tasks that should be completed - tasks = [ - self.create_dummy_data(key) - for key in range(self.max_data) - ] - - - # Wait until this is done - # I don't really know what this does - await asyncio.wait(tasks) - - - - # Creates the random numbers that are shown as "data" - # Randomly does tasks - async def create_dummy_data(self, key): - """ Create data and store it in the queue. """ - - - sec = random.randint(1, 10) - data = '{}:{}'.format(key, random.random()) - await asyncio.sleep(sec) - - self.the_queue.put((key, data)) - - - -# Class that handles all of the tkinter stuff -# Creates an AsyncioThread within -class TheWindow: - - # Constructor - def __init__(self, max_data): - # thread-safe data storage - self.the_queue = queue.Queue() - - # the GUI main object - self.root = Tk() - - # create the data variable - self.data = [] - for key in range(max_data): - self.data.append(StringVar()) - self.data[key].set('') - - # Button to start the asyncio tasks - Button(master=self.root, - text='Start Asyncio Tasks', - command=lambda: self.do_asyncio()).pack() - - # Frames to display data from the asyncio tasks - for key in range(max_data): - Label(master=self.root, textvariable=self.data[key]).pack() - - # Button to check if the GUI is freezed - Button(master=self.root, - text='Freezed???', - command=self.do_freezed).pack() - - - # Recursive Method - # Refreshes the data that will be put on the tkinter window - def refresh_data(self): - """ - """ - # do nothing if the aysyncio thread is dead - # and no more data in the queue - # Breaking statement of recursive loop - if not self.thread.is_alive() and self.the_queue.empty(): - return - - # refresh the GUI with new data from the queue - while not self.the_queue.empty(): - key, data = self.the_queue.get() - self.data[key].set(data) - - print('RefreshData...') - - # timer to refresh the gui with data from the asyncio thread - self.root.after(1000, self.refresh_data) # called only once! - - - # What the "Freeze???" button does when clicked - # Demonstrates that the GUI Window isn't frozen while the other data is parsed in - def do_freezed(self): - """ Button-Event-Handler to see if a button on GUI works. - The GOAL of this example is to make this button clickable - while the other thread/asyncio-tasks are working. """ - print('Tkinter is reacting. Thread-ID: {}' - .format(threading.get_ident())) - - - # Method that is run when the button is clicked - def do_asyncio(self): - """ - Button-Event-Handler starting the asyncio part in a separate - thread. - """ - # create fancy thread object - # Parameters: the queue that is storing the "updates", the maximum amount of data that can go in - self.thread = AsyncioThread(self.the_queue, len(self.data)) - - # timer to refresh the gui with data from the asyncio thread - self.root.after(1000, self.refresh_data) # called only once! - - # start the thread - self.thread.start() - - -# Main Method -# What logic is run when this file is ran from the terminal -if __name__ == '__main__': - - # Creates the window - # Parameter: number of labels to be filled with data - window = TheWindow(10) - - # mainloop() - window.root.mainloop() \ No newline at end of file diff --git a/PREV-VERSION/PrevBethelFiles/ServerRunner.py b/PREV-VERSION/PrevBethelFiles/ServerRunner.py deleted file mode 100644 index 0da44647..00000000 --- a/PREV-VERSION/PrevBethelFiles/ServerRunner.py +++ /dev/null @@ -1,7 +0,0 @@ - -# The following string stores all of the prints statements that the server -# needs to send to the client -prints = "" - -rep_server = REPServer(prints) -pub_server = PUBServer(prints) diff --git a/PREV-VERSION/PrevBethelFiles/TestScriptEx.py b/PREV-VERSION/PrevBethelFiles/TestScriptEx.py deleted file mode 100644 index 6b6a6862..00000000 --- a/PREV-VERSION/PrevBethelFiles/TestScriptEx.py +++ /dev/null @@ -1,44 +0,0 @@ -import threading -import time - -from PythonFiles.utils.REQClient import REQClient -from PythonFiles.utils.SUBClient import SUBClient - - -class TestScriptEx(): - def __init__(self, _test_type): - - self.incrementor = "Your console appears to be working" - self.test_type = bytes(_test_type,'UTF-8') - - time.sleep(0.75) - - - # def some_return_method(self): - # return "Some very odd and long string that has some content within it!" - - - def run_inc_thread(self): - self.inc_thread = threading.Thread(target=self.start_incrementing) - self.inc_thread.daemon = True - self.inc_thread.start() - - def start_incrementing(self): - - self.reqclient = REQClient() - self.subclient = SUBClient() - self.subclient.create_client() - time.sleep(2) - self.reqclient.run_test_thread(self.test_type) - - self.incrementor = self.subclient.get_message() - # while True: - # time.sleep(3) - # self.incrementor = self.incrementor + "Here's another line for you" - - - - def get_current_status(self): - self.incrementor = self.subclient.get_message() - self.subclient.set_message("") - return self.incrementor \ No newline at end of file diff --git a/PREV-VERSION/README.md b/PREV-VERSION/README.md deleted file mode 100644 index 54bdd4cb..00000000 --- a/PREV-VERSION/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# WagonTestGUI - -This repository contains the GUI used to run the quality control testing for all HGCAL LD Wagons. - -## Setup - -Run these commands from your working area to pull the code in: - - git clone git@github.com:UMN-CMS/WagonTestGUI.git - -When updating the code, you can use the following commands: - - git add - git commit -m "Message about what you are committing" - git push origin : - -You can then open a pull request (PR) by going to the Github repo and then we can merge your code into the master branch. - -## Goals for this Framework: - -The main goal for this framework is to have an efficient and easy-to-use user interface for running Wagon QC testing. The points of focus are: -- Integration the GUI with the test results database to store information about which boards have been tested and what tests have been run -- Easy to understand step-by-step instructions for QC testers to follow -- Implementation of a barcode scanning functionality for registering new boards and uploading test results -- Tracking of who is doing a tests, where it is taking place, and where the boards will be moved to after testing is finished - -## Background Information - -### What is a GUI? - -A GUI (or Graphical User Interface) is a program which allows users to interact with software via buttons, text entry boxes, and other module types. Most software that we are familiar with includes a user interface where we can modify data, navigate pages, and perform actions. An example of a GUI is the webpage you are currently on! You can choose to look at some of the code in this repository or perform actions to updated it with the click of a button. - -The GUI we will be developing for testing wagon functionality will be python based. There are a few packages that can be used for developing python based GUIs. The example GUI ([here](gui/initial_test_gui.py)) uses [TKinter](https://docs.python.org/3/library/tkinter.html) to produce the user interface. You can try out this GUI by performing the following command line call: `python initial_test_gui.py`. - -### What is a Wagon? - -Wagons are the motherboard connecting the active detector modules (what is measuring particle intractions) and the engines (the "brains" of the front-end electronics). Wagons are responsible for carrying clock, trigger, DAQ, and control infromation to between 2-4 modules simultaneously. They are completely passive boards and have no chips for communicating with the rest of the system. - -The purpose of wagons in the front-end readout train is to tranasmit data and control information to and from modules. Thus, we would like to ensure that each wagon in the final version of the detector has been checked for good communication ability. - -### What tests are being run? - -There are four tests that need to be run in order to verify a wagon is funcitioning properly: - -- Analog line connection check: measure the resistance of each of the analog lines on the wagon to ensure good connection -- Measurement of ID resistor: each wagon has a precision resistor used for identification of wagon type that must be measured and compared to the nominal value -- I2C read/write test: verify that the slow control communication along the wagon lines is working -- Bit error rate measurement: check the quality of the data sent along the wagon elinks diff --git a/PREV-VERSION/gui/initial_test_gui.py b/PREV-VERSION/gui/initial_test_gui.py deleted file mode 100644 index 502d57b8..00000000 --- a/PREV-VERSION/gui/initial_test_gui.py +++ /dev/null @@ -1,401 +0,0 @@ -from Tkinter import * -import ttk -from tkFileDialog import askopenfilename -from utils.read_barcode import read_barcode -from utils.board_requests import add_board_info, add_initial_tests, add_general_test, get_test_list, verify_person -import os - -# Need to make inspection its own test -# Bethel University Interns -# Andrew Kirzeder - -class TestEntryGUI(Frame): - def __init__(self, master=None): - Frame.__init__(self, master) - self.createInitWidgets() - - def clearWindow(self): - for widget in self.master.winfo_children(): - widget.destroy() - - def on_back(self, master=None): - self.clearWindow() - self.__init__() - - def open_file(self): - self.attach = askopenfilename(title="Select Attachment") - self.attach_btn_lbl["text"] = self.attach - self.attach_btn_lbl.pack(side = LEFT) - self.update() - - def createTopBar(self, lbl): - self.frm_tb = Frame(master = self.master, width = 1200) - - self.lbl_tb = Label(font=("Arial Bold", 30), width = 30, height=2, text=lbl, master=self.frm_tb) - self.QUIT = Button(text = "X", fg="red", command=self.quit, master=self.frm_tb) - - self.QUIT.pack(side=LEFT, padx = 10) - self.lbl_tb.pack(side=LEFT) - - self.frm_tb.pack(side=TOP) - - def createInitWidgets(self): - tb_label = "HGCAL Wagon QC Test" - self.createTopBar(tb_label) - - self.btn_frm = Frame(master = self.master, padx = 50, pady = 50, width = 1200, height =600) - - self.info_btn = Button(master=self.btn_frm, text="Register New Wagon", width=20, height = 10, command=self.createInfoWidgets) - self.info_btn.pack(side="left", padx = 50) - - #self.init_test_btn = Button(master = self.btn_frm, text="Add Initial Board Test", width = 20, height = 10, command=self.createInitialTestWidgets) - #self.init_test_btn.pack(side="left", padx = 50) - - self.test_btn = Button(master = self.btn_frm, text="Run Wagon Test", width = 20, height = 10, command=self.createGeneralTestWidgets) - self.test_btn.pack(side="left", padx = 50) - - self.btn_frm.pack(expand=True) - - def createInfoWidgets(self): - self.clearWindow() - - lbl_new = "Register New Wagon" - self.createTopBar(lbl_new) - - self.back = Button(master=self.frm_tb, text = "Back", command = self.on_back ) - self.back.pack(side=LEFT, padx=10) - - self.frm_tb.pack(side=TOP) - - self.main_info_frm = Frame(master=self.master, width = 1200, height = 1000, padx = 20, pady=20) - - self.general_frm = Frame(master = self.main_info_frm, width = 500, height = 200, relief=RAISED, borderwidth=5) - - self.general_lbl = Label(master = self.general_frm, text="General Info", font = ("Arial Bold", 20)) - self.general_lbl.pack(side=TOP, pady = 10) - - self.serial_num_frm = Frame(master = self.general_frm, padx=10, pady=10) - self.serial_num_lbl = Label(master=self.serial_num_frm, text = "Serial Number (or scan barcode)") - self.serial_num_ent = Entry(master=self.serial_num_frm) - - self.serial_num_lbl.pack(side=LEFT, padx=5) - self.serial_num_ent.pack(side=LEFT, padx=5) - self.serial_num_frm.pack(side=TOP) - - self.location_frm = Frame(master = self.general_frm, padx=10, pady=10) - self.location_lbl = Label(master=self.location_frm, text = "Location") - self.location_ent = Entry(master=self.location_frm) - - self.location_lbl.pack(side=LEFT, padx=5) - self.location_ent.pack(side=LEFT, padx=5) - self.location_frm.pack(side=LEFT) - - self.engine_frm = Frame(master = self.main_info_frm, width=500, height = 400, relief=RAISED, borderwidth = 5 ) - - self.engine_lbl = Label(master = self.engine_frm, text="Engine Info", font = ("Arial Bold", 20)) - self.engine_lbl.pack(side=TOP, pady = 10) - - self.daq_frm = Frame(master = self.engine_frm, padx=10, pady=10) - self.daq_lbl = Label(master=self.daq_frm, text = "DAQ Chip ID") - self.daq_ent = Entry(master=self.daq_frm) - - self.daq_lbl.pack(side=LEFT, padx=5) - self.daq_ent.pack(side=LEFT, padx=5) - self.daq_frm.pack(side=TOP) - - self.trig1_frm = Frame(master = self.engine_frm, padx=10, pady=10) - self.trig1_lbl = Label(master=self.trig1_frm, text = "Trigger Chip 1 ID") - self.trig1_ent = Entry(master=self.trig1_frm) - - self.trig1_lbl.pack(side=LEFT, padx=5) - self.trig1_ent.pack(side=LEFT, padx=5) - self.trig1_frm.pack(side=TOP) - - self.trig2_frm = Frame(master = self.engine_frm, padx=10, pady=10) - self.trig2_lbl = Label(master=self.trig2_frm, text = "Trigger Chip 2 ID") - self.trig2_ent = Entry(master=self.trig2_frm) - - self.trig2_lbl.pack(side=LEFT, padx=5) - self.trig2_ent.pack(side=LEFT, padx=5) - self.trig2_frm.pack(side=TOP) - - self.general_frm.pack(side=TOP, padx = 20) - #self.engine_frm.pack(side=LEFT, padx = 20) - - self.comments_frm = Frame(master = self.master, padx = 50, pady = 10, relief=RAISED, borderwidth=5) - self.comments_lbl = Label(master = self.comments_frm, text = "Comments", font=("Arial Bold", 20)) - self.comments_txt = Text(master = self.comments_frm, height = 3, width = 50) - - self.comments_lbl.pack(side = TOP) - self.comments_txt.pack(side = LEFT) - - self.main_info_frm.pack(side=TOP, expand=True, fill=X) - self.comments_frm.pack(pady = 20, padx = 20) - - self.submit_btn = Button(text="Submit", command=self.submit_info) - self.submit_btn.pack(pady=10, padx=10) - - barcode = None - - while not barcode: - barcode = read_barcode() - - self.serial_num_ent.insert(0, barcode) - - def submit_info(self): - info = {"serial_num": None, "board_id": None, "location": None, "daq_chip_id": 0, "trigger_chip_1_id": 0, "trigger_chip_2_id": 0, "comments": None} - - info["serial_num"] = self.serial_num_ent.get() - info["location"] = self.location_ent.get() - - if self.daq_ent.get(): - info["daq_chip_id"] = self.daq_ent.get() - else: - info["daq_chip_id"] = 0 - - if self.trig1_ent.get(): - info["trigger_chip_1_id"] = self.trig1_ent.get() - else: - info["trigger_chip_1_id"] = 0 - - if self.trig2_ent.get(): - info["trigger_chip_2_id"] = self.trig2_ent.get() - else: - info["trigger_chip_2_id"] = 0 - - info["comments"] = self.comments_txt.get("1.0", END) - - add_board_info(info) - - def createGeneralTestWidgets(self): - self.clearWindow() - - lbl_new = "Run Wagon Test" - self.createTopBar(lbl_new) - - self.back = Button(master=self.frm_tb, text = "Back", command = self.on_back ) - self.back.pack(side=LEFT, padx=10) - - self.frm_tb.pack(side=TOP) - - self.main_info_frm = Frame(master=self.master, width = 1200, height = 1000, padx = 20, pady=20) - - self.general_frm = Frame(master = self.main_info_frm, width = 500, height = 200, relief=RAISED, borderwidth=5) - - self.general_lbl = Label(master = self.general_frm, text="General Info", font = ("Arial Bold", 20)) - self.general_lbl.pack(side=TOP, pady = 10) - - self.serial_num_frm = Frame(master = self.general_frm, padx=10, pady=10) - self.serial_num_lbl = Label(master=self.serial_num_frm, text = "Serial Number (or scan barcode)") - self.serial_num_ent = Entry(master=self.serial_num_frm) - - self.serial_num_lbl.pack(side=LEFT, padx=5) - self.serial_num_ent.pack(side=LEFT, padx=5) - self.serial_num_frm.pack(side=TOP) - - self.tester_frm = Frame(master = self.general_frm, padx=10, pady=10) - self.tester_lbl = Label(master=self.tester_frm, text = "Tester") - self.tester_ent = Entry(master=self.tester_frm) - - self.tester_lbl.pack(side=LEFT, padx=5) - self.tester_ent.pack(side=LEFT, padx=5) - self.tester_frm.pack(side=TOP) - - self.test_frm = Frame(master = self.main_info_frm, width=500, height = 400, relief=RAISED, borderwidth = 5 ) - - self.test_lbl = Label(master = self.test_frm, text="Test", font = ("Arial Bold", 20)) - self.test_lbl.pack(side=TOP, pady = 10) - - self.test_name = StringVar(master = self) - - self.test_menu_name = Label(master = self.test_frm, text="Test Name: ") - self.test_menu_name.pack(side = LEFT, pady=10, padx = 5) - - self.tests = get_test_list() - self.test_names = [x[0] for x in self.tests] - self.test_types = [x[1] for x in self.tests] - - self.test_menu = ttk.Combobox(master = self.test_frm, textvariable = self.test_name, values = self.test_names) - self.test_menu.pack(side = LEFT, pady = 10, padx = 5) - - #self.t1 = BooleanVar() - - self.check_frm = Frame(master = self.test_frm) - #self.test1_chk = Checkbutton(master = self.check_frm, variable = self.t1, text="Successful?") - #self.test1_chk.pack(side=LEFT, padx=10, pady=10) - - self.check_frm.pack() - - self.general_frm.pack(side=LEFT, padx = 20) - self.test_frm.pack(side=LEFT, padx = 20, expand = True) - - self.main_info_frm.pack(side=TOP, expand=True, fill=X) - '''self.comments_frm = Frame(master = self.master, padx = 50, pady = 10, relief=RAISED, borderwidth=5) - self.comments_lbl = Label(master = self.comments_frm, text = "Comments", font=("Arial Bold", 20)) - self.comments_txt = Text(master = self.comments_frm, height = 3, width = 50) - - self.comments_lbl.pack(side = TOP) - self.comments_txt.pack(side = LEFT) - - self.comments_frm.pack(pady = 20, padx = 20) - - self.attach = None - - self.attach_frm = Frame(master = self.master, padx = 50, pady = 10, relief=RAISED, borderwidth=5) - self.attach_desc_txt = Text(master = self.attach_frm, height = 1, width = 50) - self.attach_desc_txt.insert("1.0", "Attachment Description") - self.attach_com_txt = Text(master = self.attach_frm, height = 3, width = 50) - self.attach_com_txt.insert("1.0", "Attachment Comments") - self.attach_lbl = Label(master = self.attach_frm, text = "Attachemnt", font=("Arial Bold", 20)) - - self.attach_lbl.pack(pady = 10, padx = 10) - self.attach_desc_txt.pack(pady = 10, padx = 10) - self.attach_com_txt.pack(pady = 10, padx = 10) - - self.attach_btn_lbl = Label(master = self.attach_frm, text = "") - self.attach_btn_lbl.pack(side = LEFT, padx = 10, pady = 10) - self.attach_btn = Button(master = self.attach_frm, text="Add Attachment", command=self.open_file) - self.attach_btn.pack(pady=10, padx=10) - - self.attach_frm.pack(side = TOP, padx = 10, pady = 10) - ''' - self.submit_btn = Button(text="Run Test", command=self.submit_test) - self.submit_btn.pack(expand = True, padx = 10, pady = 10) - - self.update() - - barcode = None - - while not (self.serial_num_ent.get() or barcode): - barcode = read_barcode() - - self.serial_num_ent.insert(0, barcode) - - def submit_test(self): - info = {"serial_number": None, "board_id": None, "person_id": None, "test_type": None, "sucess": 0, "comments": None, "attachdesc1": None, "attachcomment1": None} - - files = {"attach1": None} - - info["serial_number"] = self.serial_num_ent.get() - person_id = verify_person(self.tester_ent.get()) - - if person_id: - info["person_id"] = person_id - else: - info["person_id"] = 0 - info["success"] = 1 if self.t1.get() else 0 - info["test_type"] = self.test_types[self.test_names.index(self.test_name.get())] - info["comments"] = self.comments_txt.get("1.0", END) - - if self.attach: - files["attach1"] = open(self.attach,'rb') - info["attachdesc1"] = self.attach_desc_txt.get("1.0", END) - info["attachcomment1"] = self.attach_com_txt.get("1.0", END) - - print(info) - print(files) - - add_general_test(info, files) - - def createInitialTestWidgets(self): - self.clearWindow() - - lbl_new = "Add Initial Board Test" - self.createTopBar(lbl_new) - - self.back = Button(master=self.frm_tb, text = "Back", command = self.on_back ) - self.back.pack(side=LEFT, padx=10) - - self.frm_tb.pack(side=TOP) - - self.main_info_frm = Frame(master=self.master, width = 1200, height = 1000, padx = 20, pady=20) - - self.general_frm = Frame(master = self.main_info_frm, width = 500, height = 200, relief=RAISED, borderwidth=5) - - self.general_lbl = Label(master = self.general_frm, text="General Info", font = ("Arial Bold", 20)) - self.general_lbl.pack(side=TOP, pady = 10) - - self.serial_num_frm = Frame(master = self.general_frm, padx=10, pady=10) - self.serial_num_lbl = Label(master=self.serial_num_frm, text = "Serial Number (or scan barcode)") - self.serial_num_ent = Entry(master=self.serial_num_frm) - - self.serial_num_lbl.pack(side=LEFT, padx=5) - self.serial_num_ent.pack(side=LEFT, padx=5) - self.serial_num_frm.pack(side=TOP) - - self.tester_frm = Frame(master = self.general_frm, padx=10, pady=10) - self.tester_lbl = Label(master=self.tester_frm, text = "Tester") - self.tester_ent = Entry(master=self.tester_frm) - - self.tester_lbl.pack(side=LEFT, padx=5) - self.tester_ent.pack(side=LEFT, padx=5) - self.tester_frm.pack(side=TOP) - - self.test_frm = Frame(master = self.main_info_frm, width=500, height = 400, relief=RAISED, borderwidth = 5 ) - - self.test_lbl = Label(master = self.test_frm, text="Initial Tests", font = ("Arial Bold", 20)) - self.test_lbl.pack(side=TOP, pady = 10) - - self.t1 = BooleanVar() - self.t2 = BooleanVar() - self.t3 = BooleanVar() - self.t4 = BooleanVar() - - self.check_frm = Frame(master = self.test_frm) - self.test1_chk = Checkbutton(master = self.check_frm, variable = self.t1, text="Label Applied") - self.test1_chk.grid(row = 0, column=0, sticky=W, padx=10, pady=10) - self.test2_chk = Checkbutton(master = self.check_frm, variable = self.t2, text="Database Entry") - self.test2_chk.grid(row = 0, column=1, sticky=W, padx=10, pady=10) - self.test3_chk = Checkbutton(master = self.check_frm, variable = self.t3, text="Label Legibility") - self.test3_chk.grid(row = 1, column=0, sticky=W, padx=10, pady=10) - self.test4_chk = Checkbutton(master = self.check_frm, variable = self.t4, text="Power Cycle") - self.test4_chk.grid(row = 1, column=1, sticky=W, padx=10, pady=10) - - self.check_frm.pack() - - self.general_frm.pack(side=LEFT, padx = 20) - self.test_frm.pack(side=LEFT, padx = 20) - - self.comments_frm = Frame(master = self.master, padx = 50, pady = 10, relief=RAISED, borderwidth=5) - self.comments_lbl = Label(master = self.comments_frm, text = "Comments", font=("Arial Bold", 20)) - self.comments_txt = Text(master = self.comments_frm, height = 3, width = 50) - - self.comments_lbl.pack(side = TOP) - self.comments_txt.pack(side = LEFT) - - self.main_info_frm.pack(side=TOP, expand=True, fill=X) - self.comments_frm.pack(pady = 20, padx = 20) - - self.attach_btn = Button(text="Add Attachment", command=lambda:askopenfilename(title="Select test attachment")) - self.attach_btn.pack(pady=10, padx=10) - - self.submit_btn = Button(text="Submit", command=self.submit_tests) - self.submit_btn.pack(pady=10, padx=10) - - self.update() - - barcode = None - - while not (self.serial_num_ent.get() or barcode): - barcode = read_barcode() - - self.serial_num_ent.insert(0, barcode) - - def submit_tests(self): - info = {"serial_num": None, "board_id": None, "tester": None, "label_applied": 0, "database_entry": 0, "label_legibility": 0, "power_cycle": 0, "comments": None} - - info["serial_num"] = self.serial_num_ent.get() - info["tester"] = self.tester_ent.get() - info["label_applied"] = 1 if self.t1.get() else 0 - info["database_entry"] = 1 if self.t2.get() else 0 - info["label_legibility"] = 1 if self.t3.get() else 0 - info["power_cycle"] = 1 if self.t4.get() else 0 - info["comments"] = self.comments_txt.get("1.0", END) - - add_initial_tests(info) - -root = Tk() -#root.geometry('1000x500') -app = TestEntryGUI(master=root) -app.master.title("Initial Board Entry Tests") -app.mainloop() diff --git a/PREV-VERSION/gui/utils/__init__.pyc b/PREV-VERSION/gui/utils/__init__.pyc deleted file mode 100644 index be75a1d0..00000000 Binary files a/PREV-VERSION/gui/utils/__init__.pyc and /dev/null differ diff --git a/PREV-VERSION/gui/utils/board_requests.py b/PREV-VERSION/gui/utils/board_requests.py deleted file mode 100644 index 317bc58e..00000000 --- a/PREV-VERSION/gui/utils/board_requests.py +++ /dev/null @@ -1,51 +0,0 @@ -import requests -import json -from read_barcode import read_barcode - -def add_new_board(sn): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_module2.py', data={"serial_number": str(sn)}) - -def add_board_info(info): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_board_info2.py', data = info) - -def add_initial_tests(results): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_init_test.py', data = results) - -def add_general_test(results, files): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_test2.py', data = results, files=files) - -def add_test_json(json_file, files): - results = json.load(open(json_file)) - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_test_json.py', data = results, files = files) - -def get_test_list(): - r = requests.get('http://cmslab3.umncmslab/~cros0400/cgi-bin/get_test_types.py') - - lines = r.text.split('\n') - - begin = lines.index("Begin") + 1 - end = lines.index("End") - - tests = [] - - for i in range(begin, end): - temp = lines[i][1:-1].split(",") - temp[0] = str(temp[0][1:-1]) - temp[1] = int(temp[1]) - tests.append(temp) - - return tests - -def verify_person(name): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/verify_person.py', data={'name': name}) - - lines = r.text.split('\n') - - begin = lines.index("Begin") + 1 - end = lines.index("End") - - person_id = lines[begin] - - return person_id - -#add_test_json("example.json", {"attach1": open("test.txt","rb")}) diff --git a/PREV-VERSION/gui/utils/board_requests.pyc b/PREV-VERSION/gui/utils/board_requests.pyc deleted file mode 100644 index 55d0a86c..00000000 Binary files a/PREV-VERSION/gui/utils/board_requests.pyc and /dev/null differ diff --git a/PREV-VERSION/gui/utils/example.json b/PREV-VERSION/gui/utils/example.json deleted file mode 100644 index f81cfbd3..00000000 --- a/PREV-VERSION/gui/utils/example.json +++ /dev/null @@ -1 +0,0 @@ -{"tester": "Bryan", "successful": "1", "attach2_desc": "", "attach2_com": "", "test_type": "Power Cycle", "comments": "test", "serial_num": "320311002000001", "attach1_desc": "test", "attach1_com": "test comments", "attach3_desc": "", "attach3_com": ""} diff --git a/PREV-VERSION/gui/utils/read_barcode.py b/PREV-VERSION/gui/utils/read_barcode.py deleted file mode 100644 index 75d501d7..00000000 --- a/PREV-VERSION/gui/utils/read_barcode.py +++ /dev/null @@ -1,22 +0,0 @@ -import subprocess -from subprocess import PIPE - -def read_barcode(): - print("Scan barcode now (CTRL-C to exit):") - p = subprocess.Popen(["/usr/share/zebra-scanner/samples/console-app/bin/corescanner-console-app"], stdout=PIPE) - - output = p.communicate()[0] - - begin = output.find("") + len("") - end = output.find("") - - raw_hex = output[begin:end].replace(" ","") - - payload = bytearray.fromhex(raw_hex).decode() - - print(payload) - - return "3203" + payload - -if __name__ == "__main__": - read_barcode() diff --git a/PREV-VERSION/gui/utils/read_barcode.pyc b/PREV-VERSION/gui/utils/read_barcode.pyc deleted file mode 100644 index 98c6ef01..00000000 Binary files a/PREV-VERSION/gui/utils/read_barcode.pyc and /dev/null differ diff --git a/PREV-VERSION/gui/utils/write_test_json.py b/PREV-VERSION/gui/utils/write_test_json.py deleted file mode 100755 index b1102ca8..00000000 --- a/PREV-VERSION/gui/utils/write_test_json.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/python3 - -import json -import base64 - -def b64(attach): - with open(attach, "r") as f: - data = f.read() - f.close() - - data_bytes = data.encode("ascii") - encoded = base64.b64encode(data_bytes) - data = encoded.decode("ascii") - return data - -def main(): - test_dict = { - "serial_num" : "", - "tester" : "", - "test_type" : "", - "successful" : "", - "comments" : "", - "attach1" : "", - "attach1_desc" : "", - "attach1_com" : "", - "attach2" : "", - "attach2_desc" : "", - "attach2_com" : "", - "attach3" : "", - "attach3_desc" : "", - "attach3_com" : "", - } - - test_dict['serial_num'] = raw_input("Enter board serial number: ") - test_dict['tester'] = raw_input("Enter tester name: ") - test_dict['test_type'] = raw_input("Enter test type: ") - test_dict['successful'] = raw_input("Successful? (1 for yes, 0 for no): ") - test_dict['comments'] = raw_input("Comments (required): ") - - attachments = raw_input("Do you have attachments to upload? (y/n): ") - i = 1 - while(attachments == "y" or attachments == "yes"): - if i == 4: break - path = raw_input("Path to attachment: ") - desc = raw_input("Description of attachment: ") - comments = raw_input("Attachment comments: ") - if i == 1: - test_dict['attach1'] = b64(path) - test_dict['attach1_desc'] = desc - test_dict['attach1_com'] = comments - elif i == 2: - test_dict['attach2'] = b64(path) - test_dict['attach2_desc'] = desc - test_dict['attach2_com'] = comments - elif i == 3: - test_dict['attach1'] = b64(path) - test_dict['attach1_desc'] = desc - test_dict['attach1_com'] = comments - attachments = raw_input("Do you have another attachment? (y/n): ") - if attachments != "y" or attachments != "yes": - break - i += 1 - - outpath = raw_input("Ouput name for json file: ") - - if outpath[-5:] != ".json": - outpath += ".json" - - with open(outpath, "w") as g: - json.dump(test_dict, g) - - g.close() - -if __name__ == "__main__": - main() diff --git a/PREV-VERSION/labels/ZPLexample.txt b/PREV-VERSION/labels/ZPLexample.txt deleted file mode 100644 index 7e3120cf..00000000 --- a/PREV-VERSION/labels/ZPLexample.txt +++ /dev/null @@ -1,4 +0,0 @@ -^XA -^FO50,50^ADN,36,20^FDHello World! -^FS -^XZ diff --git a/PREV-VERSION/labels/label.zpl b/PREV-VERSION/labels/label.zpl deleted file mode 100644 index e341d953..00000000 --- a/PREV-VERSION/labels/label.zpl +++ /dev/null @@ -1 +0,0 @@ -^XA^FO19,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO39,31^BXN,3,200,,,,,1^FD320512320100001^FS^FO39,75^A0N,16,16^FB42,1,0,R,0^FD00001^FS^FO121,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO141,31^BXN,3,200,,,,,1^FD320512320100002^FS^FO141,75^A0N,16,16^FB42,1,0,R,0^FD00002^FS^FO222,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO242,31^BXN,3,200,,,,,1^FD320512320100003^FS^FO242,75^A0N,16,16^FB42,1,0,R,0^FD00003^FS^FO324,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO344,31^BXN,3,200,,,,,1^FD320512320100004^FS^FO344,75^A0N,16,16^FB42,1,0,R,0^FD00004^FS^FO425,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO445,31^BXN,3,200,,,,,1^FD320512320100005^FS^FO445,75^A0N,16,16^FB42,1,0,R,0^FD00005^FS^FO527,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO547,31^BXN,3,200,,,,,1^FD320512320100006^FS^FO547,75^A0N,16,16^FB42,1,0,R,0^FD00006^FS^FO628,31^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO648,31^BXN,3,200,,,,,1^FD320512320100007^FS^FO648,75^A0N,16,16^FB42,1,0,R,0^FD00007^FS^FO19,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO39,133^BXN,3,200,,,,,1^FD320512320100008^FS^FO39,177^A0N,16,16^FB42,1,0,R,0^FD00008^FS^FO121,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO141,133^BXN,3,200,,,,,1^FD320512320100009^FS^FO141,177^A0N,16,16^FB42,1,0,R,0^FD00009^FS^FO222,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO242,133^BXN,3,200,,,,,1^FD320512320100010^FS^FO242,177^A0N,16,16^FB42,1,0,R,0^FD00010^FS^FO324,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO344,133^BXN,3,200,,,,,1^FD320512320100011^FS^FO344,177^A0N,16,16^FB42,1,0,R,0^FD00011^FS^FO425,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO445,133^BXN,3,200,,,,,1^FD320512320100012^FS^FO445,177^A0N,16,16^FB42,1,0,R,0^FD00012^FS^FO527,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO547,133^BXN,3,200,,,,,1^FD320512320100013^FS^FO547,177^A0N,16,16^FB42,1,0,R,0^FD00013^FS^FO628,133^A0R,16,16^FB64,1,0,L,0^FDWag3E01^FS^FO648,133^BXN,3,200,,,,,1^FD320512320100014^FS^FO648,177^A0N,16,16^FB42,1,0,R,0^FD00014^FS^XZ \ No newline at end of file diff --git a/PREV-VERSION/labels/make_label.py b/PREV-VERSION/labels/make_label.py deleted file mode 100644 index b07d5057..00000000 --- a/PREV-VERSION/labels/make_label.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/usr/bin/python3 - -from zpl import Label -import os -import argparse - - -class myLabel(Label): - - def __init__(self, height=25.4, width=88.9, dpmm=8.0): - super().__init__(height, width, dpmm) - - def write_datamatrix(self, height=1, orientation='N', sq=200, aspect=1): - - self.code += ("^BX{},{},{},,,,,{}").format(orientation, height, sq, aspect) - -class Barcode: - - def __init__(self, payload): - self.serial = payload[-5:] - self.subtype = payload[4:-5] - self.first = payload[:4] - - self.full_serial = payload - - self.get_nickname() - - - def get_nickname(self): - - labels = { - "060001": "LD HB1 P", - "060002": "LD HB1 E", - "100000": "EngV1", - "100001": "EngV2", - "100002": "EngV2b", - "100300": "EngV3", - "100100": "Eng", - "110002": "WagV2-E", - "110003": "WagV2-W", - "123101": "Wag3W01", - "123201": "Wag3E01", - "500001": "TBT", - "500002": "TBT2", - "500011": "HXCTR", - "510001": "IntrPos", - "510011": "WagW-TBT", - "510012": "WagE-TBT", - "510021": "FMC-EngV2", - "990001": "Test" - } - - self.nickname = labels[str(self.subtype)] - return self.nickname - - def get_label_name(self): - return self.get_nickname() + self.serial - - -def produce_barcode(barcode, x_offset=0, y_offset=0): - - l = myLabel(9.525, 9.525, dpmm=8.0) - - l.origin(0.25,0.75) - l.write_text(barcode.get_nickname(), char_height=2, char_width=2, line_width=8, orientation='R', justification='L') - l.endorigin() - - l.origin(2.75, 0.75) - l.write_datamatrix(height=3, orientation='N', sq=200, aspect=1) - l.write_text('{}'.format(barcode.full_serial)) - l.endorigin() - - l.origin(2.75, 6.25) - l.write_text(barcode.serial, char_height=2, char_width=2, line_width=5.25, orientation='N', justification='R') - l.endorigin() - - print(l.dumpZPL()) - #l.preview() - - - if not os.path.isdir("{}".format(barcode.get_nickname())): - os.makedirs("{}".format(barcode.get_nickname())) - - with open("{}/{}.zpl".format(barcode.get_nickname(), barcode.get_label_name()),'w') as f: - f.write(l.dumpZPL()) - f.close() - #l.preview() - - return l - -def add_to_megalabel(megalabel, barcode, x_offset=1.5875, y_offset=1.5875): - - megalabel.origin(0.25+x_offset,0.75+y_offset) - megalabel.write_text(barcode.get_nickname(), char_height=2, char_width=2, line_width=8, orientation='R', justification='L') - megalabel.endorigin() - - megalabel.origin(2.75+x_offset, 0.75+y_offset) - megalabel.write_datamatrix(height=3, orientation='N', sq=200, aspect=1) - megalabel.write_text('{}'.format(barcode.full_serial)) - megalabel.endorigin() - - megalabel.origin(2.75+x_offset, 6.25+y_offset) - megalabel.write_text(barcode.serial, char_height=2, char_width=2, line_width=5.25, orientation='N', justification='R') - megalabel.endorigin() - - -def produce_strips(barcodes): - - if not os.path.isdir(barcodes[0].get_nickname()): - os.makedirs(barcodes[0].get_nickname()) - - l = myLabel(25.4, 88.9, dpmm=8.0) - - left = 2.175 - top = 3.175 - spacing = 12.7 - - for y in range(0, 2): - for x in range(0, 7): - add_to_megalabel(l, barcodes[y*7+x], x_offset=left+x*spacing, y_offset=top+y*spacing) - - print(l.dumpZPL()) - #l.preview() - - with open("label.zpl", 'w') as f: - f.write(l.dumpZPL()) - f.close() - - return l - -### DEPRECIATED BELOW UNTIL MAIN ########## - - '''im = Image.open(name) - im = ImageOps.contain(im, (54,54), method=Image.NEAREST) - border = 22 - - im = ImageOps.expand(im, border, 255) - im = im.crop((0, 22, 76, 98)) - - font1 = ImageFont.truetype("MotorolaScreentype.ttf", 16) - - if len(label) > 8: - font2 = ImageFont.truetype("MotorolaScreentype.ttf", 16) - elif len(label) > 6: - font2 = ImageFont.truetype("MotorolaScreentype.ttf", 16) - else: - font2 = ImageFont.truetype("MotorolaScreentype.ttf", 16) - - ImageDraw.Draw(im).text((28, 50), payload[-5:], font=font1) - im = im.rotate(90) - ImageDraw.Draw(im).text((6, 50), label, font=font2) - im = im.rotate(-90) - #im = ImageOps.expand(im, 1, 230) - im.save(name) - - return im - ''' - -def main(): - parser = argparse.ArgumentParser(description="Make data matrix labels for HGCAL electronics") - parser.add_argument("-m", "--majortype", action="store", type=str, dest="majortype", default="Test", help="Major type name for component (e.g. Engine, Wagon, Concentrator Mezzanine). See documentation for full list of major types") - parser.add_argument("-p", "--production", action="store_true", dest="production", default=False, help="Add flag if component is final production") - parser.add_argument("-s", "--subtype", action="store", type=str, dest="subtype", default=None, help="Version specification for prototypes (e.g. V1, V2, V2b)") - parser.add_argument("-a", "--adapter", action = "store", type=str, dest="adapter", default=None, help="Name of adapter (if applicable)") - parser.add_argument("-t", "--tester", action = "store", type=str, dest="tester", default=None, help="Name of tester (if applicable)") - parser.add_argument("-x", "--hexaboard", action = "store", type=int, dest="hexaboard", default=None, help="Specify the whether partial or full production or prototype (Full prototype = 0, partial prototype = 1, partial production = 2, full production = 3, if applicable)") - parser.add_argument("-v", "--vendor", action = "store", type=str, dest="vendor", default=None, help="Specify the vendor for hexaboard (P or E, if applicable)") - parser.add_argument("-w", "--wagon", action = "store", type=str, dest="wagon", default=None, help="Specify west or east wagon subtype (W or E)") - parser.add_argument("-n", "--number", action = "store", type=int, dest="n", default=1, help="Number of labels to make with given specifications") - parser.add_argument("--serial", action="store", type=int, dest="serial", default=0, help="Specify serial number at which to start") - args = parser.parse_args() - - types = { - "ldhexaboard" : 6, - "hdhexaboard" : 7, - "ldengine" : 10, - "ldwagon" : 12, - "concentratormezzanine" : 14, - "boardtester" : 50, - "adapter" : 51, - "test" : 99, - } - - wagons = { - "w" : 1, - "e" : 2, - } - - adapters = { - "interposer" : 1, - "wagonwest" : 11, - "wagoneast" : 12, - "fmctoengine" : 21, - } - - testers = { - "tileboardtester" : 1, - "tileboardtesterv2" : 2, - "hexacontroller" : 11, - } - - subtypes = { - "v1" : 0, - "v2" : 1, - "v2b" : 2, - "v3" : 3, - } - - labels = { - "060001": "LD HB1 P", - "060002": "LD HB1 E", - "100000": "EngV1", - "100001": "EngV2", - "100002": "EngV2b", - "100300": "EngV3", - "100100": "Eng", - "110002": "WagV2-E", - "110003": "WagV2-W", - "123101": "Wag3W01", - "123201": "Wag3E01", - "500001": "TBT", - "500002": "TBT2", - "500011": "HXCTR", - "510001": "IntrPos", - "510011": "WagW-TBT", - "510012": "WagE-TBT", - "510021": "FMC-EngV2", - "990001": "Test" - } - - majortype = types[args.majortype.lower()] - - if majortype == 6 and (args.vendor == None or args.hexaboard == None): - print("Specify both a vendor and production type for hexaboard") - return - elif majortype == 11 and args.wagon == None: - print("Specify whether is is an east or west wagon, use -w with W or E") - elif majortype == 50 and args.tester == None: - print("No tester type specified, use -t with name of tester") - return - elif majortype == 51 and args.adapter == None: - print("No adapter type specified, use -a with name of adapter") - return - elif majortype == 6: - if args.vendor.lower() == "p": - vendor = 1 - elif args.vendor.lower() == "e": - vendor = 2 - else: - print("Please specify the correct vendor indicator") - return - sub = args.hexaboard * 100 + vendor - elif majortype == 10: - subtype = subtypes[args.subtype] - sub = 100 * subtype - elif majortype == 12: - we = 100 * wagons[args.wagon.lower()] - vers = 1000 * subtypes[args.subtype.lower()] - sub = vers + we + 1 - print(sub) - elif majortype == 50: - sub = testers[args.tester.lower()] - elif majortype == 51: - sub = adapters[args.adapter.lower()] - else: - sub = None - - - production = 1 if args.production else 0 - if production == 1 and args.version != None: - print("Subtype not needed for production components") - subtype = "0" - elif majortype != 50 and majortype != 51 and majortype != 6 and majortype != 11: - subtype = subtypes[args.subtype.lower()] - - - names = [] - images = [] - #Do fancy database checking here to not double use serial numbers - barcodes = [] - - for i in range(1,args.n + 1): - serial = args.serial + i - - if sub == None: - payload = "3205{0:02d}{1:02d}{2:02d}{4:05d}".format(majortype, production, subtype, 0, serial) - else: - payload = "3205{0:02d}{1:04d}{2:05d}".format(majortype, sub, serial) - - label = labels[str(payload[4:10])] - print("Making barcode for {} with value: {}".format(label, payload)) - - barcode = Barcode(payload) - - l = produce_barcode(barcode) - - barcodes.append(barcode) - - if len(barcodes) > 1: - produce_strips(barcodes) - - ''' - width, height = images[0].size - - rows = [] - - new_image = Image.new("L", (args.n * (width + 25), height + 25)) - i_row = 0 - j = 0 - for i in range(args.n): - if i % 2 == 0: - new_image.paste(images[i], ((width + 13) * i, 13)) - else: - new_image.paste(images[i], ((width + 12) * i, 13)) - - if (i + 1) % 7 == 0: - j += 1 - new_image.save("{0}/{0}{1}.png".format(label, j)) - rows.append(new_image) - new_image = Image.new("L", (args.n * (width + 13), height + 25)) - - if (i + 1) % 7 != 0: - j += 1 - new_image.save("{0}/{0}{1}.png".format(label,j)) - rows.append(new_image) - - if j > 1: - height = sum([r.height for r in rows]) - print(rows[0].width, rows[0].height) - new_image = Image.new("L", (rows[0].width, height)) - - for i,r in enumerate(rows): - new_image.paste(r, (0, i*(height + 26))) - - new_image.save("{0}/{0}.png".format(label, label)) - - ''' - -if __name__ == "__main__": - main() diff --git a/PREV-VERSION/utils/board_requests.py b/PREV-VERSION/utils/board_requests.py deleted file mode 100644 index dc9ea14f..00000000 --- a/PREV-VERSION/utils/board_requests.py +++ /dev/null @@ -1,51 +0,0 @@ -import requests -import json -from read_barcode import read_barcode - -def add_new_board(sn): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_module2.py', data={"serial_number": str(sn)}) - -def add_board_info(info): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_board_info2.py', data = info) - -def add_initial_tests(results): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_init_test.py', data = results) - -def add_general_test(results, files): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_test2.py', data = results, files=files) - -def add_test_json(json_file, files): - results = json.load(open(json_file)) - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/add_test_json.py', data = results, files = files) - -def get_test_list(): - r = requests.get('http://cmslab3.umncmslab/~cros0400/cgi-bin/get_test_types.py') - - lines = r.text.split('\n') - - begin = lines.index("Begin") + 1 - end = lines.index("End") - - tests = [] - - for i in range(begin, end): - temp = lines[i][1:-1].split(",") - temp[0] = str(temp[0][1:-1]) - temp[1] = int(temp[1]) - tests.append(temp) - - return tests - -def verify_person(name): - r = requests.post('http://cmslab3.umncmslab/~cros0400/cgi-bin/verify_person.py', data={'name': name}) - - lines = r.text.split('\n') - - begin = lines.index("Begin") + 1 - end = lines.index("End") - - person_id = lines[begin] - - return person_id - -add_test_json("example.json", {"attach1": open("test.txt","rb")}) diff --git a/PREV-VERSION/utils/read_barcode.py b/PREV-VERSION/utils/read_barcode.py deleted file mode 100644 index 75d501d7..00000000 --- a/PREV-VERSION/utils/read_barcode.py +++ /dev/null @@ -1,22 +0,0 @@ -import subprocess -from subprocess import PIPE - -def read_barcode(): - print("Scan barcode now (CTRL-C to exit):") - p = subprocess.Popen(["/usr/share/zebra-scanner/samples/console-app/bin/corescanner-console-app"], stdout=PIPE) - - output = p.communicate()[0] - - begin = output.find("") + len("") - end = output.find("") - - raw_hex = output[begin:end].replace(" ","") - - payload = bytearray.fromhex(raw_hex).decode() - - print(payload) - - return "3203" + payload - -if __name__ == "__main__": - read_barcode() diff --git a/PREV-VERSION/utils/write_test_json.py b/PREV-VERSION/utils/write_test_json.py deleted file mode 100644 index b1102ca8..00000000 --- a/PREV-VERSION/utils/write_test_json.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/python3 - -import json -import base64 - -def b64(attach): - with open(attach, "r") as f: - data = f.read() - f.close() - - data_bytes = data.encode("ascii") - encoded = base64.b64encode(data_bytes) - data = encoded.decode("ascii") - return data - -def main(): - test_dict = { - "serial_num" : "", - "tester" : "", - "test_type" : "", - "successful" : "", - "comments" : "", - "attach1" : "", - "attach1_desc" : "", - "attach1_com" : "", - "attach2" : "", - "attach2_desc" : "", - "attach2_com" : "", - "attach3" : "", - "attach3_desc" : "", - "attach3_com" : "", - } - - test_dict['serial_num'] = raw_input("Enter board serial number: ") - test_dict['tester'] = raw_input("Enter tester name: ") - test_dict['test_type'] = raw_input("Enter test type: ") - test_dict['successful'] = raw_input("Successful? (1 for yes, 0 for no): ") - test_dict['comments'] = raw_input("Comments (required): ") - - attachments = raw_input("Do you have attachments to upload? (y/n): ") - i = 1 - while(attachments == "y" or attachments == "yes"): - if i == 4: break - path = raw_input("Path to attachment: ") - desc = raw_input("Description of attachment: ") - comments = raw_input("Attachment comments: ") - if i == 1: - test_dict['attach1'] = b64(path) - test_dict['attach1_desc'] = desc - test_dict['attach1_com'] = comments - elif i == 2: - test_dict['attach2'] = b64(path) - test_dict['attach2_desc'] = desc - test_dict['attach2_com'] = comments - elif i == 3: - test_dict['attach1'] = b64(path) - test_dict['attach1_desc'] = desc - test_dict['attach1_com'] = comments - attachments = raw_input("Do you have another attachment? (y/n): ") - if attachments != "y" or attachments != "yes": - break - i += 1 - - outpath = raw_input("Ouput name for json file: ") - - if outpath[-5:] != ".json": - outpath += ".json" - - with open(outpath, "w") as g: - json.dump(test_dict, g) - - g.close() - -if __name__ == "__main__": - main() diff --git a/PhotoTakingGUI/CameraDemo/AutoFocus.py b/PhotoTakingGUI/CameraDemo/AutoFocus.py new file mode 100644 index 00000000..a04e8beb --- /dev/null +++ b/PhotoTakingGUI/CameraDemo/AutoFocus.py @@ -0,0 +1,101 @@ +import cv2 #sudo apt-get install python-opencv +import numpy as py +import os +import time +import smbus +i2cbus = 1 +bus = smbus.SMBus(bus = i2cbus) +try: + import picamera + from picamera.array import PiRGBArray +except: + sys.exit(0) + +def focusing(val): + value = (val << 4) & 0x3ff0 + data1 = (value >> 8) & 0x3f + data2 = value & 0xf0 + # time.sleep(0.5) + print("focus value: {}".format(val)) + # bus.write_byte_data(0x0c,data1,data2) + os.system("i2cset -y %i 0x0c %d %d" % (i2cbus, data1,data2)) + +def sobel(img): + img_gray = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) + img_sobel = cv2.Sobel(img_gray,cv2.CV_16U,1,1) + return cv2.mean(img_sobel)[0] + +def laplacian(img): + img_gray = cv2.cvtColor(img,cv2.COLOR_RGB2GRAY) + img_sobel = cv2.Laplacian(img_gray,cv2.CV_16U) + return cv2.mean(img_sobel)[0] + + +def calculation(camera): + rawCapture = PiRGBArray(camera) + camera.capture(rawCapture,format="bgr", use_video_port=True) + image = rawCapture.array + rawCapture.truncate(0) + return laplacian(image) + + +if __name__ == "__main__": + #open camera + camera = picamera.PiCamera() + + #camera.awb_gains=4 + #camera.exposure_mode='off' + #camera.awb_mode='fluorescent' + #open camera preview + camera.start_preview() + #set camera resolution to 640x480(Small resolution for faster speeds.) + camera.resolution = (640, 480) + time.sleep(0.1) + print("Start focusing") + + max_index = 10 + max_value = 0.0 + last_value = 0.0 + dec_count = 0 + focal_distance = 10 + + while True: + #Adjust focus + focusing(focal_distance) + #Take image and calculate image clarity + val = calculation(camera) + #Find the maximum image clarity + if val > max_value: + max_index = focal_distance + max_value = val + + #If the image clarity starts to decrease + if val < last_value: + dec_count += 1 + else: + dec_count = 0 + #Image clarity is reduced by six consecutive frames + if dec_count > 6: + break + last_value = val + + #Increase the focal distance + focal_distance += 15 + if focal_distance > 1000: + break + + #Adjust focus to the best + focusing(max_index) + time.sleep(1) + #set camera resolution to 2592x1944 + camera.resolution = (1920,1080) + #save image to file. + camera.capture("test.jpg") + print("max index = %d,max value = %lf" % (max_index,max_value)) + #while True: + # time.sleep(1) + + camera.stop_preview() + camera.close() + + diff --git a/PhotoTakingGUI/CameraDemo/Cam_Screenshot.png b/PhotoTakingGUI/CameraDemo/Cam_Screenshot.png new file mode 100755 index 00000000..9775fe9c Binary files /dev/null and b/PhotoTakingGUI/CameraDemo/Cam_Screenshot.png differ diff --git a/PhotoTakingGUI/CameraDemo/CameraScene.py b/PhotoTakingGUI/CameraDemo/CameraScene.py new file mode 100755 index 00000000..265e9cfd --- /dev/null +++ b/PhotoTakingGUI/CameraDemo/CameraScene.py @@ -0,0 +1,154 @@ +''' + CAMERA_SCENE +------------------ +Instructions: +1. Ensure a version of python has been installed (created on Python 3.11.4) +2. Run the command "pip install Pillow" +3. Run the command "pip install opencv-python" +------------------ +''' +# import PythonFiles +import json, logging +import tkinter as tk +import cv2 +import PIL.Image, PIL.ImageTk +import time +import os + + +################################################################################# + +# Instantiating logging +# Code that should go in every file in the GUI(s) +logging.getLogger('PIL').setLevel(logging.WARNING) +logger = logging.getLogger('HGCAL_GUI') +FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +logging.basicConfig(filename="/home/{}/GUILogs/visual_gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + + +# Frame class for basic webcam functionality +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data +class CameraScene(tk.Frame): + + def __init__(self, parent, master_frame, data_holder, video_source=0 ): + logging.info("CameraScene: Beginning to instantiate the CameraScene.") + print("\nCameraScene: Beginning to instantiate the CameraScene.") + + # Call to the super class's constructor + # Super class is the tk.Frame class + super().__init__(master_frame, width=850, height=500) + + logging.info("\nCameraScene: Frame has been created.") + + self.data_holder = data_holder + self.parent = parent + + # Creates instance variable of parameter video_source + self.video_source=video_source + + # Adds the video capturing component to the canvas + # Then packs the canvas to the frame + self.vid= MyVideoCapture(self.video_source) + self.canvas=tk.Canvas(self, width=self.vid.width, height = self.vid.height) + self.canvas.pack() + + # Frame for the buttons + btn_frame=tk.Frame(self, background=self.from_rgb((117, 123, 129))) + btn_frame.place(x=0,y=0, anchor="nw", width=self.vid.width+4) + + # Snapshot button + self.btn_snapshot=tk.Button(btn_frame, text="Snapshot",width=20, command=self.snapshot, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_snapshot.pack(side="left", padx=10, pady=10) + + # Proses button + # Empty command; could be linked with more features + self.btn_proses=tk.Button(btn_frame, text="Proses", width=10, command=None, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_proses.pack(side="left", padx=10, pady=10) + + # About button + # Empty command; could be linked with more features + self.btn_about=tk.Button(btn_frame, text="About", width=10, command=None, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_about.pack(side="right", padx=10, pady=10) + + # How long in between photo-frames on the GUI + self.delay=15 + + # Updates the video constantly on this slide + self.update() + + + + # Takes a snapshot of the video + # Saves in the same directory as the + def snapshot(self): + ret, frame=self.vid.get_frame() + + if ret: + # Writes the image to a file with a name that includes the date + # TODO Change this to be a more readable file name later + self.photo_name = "frame-"+time.strftime("%d-%m-%Y-%H-%M-%S") + ".jpg" + + try: + cv2.imwrite(self.photo_name, cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) ) + # self.data_holder. + except: + logging.warning("CameraScene: Unable to write the snapshot to a file") + print("\nUnable to save photo to file {}".format(self.photo_name)) + + else: + print("\nUnable to take a snapshot.\n") + + # This is the key method to show the active camera on the GUI + # Basically gets an image from the camera and updates the PIL Image Canvas + # Updates the canvas after "self.delay" milliseconds + def update(self): + ret, frame=self.vid.get_frame() + + if ret: + # Updates the canvas on the GUI + self.photo = PIL.ImageTk.PhotoImage(image=PIL.Image.fromarray(frame)) + self.canvas.create_image(0,0, image=self.photo, anchor=tk.NW) + + # Recursive method; waits "self.delay" before calling itself + self.window.after(self.delay,self.update) + + def from_rgb(self,rgb): + return "#%02x%02x%02x" % rgb + +# Class responsible for the video capturing feature +# Requires the import of cv2 +class MyVideoCapture: + """docstring for MyVideoCapture""" + def __init__(self, video_source=0): + # Instantiating a VideoCapture device from cv2 library + self.vid = cv2.VideoCapture(video_source) + + # Throw an exception if the video source cannot be opened. + if not self.vid.isOpened(): + raise ValueError("VideoCapture: Unable to open the video source. Check camera.", video_source) + + # Getting the camera width and height to correctly display resolution + # Important component for a correct display + self.width = self.vid.get(cv2.CAP_PROP_FRAME_WIDTH) + self.height = self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT) + + # Gets the VideoCapture video + # Called by the update() method + def get_frame(self): + if self.vid.isOpened(): + ret, frame = self.vid.read() + if ret: + return (ret, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + else: + return (ret, None) + else: + return (ret,None) + + # Closes the video camera with the "release()" command + # Important for closing gracefully + def __del__(self): + if self.vid.isOpened(): + self.vid.release() + diff --git a/PhotoTakingGUI/CameraDemo/MainFunction.py b/PhotoTakingGUI/CameraDemo/MainFunction.py new file mode 100755 index 00000000..a40f5568 --- /dev/null +++ b/PhotoTakingGUI/CameraDemo/MainFunction.py @@ -0,0 +1,131 @@ +''' + CAMERA GUI +------------------ +Instructions: +1. Ensure a version of python has been installed (created on Python 3.11.4) +2. Run the command "pip install Pillow" +3. Run the command "pip install opencv-python" +4. Run the command "pip install tkinter" +5. Run the GUI; "python ./MainFunction.py" +------------------ + +''' + +import tkinter +import cv2 +import PIL.Image, PIL.ImageTk +import time + +from picamera2 import Picamera2 + +# GUI class for basic webcam functionality +# Sample class instantiation: App(tkinter.Tk(),"tkinter ad OpenCV") +class App: + + def __init__(self, window, window_title, video_source=0): + + # Sets up the tkinter window + self.window=window + self.window.title=(window_title) + + # Creates instance variable of parameter video_source + self.video_source=video_source + + # Adds the video capturing component to the canvas + # Then packs the canvas to the frame + self.vid= MyVideoCapture(self.video_source) + self.canvas=tkinter.Canvas(window, width=self.vid.width, height = self.vid.height) + self.canvas.pack() + + # Frame for the buttons + btn_frame=tkinter.Frame(window, background=self.from_rgb((117, 123, 129))) + btn_frame.place(x=0,y=0, anchor="nw", width=self.vid.width+4) + + # Snapshot button + self.btn_snapshot=tkinter.Button(btn_frame, text="Snapshot",width=20, command=self.snapshot, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_snapshot.pack(side="left", padx=10, pady=10) + + # Proses button + # Empty command; could be linked with more features + self.btn_proses=tkinter.Button(btn_frame, text="Proses", width=10, command=None, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_proses.pack(side="left", padx=10, pady=10) + + # About button + # Empty command; could be linked with more features + self.btn_about=tkinter.Button(btn_frame, text="About", width=10, command=None, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_about.pack(side="right", padx=10, pady=10) + + # How long in between photo-frames on the GUI + self.delay=15 + + self.update() + + # Required for tkinter to stay open + # Closing the mainloop will close the program + self.window.mainloop() + + # Takes a snapshot of the video + # Saves in the same directory as the + def snapshot(self): + ret, frame=self.vid.get_frame() + + if ret: + # Writes the image to a file with a name that includes the date + # TODO Change this to be a more readable file name later + cv2.imwrite("frame-"+time.strftime("%d-%m-%Y-%H-%M-%S") + ".jpg", cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) ) + else: + print("\nUnable to take a snapshot.\n") + + # This is the key method to show the active camera on the GUI + # Basically gets an image from the camera and updates the PIL Image Canvas + # Updates the canvas after "self.delay" milliseconds + def update(self): + ret, frame=self.vid.get_frame() + + if ret: + # Updates the canvas on the GUI + self.photo = PIL.ImageTk.PhotoImage(image=PIL.Image.fromarray(frame)) + self.canvas.create_image(0,0, image=self.photo, anchor=tkinter.NW) + + # Recursive method; waits "self.delay" before calling itself + self.window.after(self.delay,self.update) + + def from_rgb(self,rgb): + return "#%02x%02x%02x" % rgb + +# Class responsible for the video capturing feature +# Requires the import of cv2 +class MyVideoCapture: + """docstring for MyVideoCapture""" + def __init__(self, video_source=0): + # Instantiating a VideoCapture device from cv2 library + self.vid = cv2.VideoCapture(video_source) + + # Throw an exception if the video source cannot be opened. + if not self.vid.isOpened(): + raise ValueError("VideoCapture: Unable to open the video source. Check camera.", video_source) + + # Getting the camera width and height to correctly display resolution + # Important component for a correct display + self.width = self.vid.get(cv2.CAP_PROP_FRAME_WIDTH) + self.height = self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT) + + # Gets the VideoCapture video + # Called by the update() method + def get_frame(self): + if self.vid.isOpened(): + ret, frame = self.vid.read() + if ret: + return (ret, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + else: + return (ret, None) + else: + return (ret,None) + + # Closes the video camera with the "release()" command + # Important for closing gracefully + def __del__(self): + if self.vid.isOpened(): + self.vid.release() + +App(tkinter.Tk(),"tkinter ad OpenCV") diff --git a/PhotoTakingGUI/CameraDemo/TestFunction.py b/PhotoTakingGUI/CameraDemo/TestFunction.py new file mode 100755 index 00000000..793625b1 --- /dev/null +++ b/PhotoTakingGUI/CameraDemo/TestFunction.py @@ -0,0 +1,140 @@ +''' + CAMERA GUI +------------------ +Instructions: +1. Ensure a version of python has been installed (created on Python 3.11.4) +2. Run the command "pip install Pillow" +3. Run the command "pip install opencv-python" +4. Run the command "pip install tkinter" +5. Run the GUI; "python ./MainFunction.py" +------------------ + +''' + +import tkinter +import cv2 +import PIL.Image, PIL.ImageTk +import time + +# GUI class for basic webcam functionality +# Sample class instantiation: App(tkinter.Tk(),"tkinter ad OpenCV") +class App: + + def __init__(self, window, window_title, video_source=0): + + # Sets up the tkinter window + self.window=window + self.window.title=(window_title) + + # Creates instance variable of parameter video_source + self.video_source=video_source + + # Adds the video capturing component to the canvas + # Then packs the canvas to the frame + self.vid= MyVideoCapture(self.video_source) + self.canvas=tkinter.Canvas(window, width=self.vid.width, height = self.vid.height) + self.canvas.pack() + + # Frame for the buttons + btn_frame=tkinter.Frame(window, background=self.from_rgb((117, 123, 129))) + btn_frame.place(x=0,y=0, anchor="nw", width=800) + + # Snapshot button + self.btn_snapshot=tkinter.Button(btn_frame, text="Snapshot",width=20, command=self.snapshot, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_snapshot.pack(side="left", padx=10, pady=10) + + # Proses button + # Empty command; could be linked with more features + self.btn_proses=tkinter.Button(btn_frame, text="Proses", width=10, command=None, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_proses.pack(side="left", padx=10, pady=10) + + # About button + # Empty command; could be linked with more features + self.btn_about=tkinter.Button(btn_frame, text="About", width=10, command=None, bg=self.from_rgb((52, 61, 70)), fg="white") + self.btn_about.pack(side="right", padx=10, pady=10) + + # How long in between photo-frames on the GUI + self.delay=10 + + self.update() + + # Required for tkinter to stay open + # Closing the mainloop will close the program + self.window.mainloop() + + # Takes a snapshot of the video + # Saves in the same directory as the + def snapshot(self): + ret, frame=self.vid.get_frame() + + if ret: + # Writes the image to a file with a name that includes the date + # TODO Change this to be a more readable file name later + cv2.imwrite("frame-"+time.strftime("%d-%m-%Y-%H-%M-%S") + ".jpg", cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) ) + else: + print("\nUnable to take a snapshot.\n") + + # This is the key method to show the active camera on the GUI + # Basically gets an image from the camera and updates the PIL Image Canvas + # Updates the canvas after "self.delay" milliseconds + def update(self): + ret, frame=self.vid.get_frame() + + if ret: + # Updates the canvas on the GUI + image=PIL.Image.fromarray(frame) + image = image.resize((1036,778)) + self.photo = PIL.ImageTk.PhotoImage(image) + self.canvas.create_image(0,0, image=self.photo, anchor=tkinter.NW) + + # Recursive method; waits "self.delay" before calling itself + self.window.after(self.delay,self.update) + + def from_rgb(self,rgb): + return "#%02x%02x%02x" % rgb + +# Class responsible for the video capturing feature +# Requires the import of cv2 +class MyVideoCapture: + """docstring for MyVideoCapture""" + def __init__(self, video_source=0): + # Instantiating a VideoCapture device from cv2 library + self.vid = cv2.VideoCapture(0) + self.vid.set(cv2.CAP_PROP_FRAME_WIDTH, 2047) + self.vid.set(cv2.CAP_PROP_FRAME_HEIGHT, 1536) + + width = int(self.vid.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + print("\nImage Width: {}, Image Height: {}".format(width, height)) + + + # Throw an exception if the video source cannot be opened. + if not self.vid.isOpened(): + raise ValueError("VideoCapture: Unable to open the video source. Check camera.", video_source) + + # Getting the camera width and height to correctly display resolution + # Important component for a correct display + self.width = width + self.height = height + + # Gets the VideoCapture video + # Called by the update() method + def get_frame(self): + if self.vid.isOpened(): + ret, frame = self.vid.read() + if ret: + return (ret, cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + else: + return (ret, None) + else: + return (ret,None) + + # Closes the video camera with the "release()" command + # Important for closing gracefully + def __del__(self): + if self.vid.isOpened(): + self.vid.release() + +# How to instantiate this GUI +App(tkinter.Tk(),"tkinter ad OpenCV") diff --git a/PhotoTakingGUI/CameraDemo/frame-11-07-2023-10-02-20.jpg b/PhotoTakingGUI/CameraDemo/frame-11-07-2023-10-02-20.jpg new file mode 100644 index 00000000..d8ee9779 Binary files /dev/null and b/PhotoTakingGUI/CameraDemo/frame-11-07-2023-10-02-20.jpg differ diff --git a/PhotoTakingGUI/CameraDemo/frame-11-07-2023-10-25-31.jpg b/PhotoTakingGUI/CameraDemo/frame-11-07-2023-10-25-31.jpg new file mode 100644 index 00000000..73ba57d1 Binary files /dev/null and b/PhotoTakingGUI/CameraDemo/frame-11-07-2023-10-25-31.jpg differ diff --git a/PhotoTakingGUI/CameraDemo/original_photo.jpg b/PhotoTakingGUI/CameraDemo/original_photo.jpg new file mode 100644 index 00000000..8fa81033 Binary files /dev/null and b/PhotoTakingGUI/CameraDemo/original_photo.jpg differ diff --git a/PhotoTakingGUI/CameraDemo/test.jpg b/PhotoTakingGUI/CameraDemo/test.jpg new file mode 100644 index 00000000..e84c2caf Binary files /dev/null and b/PhotoTakingGUI/CameraDemo/test.jpg differ diff --git a/PhotoTakingGUI/Configs/Demo_Local_cfg.yaml b/PhotoTakingGUI/Configs/Demo_Local_cfg.yaml new file mode 100644 index 00000000..5acab778 --- /dev/null +++ b/PhotoTakingGUI/Configs/Demo_Local_cfg.yaml @@ -0,0 +1,88 @@ +--- + +# Specify which board you want to test here +GUIType: Demo + +# Specify if a scanner is used for barcodes +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# Uncomment the test handler which you would like to use +# +# Note that for SSH, the username and hostname must be specified. +# It is assumed that you have set up SSH key access for this username +# On the specified host. +# +# ZMQ will use the built-in request server and client. +# The IP address of the GUI node and testing node must be specified separately +# Additionally, SSH key access removes the need to start the server +# on the tester by hand. Path and server file name needed for remote +# server start up +# +TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +#TestHandler: {name: ZMQ, localip: , remoteip: , username: , serverpath: , serverscript: } + + +# Let the GUI know if you want to check serial numbers for multiple board types at a single testing location +SerialCheckSafe: false + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py +- name: Counting2 + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting2 + TestPath: Tests + TestScript: demo_count.py + +######################################## +# NOTE TO SELF: Add in test sequence as # +# a separate entry in config # +# All tests in one area, not physical # +# vs. automatic and add a test type # +# # +######################################## + + +# Tests which require a physical measurement +# The pass fail criteria for these tests are specified here +PhysicalTest: + - name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + ################################# + # REMOVE FOR DEMO # + ################################# + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: false + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" + +People: + - Caleb + +... diff --git a/PhotoTakingGUI/Configs/Demo_SSH_cfg.yaml b/PhotoTakingGUI/Configs/Demo_SSH_cfg.yaml new file mode 100644 index 00000000..42026ecf --- /dev/null +++ b/PhotoTakingGUI/Configs/Demo_SSH_cfg.yaml @@ -0,0 +1,71 @@ +--- +#----------------------------------------------- +# Note: Demo will not work out of the box +# Need to follow installation procedure in +# README.md +#----------------------------------------------- + +# Specify which board you want to test here +# this doesn't actually matter much, the GUI type will update based on the board type entered in scan scene +GUIType: Demo + +# Specify if a scanner is used for barcodes +UsingScanner: false + +# How the tests will be run (local, ssh, or ZMQ) +# Uncomment the test handler which you would like to use +# +# Note that for SSH, the username and hostname must be specified. +# It is assumed that you have set up SSH key access for this username on the specified host. +# +# ZMQ will use the built-in request server and client. +# The IP address of the GUI node and testing node must be specified separately +# Additionally, SSH key access removes the need to start the server +# on the tester by hand. Path and server file name needed for remote +# server start up +#TestHandler: {name: Local, remoteip: localhost} +TestHandler: {name: SSH, username: bovar008, hostname: cmsfactory2, remoteip: } +#TestHandler: {name: ZMQ, localip: , remoteip: , username: , serverpath: , serverscript: } + +# Let the GUI know if you want to check serial numbers for multiple board types at a single testing location +SerialCheckSafe: false + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestPath and TestScript aren't used in SSH +# TestCommand field is the command run in SSH, -u is needed for realtime output from a python script +# TestPath should be in reference to the testing home directory +Test: +- name: Counting + required: true + desc_short: Count to ten + desc_long: Simple for loop with waits to simulate a tests running locally + TestClass: counting + TestPath: Tests + TestScript: demo_count.py + TestCommand: python3 -u HGCALTestGUI/Tests/demo_count.py + TestConfig: HGCALTestGUI/Tests/test_configs/counting.yaml + +# Tests which require a physical measurement +# The pass fail criteria for these tests are specified here +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: false + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/PhotoTakingGUI/Configs/Engine_cfg.py b/PhotoTakingGUI/Configs/Engine_cfg.py new file mode 100644 index 00000000..0afac8a1 --- /dev/null +++ b/PhotoTakingGUI/Configs/Engine_cfg.py @@ -0,0 +1,223 @@ +base_path = "/home/HGCAL_dev/test_scripts" + +from dumpToYaml import dump_to_yaml + +masterCfg = { + + "GUIType": "Engine", + + "UsingScanner": True, + + + # Order of tests matters here + # This should be the same order that you want the tests to be run in + # Number of test will also be decide by this list so don't miss any + "Test": [ + { + "name": "Power-Ground Resistance", + "required": 1, + "desc_short": "Measure resistance between power and ground", + "desc_long": "Check that the power and grounds are not shorted at the terminal, or between the inputs.", + "TestClass" : "TestPowerGround", + }, + { + "name": "1.5V Input Check", + "required": 1, + "desc_short": "Check that the 1.5V input is not shorted.", + "desc_long": "Check that resistance between across C906 or C908 is non-zero.", + "TestClass" : "Test1.5VInput", + }, + { + "name": "10V Input Check", + "required": 1, + "desc_short": "Check that the 10V input is not shorted.", + "desc_long": "Check that resistance between across C907 or C909 is non-zero.", + "TestClass" : "Test10VInput", + }, + { + "name": "1.2V Output Check", + "required": 1, + "desc_short": "Check that the 1.2V output is not shorted.", + "desc_long": "Check that resistance between across C904 or C904 or TP901 is non-zero.", + "TestClass" : "Test1.2VOutput", + }, + { + "name": "RX 2.5V Output Check", + "required": 1, + "desc_short": "Check that the RX 2.5V output is not shorted.", + "desc_long": "Check that resistance across C902 is non-zero.", + "TestClass" : "TestRX2.5VOutput", + }, + { + "name": "TX 2.5V Output Check", + "required": 1, + "desc_short": "Check that the TX 2.5V output is not shorted.", + "desc_long": "Check that resistance across either C903 or TP902 is non-zero.", + "TestClass" : "TestTX2.5VOutput", + }, + # Power on Tests + { + "name": "LDO Output", + "required": 1, + "desc_short": "Check that the LDO output voltage is around 1.2V", + "desc_long": "Measure the votlage across either R911 or TP901 and verify that it is appropriate.", + "TestClass" : "TestLDOOutput", + }, + { + "name": "LinPol RX Check", + "required": 1, + "desc_short": "Check that the RX voltage from the linppol is operating correctly", + "desc_long": "Check that voltages across either R905 or R902 is 2.5V.", + "TestClass" : "TestLinPolRX", + }, + { + "name": "LinPol TX Check", + "required": 1, + "desc_short": "Check that the TX voltage from the linppol is operating correctly", + "desc_long": "Measure the voltage across either TP902 or R906 or C903 is 2.5V.", + "TestClass" : "TestLinPolTX", + }, + + #Operations Tests + { + "name": "X_PWR", + "required": 1, + "desc_short": "Check the the X_PWR voltage is correct.", + "desc_long": "Measure using the tester, and should find approximately 1.2V.", + "TestClass" : "TestXPWR", + }, + { + "name": "lpGBT setup", + "required": 1, + "desc_short": "Ensure setup can be performed", + "desc_long": "Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz", + "TestClass" : "TestlpGBTsetup", + }, + { + "name": "EClock Rates", + "required": 1, + "desc_short": "Ensure EClock rates are correct", + "desc_long": "Check that all EClocks are running at 320MHz.", + "TestClass" : "TestEClock", + }, + { + "name": "lpGBT IC/EC communication", + "required": 1, + "desc_short": "Check operability of lpGBT IC/EC communication", + "desc_long": "Read and write to lpBGT registers via ICEC. Check DAQ lpGBT read of registers via IC. Check Trigger lpGBTs: successful read registers via EC. Ensure write and readback to user ID registers (0x004 - 0x007)", + "TestClass" : "TestlpGBTcom", + }, + { + "name": "I2C", + "required": 1, + "desc_short": "Engine can use I2C master", + "desc_long": "Check that engine can communicate as an I2C master", + #"TestScript": "engine_test_suite.py", + "TestClass" : "TestI2C", + + }, + { + "name": "GPIO functionality", + "required": 1, + "desc_short": "Check the quality of the GPIOs", + "desc_long": "Read and write to all GPIO channels and verify levels. Write nominal configuration and then toggle each line one-by-one and verify change in both lpGBT status and voltage level", + "TestClass" : "TestGpio" + }, + { + "name": "ADC functionality", + "required": 1, + "desc_short": "Check quality of the ADCs", + "desc_long": "Measure known voltages/resistances. Check measured values for all 4 gains within tolerances, (only need to do all 4 gains for one measurement).", + "TestClass" : "TestAdc" + }, + { + "name": "Uplink quality", + "required": 1, + "desc_short": "Check the quality of the uplinks", + "desc_long": "PRBS validation from lpGBTs. Check bit error rate below threshold.", + "TestClass" : "TestUplink" + }, + { + "name": "Downlink quality", + "required": 1, + "desc_short": "Check the quality of the downlinks", + "desc_long": "Eye opening test. Check eye opening width and height below threshold.", + "TestClass" : "TestDownlink", + }, + { + "name": "Fast Command quality", + "required": 1, + "desc_short": "Check the quality of the Fast Command path", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestFC" + }, + { + "name": "Elink quality", + "required": 1, + "desc_short": "Check the quality of the elinks", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestElinkUp" + }, + { + "name": "Crossover link quality", + "required": 1, + "desc_short": "Check the quality of the crossover links", + "desc_long": "PRBS validation from and back to ZCU. Check bit error rate below threshold.", + "TestClass" : "TestCrossover", + }, + ], + + + "PhysicalTest": [ + #{ + # "name": "SAMPLE test", + # "required": 1, + # "desc_short": "Some short description", + # "desc_long": "Really long description for later purposes.", + # "criteria": { + # "first testing criteria", + # "second testing criteria", + # "third testing criteria", + # }, + + #}, + + ], + + "Board_type": [ + { + "name": "Engine V3 Right", + "type_sn": "100300", + "requiredTests": [0, 1, 2, 3, 4], + }, + { + "name": "Engine V3 Left", + "type_sn": "100310", + "requiredTests": [0, 1, 2, 3, 4], + }, + ], + # People who you would like to add as testers by default + # HGCAL_dev can be used for debug testing in the beginning + # The GUI will require everyone to have their own "account" + "People": [ + "Nadja", + "Charlie", + "Bryan", + "Devin", + "HGCAL_dev", + ], + # Information for sending and receiving data to/from the database + # Needs to be different based on board type + "DBInfo": { + "use_database": True, + "name": "EngineDB", + "reader": "EngineDBReadUser", + "inserter": "EngineDBInserter", + "admin": "EngineDBInserter", + "baseURL": "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/EngineDB", + }, +} + +masterCfg["Test"] = [dict(**x, TestPath=base_path, TestScript= "engine_test_suite.py") for x in masterCfg["Test"] if "TestClass" in x] + +dump_to_yaml(masterCfg) diff --git a/PhotoTakingGUI/Configs/Engine_cfg.yaml b/PhotoTakingGUI/Configs/Engine_cfg.yaml new file mode 100644 index 00000000..2c44e810 --- /dev/null +++ b/PhotoTakingGUI/Configs/Engine_cfg.yaml @@ -0,0 +1,124 @@ +GUIType: Engine + +People: [Nadja, Charlie, Bryan, Devin, HGCAL_dev] + +Board_type: + - name: Engine V3 Right + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100300' + - name: Engine V3 Left + requiredTests: [0, 1, 2, 3, 4] + type_sn: '100310' + +DBInfo: + admin: FactoryInserter + baseURL: http://cmslab1.spa.umn.edu/Factory/EngineDB + #baseURL: http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/EngineDB + inserter: FactoryInserter + name: EngineDB_PRO + reader: FactoryReadUser + use_database: true + + +SerialCheckSafe: true +UsingScanner: true + +# TestHandler: {name: Local, remoteip: localhost} +# TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: "localhost", remoteip: "umn-zcu102-b", username: "test_server", + serverpath: , serverscript: } + + +PhysicalTest: [] +Test: + - TestClass: TestXPWR + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Measure using the tester, and should find approximately 1.2V. + desc_short: Check the the X_PWR voltage is correct. + name: X_PWR + required: 1 + + - TestClass: TestSetupLpgbt + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Perform nominal setup from BE. Do setup, link trick, setup. Check PUSMStatus + (0x1d9) reports READY (0x13) for all 3 lpGBTs. Check lpGBTs locked to BE All + 3 RX equal within 200 Hz. Check All 3 RX-DV equal within 200 Hz + desc_short: Ensure setup can be performed + name: lpGBT setup + required: 1 + + - TestClass: TestLpgbtId + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_startup.py + desc_long: Read lpgbt ids + desc_short: Check ids + name: LPGBT ID + required: 1 + + + - TestClass: TestI2C + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_i2c.py + desc_long: Check that engine can communicate as an I2C master + desc_short: Engine can use I2C master + name: I2C + required: 1 + + - TestClass: TestGpio + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_gpio.py + desc_long: Read and write to all GPIO channels and verify levels. Write nominal + configuration and then toggle each line one-by-one and verify change in both + lpGBT status and voltage level + desc_short: Check the quality of the GPIOs + name: GPIO functionality + required: 1 + + - TestClass: TestAdc + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_adc.py + desc_long: Measure known voltages/resistances. Check measured values for all 4 + gains within tolerances, (only need to do all 4 gains for one measurement). + desc_short: Check quality of the ADCs + name: ADC functionality + required: 1 + + - TestClass: TestUplink + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from lpGBTs. Check bit error rate below threshold. + desc_short: Check the quality of the uplinks + name: Uplink quality + required: 1 + + + - TestClass: TestFC + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the Fast Command path + name: Fast Command quality + required: 1 + + - TestClass: TestElinkUp + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: PRBS validation from and back to ZCU. Check bit error rate below threshold. + desc_short: Check the quality of the elinks + name: Elink quality + required: 1 + + - TestClass: TestEClockRates + TestPath: /home/test_server/test_code/engine_tests + TestScript: test_links.py + desc_long: Check that all EClocks are running at 320MHz. + desc_short: Ensure EClock rates are correct + name: EClock Rates + required: 1 + + +Photo: +- {name: 'Top', desc_short: 'Top side of the board'} +- {name: 'Bottom', desc_short: 'Bottom side of the board'} diff --git a/PhotoTakingGUI/Configs/Flex_cfg.yaml b/PhotoTakingGUI/Configs/Flex_cfg.yaml new file mode 100644 index 00000000..3ba55dcf --- /dev/null +++ b/PhotoTakingGUI/Configs/Flex_cfg.yaml @@ -0,0 +1,32 @@ +--- +DBInfo: + baseURL: http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB + inserter: WagonDBInserter + name: WagonDB + reader: WagonDBReadUser + use_database: false +GUIType: FlexCable +SerialCheckSafe: false +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: cmstester8, username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/FlexTest/FlexCableTesting + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: ID Resistor Measurement + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/FlexTest/FlexCableTesting + TestScript: run_bert_tmp.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Bit Error Rate Test + required: 1 +UsingScanner: true + +People: [Lauren, Billy] +... diff --git a/PhotoTakingGUI/Configs/Skeleton_cfg.yaml b/PhotoTakingGUI/Configs/Skeleton_cfg.yaml new file mode 100644 index 00000000..2968c97c --- /dev/null +++ b/PhotoTakingGUI/Configs/Skeleton_cfg.yaml @@ -0,0 +1,67 @@ +--- + +# Specify which board you want to test here +GUIType: Wagon + +# Specify if a scanner is used for barcodes +UsingScanner: true + +# How the tests will be run (local, ssh, or ZMQ) +TestHandler: ZMQ + +# Order of tests matters here +# This should be the same order that you want the tests to be run in +# Number of test will also be decided by this list so don't miss any +# TestClass, TestScript, and TestPath fields will be used to write the REPserver script +# TestPath should be in reference to the testing home directory +Test: +- name: Resistance Measurement + required: true + desc_short: Measure resistance of analog lines + desc_long: Test must be completed before attempting to measure ID resistor + TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py +- name: ID Resistor Measurement + required: true + desc_short: Measure resistance of ID resistor + desc_long: Must be completed after the general resistance measurement + TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py +- name: I2C Read/Write + required: true + desc_short: Check I2C read/write along wagon + desc_long: Test must be completed before BERT for wagon wheel configuration + TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw + TestScript: run_iic_check.py +- name: Bit Error Rate Test + required: true + desc_short: Determine quality of data transmission + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + TestClass: BERT + TestPath: /home/HGCAL_dev/sw + TestScript: run_bert.py + + +PhysicalTest: +- name: SAMPLE test + required: true + desc_short: Some short description of the test + desc_long: Long description of the test + criteria: + - first testing criteria + - second testing criteria + - third testing criteria + + +# Information for sending and receiving data to/from the database +# Needs to be different based on board type +DBInfo: + use_database: true + name: WagonDB + reader: WagonDBReadUser + inserter: WagonDBInserter + baseURL: "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" +... diff --git a/PhotoTakingGUI/Configs/Wagon_cfg.py b/PhotoTakingGUI/Configs/Wagon_cfg.py new file mode 100644 index 00000000..4099544d --- /dev/null +++ b/PhotoTakingGUI/Configs/Wagon_cfg.py @@ -0,0 +1,85 @@ +from dumpToYaml import dump_to_yaml + +masterCfg = { + + "GUIType": "Wagon", + + "UsingScanner": True, + + # Order of tests matters here + # This should be the same order that you want the tests to be run in + # Number of test will also be decide by this list so don't miss any + # TestClass, TestScript, and TestPath fields will be used to write the REPserver script + # TestPath should be in reference to the testing home directory + "Test": [ + { + "name": "Resistance Measurement", + "required": 1, + "desc_short": "Measure resistance of analog lines", + "desc_long": "Test must be completed before attempting to measure ID resistor", + "TestClass": "gen_resist_test", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "wagon_rtd.py" + }, + + { + "name": "ID Resistor Measurement", + "required": 1, + "desc_short": "Measure resistance of ID resistor", + "desc_long": "Must be completed after the general resistance measurement", + "TestClass": "id_resist_test", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "wagon_rtd.py" + }, + + { + "name": "I2C Read/Write", + "required": 1, + "desc_short": "Check I2C read/write along wagon", + "desc_long": "Test must be completed before BERT for wagon wheel configuration", + "TestClass": "IIC_Check", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "run_iic_check.py" + }, + + { + "name": "Bit Error Rate Test", + "required": 1, + "desc_short": "Determine quality of data transmission", + "desc_long": "Needs to be completed after I2C check in order to set up wagon wheel", + "TestClass": "BERT", + "TestPath": "/home/HGCAL_dev/sw", + "TestScript": "run_bert.py" + }, + ], + + + "PhysicalTest": [ + #{ + # "name": "SAMPLE test", + # "required": 1, + # "desc_short": "Some short description", + # "desc_long": "Really long description for later purposes.", + # "criteria": { + # "first testing criteria", + # "second testing criteria", + # "third testing criteria", + # }, + + #}, + + ], + + + # Information for sending and receiving data to/from the database + # Needs to be different based on board type + "DBInfo": { + "use_database": True, + "name": "WagonDB", + "reader": "WagonDBReadUser", + "inserter": "WagonDBInserter", + "baseURL": "http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB" + } + } + +dump_to_yaml(masterCfg) diff --git a/PhotoTakingGUI/Configs/Wagon_cfg.yaml b/PhotoTakingGUI/Configs/Wagon_cfg.yaml new file mode 100644 index 00000000..6a511928 --- /dev/null +++ b/PhotoTakingGUI/Configs/Wagon_cfg.yaml @@ -0,0 +1,55 @@ +--- +DBInfo: + admin: FactoryInserter + #baseURL: http://cmslab3.spa.umn.edu/~cros0400/cgi-bin/WagonDB + baseURL: http://cmslab1.spa.umn.edu/Factory/WagonDB + inserter: FactoryInserter + name: WagonDB_PRO + reader: FactoryReadUser + use_database: true +GUIType: Wagon +SerialCheckSafe: true +#TestHandler: {name: Local, remoteip: localhost} +#TestHandler: {name: SSH, username: , hostname: , remoteip: } +TestHandler: {name: ZMQ, localip: , remoteip: 192.168.140.22, username: , serverpath: , serverscript: } +PhysicalTest: [] +Test: +- TestClass: ADC + TestPath: /home/HGCAL_dev/sw + TestScript: run_adc_self_test.py + desc_long: Checking all ADCs to see if they are running as expected before running any tests + desc_short: ADC interal test + name: ADC Self Test + required: 1 +- TestClass: gen_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py + desc_long: Test must be completed before attempting to measure ID resistor + desc_short: Measure resistance of analog lines + name: Resistance Measurement + required: 1 +- TestClass: id_resist_test + TestPath: /home/HGCAL_dev/sw + TestScript: wagon_rtd.py + desc_long: Must be completed after the general resistance measurement + desc_short: Measure resistance of ID resistor + name: ID Resistor Measurement + required: 1 +- TestClass: IIC_Check + TestPath: /home/HGCAL_dev/sw + TestScript: run_iic_check.py + desc_long: Test must be completed before BERT for wagon wheel configuration + desc_short: Check I2C read/write along wagon + name: I2C Read/Write + required: 1 +- TestClass: BERT + TestPath: /home/HGCAL_dev/sw + TestScript: run_bert.py + desc_long: Needs to be completed after I2C check in order to set up wagon wheel + desc_short: Determine quality of data transmission + name: Bit Error Rate Test + required: 1 +UsingScanner: true +Photo: +- {name: 'Top', desc_short: 'Top side of the board'} +- {name: 'Bottom', desc_short: 'Bottom side of the board'} diff --git a/PhotoTakingGUI/Configs/__init__.py b/PhotoTakingGUI/Configs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PhotoTakingGUI/Configs/convert_to_yaml.py b/PhotoTakingGUI/Configs/convert_to_yaml.py new file mode 100644 index 00000000..819dc9aa --- /dev/null +++ b/PhotoTakingGUI/Configs/convert_to_yaml.py @@ -0,0 +1,26 @@ +#This script will convert any python dictionaries you want into yaml +#visit dumptoyaml.py for doing this within another python script + +import yaml + +#Import any dictionary files you want to convert to yaml here +from Engine_cfg import masterCfg as master_engine +from Wagon_cfg import masterCfg as master_wagon + +#Create a dictionary with the names of the yaml files you want to create as keys +#and the dictionary files as the values +#Only put alphanumeric characters into the files name, no spaces +py_files = { + 'Engine_cfg': master_engine, + 'Wagon_cfg': master_wagon, + } + +write_yaml(py_files) + +#This function can be passed a dictionary from other functions and will write a yaml file with that dictionary +def write_yaml(dictionary): + #This for loop iterates over all entries in the dictionary and creates a corresponding yaml file + for i in dictionary: + file = open(i + '.yaml', 'w') + yaml.dump(dictionary[i], file) + file.close() diff --git a/PhotoTakingGUI/Configs/dumpToYaml.py b/PhotoTakingGUI/Configs/dumpToYaml.py new file mode 100644 index 00000000..67ceab7d --- /dev/null +++ b/PhotoTakingGUI/Configs/dumpToYaml.py @@ -0,0 +1,17 @@ +import yaml + +from glob import glob + +def dump_to_yaml(mastercfg): + + yaml_string = yaml.dump(mastercfg) + + + with open("temp.yaml", "w") as f: + + f.write(yaml_string) + + f.close() + + + diff --git a/PhotoTakingGUI/MainFunctionVI.py b/PhotoTakingGUI/MainFunctionVI.py new file mode 100644 index 00000000..458be856 --- /dev/null +++ b/PhotoTakingGUI/MainFunctionVI.py @@ -0,0 +1,89 @@ +# Need to make the log file path before any imports +import os +guiLogPath = "/home/{}/GUILogs/".format(os.getlogin()) + +if not os.path.exists(guiLogPath): + os.makedirs(guiLogPath) + +import sys +sys.path.append("..") + +# Imports the GUIWindow +from PythonFiles.GUIWindow import GUIWindow +import socket +import logging +import logging.handlers +import yaml + +logger = logging.getLogger('HGCAL_Photo') +logger.setLevel(logging.DEBUG) + +if not logger.handlers: + fh = logging.handlers.TimedRotatingFileHandler(guiLogPath + "photo_gui.log", when="midnight", interval=1) + fh.setLevel(logging.DEBUG) + + ch = logging.StreamHandler() + ch.setLevel(logging.ERROR) + + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + logger.addHandler(fh) + logger.addHandler(ch) + +class StreamToLogger(object): + def __init__(self, logger, level): + self.logger = logger + self.level = level + self.buffer = '' + + def write(self, message): + if message != '\n': + self.logger.log(self.level, message.strip()) + + def flush(self): + pass + +sys.stdout = StreamToLogger(logger, logging.DEBUG) +#sys.stderr = StreamToLogger(logger, logging.ERROR) + +def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + +sys.excepthook = handle_exception + + +# Creates a main function to initialize the GUI +def main(): + logger.info("Creating new instance of HGCAL_Photo") + + filepath = os.path.dirname(os.path.abspath(__file__)) + logger.info("Current path is: %s" % (filepath)) + + node = socket.gethostname() + logger.info("Node is: %s" % node) + + try: + config_path = sys.argv[1] + except: + config_path = "{}/Configs/Wagon_cfg.yaml".format(filepath) + + logger.info('Board Config: %s' % config_path) + masterCfg = import_yaml(config_path) + + main_window = GUIWindow(masterCfg, filepath) + + +def import_yaml(config_path): + + return yaml.safe_load(open(config_path,"r")) + + + + +if __name__ == "__main__": + main() diff --git a/PhotoTakingGUI/PythonFiles/Data/DBSender.py b/PhotoTakingGUI/PythonFiles/Data/DBSender.py new file mode 100644 index 00000000..8e4b66ec --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Data/DBSender.py @@ -0,0 +1,252 @@ +import requests +import json +import socket +import base64 +import os +from PIL import Image +import logging + +from io import BytesIO +# from read_barcode import read_barcode + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Data.DBSender') + + +class DBSender(): + + def __init__(self, gui_cfg, main_path): + self.gui_cfg = gui_cfg + self.main_path = main_path + + # Predefined URL for the database + self.db_url = self.gui_cfg.getDBInfo("baseURL") + + # If True, use database + # If False, run in "offline" mode + self.use_database = self.gui_cfg.get_if_use_DB() + + + + # Since we will have the tester in a separate room, we need to do modify the http requests + # This proxy will be used to make http requests directly to cmslab3 via an ssh tunnel + def getProxies(self): + if (self.use_database): + if "umncmslab" in socket.gethostname(): + return None + + return {"http": "http://127.0.0.1:8080"} + + # If not using the database, then... + else: + pass + + def add_new_user_ID(self, user_ID, passwd): + + if (self.use_database): + + try: + r = requests.post('{}/add_tester2.py'.format(self.db_url), data= {'person_name':user_ID, 'password': passwd}) + except Exception as e: + logger.error("Unable to add the user to the database. Username: {}. Check to see if your password is correct.".format(user_ID)) + logger.debug(r.text) + + + # If not using the database, use this... + else: + pass + + def decode_label(self, full_id): + + if len(full_id) != 15: + logger.warning("Invalid label scanned") + label_info = None + else: + r = requests.post('{}/decode_label.py'.format(self.db_url), data={'label': full_id}) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `decode_label.py`. Check that the label library has been updated for the web API.") + logger.debug(r.text) + + temp = [] + + for i in range(begin, end): + temp.append(lines[i]) + + label_info = {'Major Type': temp[0], 'Subtype': temp[1], 'SN': temp[2]} + + return label_info + + # Returns an acceptable list of usernames from the database + def get_usernames(self): + if (self.use_database): + r = requests.get('{}/get_usernames.py'.format(self.db_url)) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `get_usernames.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + usernames = [] + + for i in range(begin, end): + temp = lines[i] + usernames.append(temp) + + return usernames + + # If not using database... + else: + + return ['User1', 'User2', 'User3'] + + # writes image to server disk and saves the name in the database + def add_board_image(self, full_id, image, view): + buffered = BytesIO() + image.save(buffered, format="JPEG") + encodedImage = base64.b64encode(buffered.getvalue()) + r = requests.post('{}/add_board_image~.py'.format(self.db_url), data={"full_id": full_id, "image": encodedImage, "view": view}) + + lines = r.text.split('\n') + + saved_image = False + for l in lines: + if 'File received successfully!' in l: + saved_image = True + logger.info('Image uploaded successfully!') + + if saved_image == False: + total_size = 0 + for dirpath, dirnames, filenames in os.walk("{}/PythonFiles/Images".format(self.main_path)): + for f in filenames: + fp = os.path.join(dirpath, f) + total_size += os.path.getsize(fp) + logger.error("Failed to save image, opting for local file storage...") + logger.debug(r.text) + logger.info("Image directory size is %s megabytes" % (total_size/1000000)) + if total_size < 5000000000: + image.save("{}/PythonFiles/Images/{}_{}.png".format(self.main_path, full_id, view)) + logger.info("Image saved to local disk successfully.") + else: + raise "Image Directory is too full, please upload and delete images." + + + def upload_local_board(self, path, full_id, view): + image = Image.open(path) + buffered = BytesIO() + image.save(buffered, format="JPEG") + encodedImage = base64.b64encode(buffered.getvalue()) + r = requests.post('{}/add_board_image~.py'.format(self.db_url), data={"full_id": full_id, "image": encodedImage, "view": view}) + + lines = r.text.split('\n') + + for l in lines: + if 'File received successfully!' in l: + logger.info('Image uploaded successfully, deleting local file...') + os.remove(path) + logger.info('File removed.') + + + + # Returns a dictionary of booleans + # Whether or not DB has passing results + def get_previous_test_results(self, full_id): + + r = requests.post('{}/get_previous_test_results.py'.format(self.db_url), data={"full_id": str(full_id)}) + + lines = r.text.split('\n') + + try: + begin1 = lines.index("Begin1") + 1 + end1 = lines.index("End1") + begin2 = lines.index("Begin2") + 1 + end2 = lines.index("End2") + begin3 = lines.index("Begin3") + 1 + end3 = lines.index("End3") + except: + logger.error("There was an issue with the web API script `get_previous_test_results.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + tests_run = [] + outcomes = [] + poss_tests = [] + + for i in range(begin1, end1): + tests_run.append(lines[i]) + for i in range(begin2, end2): + outcomes.append(lines[i]) + for i in range(begin3, end3): + poss_tests.append(lines[i]) + + tests_passed = [] + for i in range(len(tests_run)): + tests_passed.append([tests_run[i], outcomes[i]]) + + return tests_passed, poss_tests + + + # Posts a new board with passed in full id + # this is only called if the full id isn't recognized by is_new_board + def add_new_board(self, full, user_id, comments): + r = requests.post('{}/add_module2.py'.format(self.db_url), data={"full_id": str(full), 'manufacturer': "None"}) + r = requests.post('{}/board_checkin2.py'.format(self.db_url), data={"full_id": str(full), 'person_id': str(user_id), 'comments': str(comments)}) + + + try: + lines = r.text.split('\n') + + begin = lines.index("Begin") + 1 + end = lines.index("End") + + in_id = None + + for i in range(begin, end): + in_id = lines[i] + except: + in_id = None + + return in_id + + # checks if the board is in the database + def is_new_board(self, full): + r = requests.post('{}/is_new_board.py'.format(self.db_url), data={"full_id": str(full)}) + + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error("There was an issue with the web API script `is_new_board.py`. There is likely a syntax error in an associated web API script.") + logger.debug(r.text) + + + for i in range(begin, end): + + if "True" in lines[i]: + return True + elif "False" in lines[i]: + return False + + + def add_test_json(self, json_file, datafile_name): + load_file = open(json_file) + results = json.load(load_file) + load_file.close() + + datafile = open(datafile_name, "rb") + + attach_data = {'attach1': datafile} + + if (self.use_database): + r = requests.post('{}/add_test_json.py'.format(self.db_url), data = results, files = attach_data) + + else: + pass + diff --git a/PhotoTakingGUI/PythonFiles/Data/DataHolder.py b/PhotoTakingGUI/PythonFiles/Data/DataHolder.py new file mode 100644 index 00000000..48d642a8 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Data/DataHolder.py @@ -0,0 +1,283 @@ +############################################################################### +import json, logging, socket, PythonFiles, copy, os +import requests +from PythonFiles.Data.DBSender import DBSender +from PythonFiles.update_config import update_config + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Data.DataHolder') + +class DataHolder(): + + ################################################# + + # List of the variables being held by data holder + def __init__(self, gui_cfg, main_path): + + # Object for taking care of instantiation for different test types + self.gui_cfg = gui_cfg + self.main_path = main_path + + # Object that sends information to the database + self.data_sender = DBSender(gui_cfg, main_path) + + # dictionary to store data + self.data_dict = { + 'user_ID': "_", + 'test_stand': str(socket.gethostname()), + 'current_full_ID': "-1BAD", + 'comments': "_", + 'prev_results': None, + 'test_names': None, + 'checkin_id': None, + 'tests_run': [str(i + 1) for i in range(self.getNumTest())], + } + self.data_dict["inspection_completed"] = False + self.data_dict["inspection_pass"] = False + + # dictionary to hold images before sending them to the database + self.image_holder = {} + + self.image_data = [] + + self.label_info = None + + self.gui_cfg.setTestIndex(1) + + self.current_test_idx = self.gui_cfg.getTestIndex() + + self.photo_list = self.gui_cfg.getPhotoList() + + + ################################################# + + def num_photo(self): + return len(self.photo_list) + + def get_photo_list(self): + return self.photo_list + + def add_new_user_name(self, user_ID, passwd): + self.data_dict['user_ID'] = user_ID + + is_new_user_ID = True + + for item in self.get_all_users(): + if self.data_dict['user_ID'] == item: + is_new_user_ID = False + + if is_new_user_ID: + self.data_sender.add_new_user_ID(self.data_dict['user_ID'], passwd) + + + # checks if the board is in the database already + def check_if_new_board(self): + logger.info("Checking if full id is a new board...") + + full = self.get_full_ID() + is_new_board = self.data_sender.is_new_board(full) + + if is_new_board == True: + logger.info('Board is new, sending user to visual inspection station.') + + # otherwise it looks for previous test results + else: + logger.info("Board has been checked in, getting previous results") + prev_results, test_names = self.data_sender.get_previous_test_results(full) + if prev_results: + self.data_dict['test_names'] = test_names + self.data_dict['prev_results'] = prev_results + else: + self.data_dict['test_names'] = None + self.data_dict['prev_results'] = 'No tests have been run on this board.' + + def decode_label(self, full_id): + self.label_info = self.data_sender.decode_label(full_id) + + + ################################################# + + + def set_user_ID(self, user_ID): + + self.data_dict['user_ID'] = user_ID + logger.info("User ID set to %s" % user_ID) + + ################################################## + + # sets the full id and updates the config + def set_full_ID(self, full): + self.data_dict['current_full_ID'] = full + new_cfg = update_config(full) + self.gui_cfg = new_cfg + self.data_holder_new_test() + self.data_sender = DBSender(self.gui_cfg, self.main_path) + + try: + self.num_modules = int(full[5]) + 1 + except ValueError: + self.num_modules = 0 + + logger.info("Full ID set to %s" % full) + + def save_image(self): + for i in self.image_holder: + image = self.image_holder[i] + # Saves the image to a file + image.save(i) + + # sends the image to the database + def send_image(self): + for i in self.image_holder: + idx = int(i[-5]) + if idx == 0: + view = 'Top' + else: + view = 'Bottom' + logger.info('Uploading %s image...' % view) + self.data_sender.add_board_image(self.data_dict["current_full_ID"], self.image_holder[i], view) + + + def upload_local_boards(self, board_list): + for path, sn, view in board_list: + self.data_sender.upload_local_board(path, sn, view) + logger.info('Locally saved images uploaded sucessfully') + + ################################################## + + def get_full_ID(self): + return self.data_dict['current_full_ID'] + + ################################################# + + # Future method to send data to the database + def send_all_to_DB(self): + + person_ID = self.data_dict['user_ID'] + comments = self.data_dict['comments'] + full_id = self.get_full_ID() + + + for i in range(len(self.data_dict['tests_run'])): + temp = 0 + if self.data_lists['test_results'][i]: + temp = 1 + info_dict = {"full_id":full_id,"tester": person_ID, "test_type": self.tests_run[i], "successful": temp, "comments": comments} + with open("{}/JSONFiles/storage.json".format(PythonFiles.__path__[0]), "w") as outfile: + json.dump(info_dict, outfile) + self.data_sender.add_test_json("{}/JSONFiles/storage.json".format(PythonFiles.__path__[0])) + #message = "add_test_json;{'json_file': {}/JSONFiles/storage.json, ''}" + logger.info("All results sent to database.") + ################################################# + + # current method to send to the database + def send_to_DB(self): + test_names = "Visual Inspection" + test_type_id = 0 + + info_dict = {"full_id":self.get_full_ID(),"tester": self.data_dict['user_ID'], "test_type": test_type_id, "successful": self.data_dict["inspection_pass"], "comments": self.data_dict['comments']} + + with open("{}/JSONFiles/storage.json".format(PythonFiles.__path__[0]), "w") as outfile: + json.dump(info_dict, outfile) + + with open("{}/JSONFiles/data.json".format(PythonFiles.__path__[0]), "w") as outfile: + json.dump(self.inspection_data, outfile) + + self.data_sender.add_test_json("{}/JSONFiles/storage.json".format(PythonFiles.__path__[0]), "{}/JSONFiles/data.json".format(PythonFiles.__path__[0])) + logger.info("Test results sent to database.") + + ################################################# + + def get_all_users(self): + users_list = self.data_sender.get_usernames() + return users_list + + ################################################# + + # sends the visual inspection json comments to the database + def update_from_json_string(self): + + test_type = "Visual Inspection" + test_type_id = 0 + + passed = not any([x for x in self.inspection_data.values()][:-1]) + + self.data_dict['inspection_completed'] = True + self.data_dict['inspection_pass'] = int(passed) + self.data_dict['data'] = self.inspection_data + + self.send_to_DB() + + logger.info("Test results have been saved") + + ################################################ + + def add_inspection_to_comments(self): + if self.inspection_data['board_bent']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Board is chipped or bent." + if self.inspection_data['board_broken']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Wagon connnection pin is bent." + if self.inspection_data['component_missing']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Engine connection pin is bent." + if self.inspection_data['component_broken']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " There are visual scratches on the board." + if self.inspection_data['inspection_comments'] != "_": + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " User comments: " + self.inspection_data['inspection_comments'] + + ################################################ + + # Tracking the test index in another place and propagating to the config + def setTestIdx(self, test_idx): + + self.current_test_idx = test_idx + self.gui_cfg.setTestIndex(self.current_test_idx) + + def getNumTest(self): + return self.gui_cfg.getNumTest() + + def getTestNames(self): + return self.gui_cfg.getTestNames() + + ################################################ + + # Keeps the login information stored + # resets all other data + def data_holder_new_test(self): + + self.data_dict = { + 'user_ID': self.data_dict['user_ID'], + 'test_stand': str(socket.gethostname()), + 'current_full_ID': self.data_dict['current_full_ID'], + 'comments': "_", + 'is_new_board': False, + 'prev_results': None, + 'test_names': None, + 'checkin_id': None, + 'tests_run': [str(i + 1) for i in range(self.getNumTest())], + } + self.data_dict["inspection_completed"] = False + self.data_dict["inspection_pass"] = False + + + self.image_holder = {} + + logger.info("DataHolder Information has been reset for a new test.") + + self.gui_cfg.setTestIndex(1) + + self.current_test_idx = self.gui_cfg.getTestIndex() + + ################################################ + +################################################################################# + + diff --git a/PhotoTakingGUI/PythonFiles/Data/__init__.py b/PhotoTakingGUI/PythonFiles/Data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PhotoTakingGUI/PythonFiles/GUIConfig.py b/PhotoTakingGUI/PythonFiles/GUIConfig.py new file mode 100644 index 00000000..f19f572a --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/GUIConfig.py @@ -0,0 +1,60 @@ +import logging + +logger = logging.getLogger('HGCAL_Photo') +# Class to handle creation of different types of GUIs based on which board we want to test +# This class will hold all of the frame information and order them accordingly + +class GUIConfig(): + + # Loads in a config file with board type name + # Information about board tests and database are stored within the config + def __init__(self, board_cfg): + self.board_cfg = board_cfg + self.current_idx = 1 + + self.configure() + + # Return true if GUI should use the database + def get_if_use_DB(self): + return self.board_cfg['DBInfo']['use_database'] + + + # Create the GUI instance based off testing information + def configure(self): + + # Possibly do something special here if need be + + logger.info("Instance of {} GUI created.".format(self.getGUIType())) + + # Get number of tests to define order of scenes and sidebar + def getNumTest(self): + return len(self.board_cfg["Test"]) + + # Get number of tests to define order of scenes and sidebar + def getTests(self): + return self.board_cfg["Test"] + + # Get database info for getting and posting test results + def getDBInfo(self, key=None): + if key is None: + return self.board_cfg["DBInfo"] + else: + return self.board_cfg["DBInfo"][key] + + def getGUIType(self): + return self.board_cfg["GUIType"] + + def setTestIndex(self, idx): + self.current_idx = idx + + def getTestIndex(self): + return self.current_idx + + + def getTestNames(self): + return [test["name"] for test in self.board_cfg["Test"]] + + # Returns a dictionary of each photo required + def getPhotoList(self): + return self.board_cfg['Photo'] + diff --git a/PhotoTakingGUI/PythonFiles/GUIWindow.py b/PhotoTakingGUI/PythonFiles/GUIWindow.py new file mode 100644 index 00000000..ba10d286 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/GUIWindow.py @@ -0,0 +1,477 @@ +################################################################################# + +# Importing all neccessary modules +from pickle import NONE +import tkinter as tk +from tkinter import ttk +from turtle import bgcolor +from PythonFiles.GUIConfig import GUIConfig +from PythonFiles.Data.DataHolder import DataHolder +from PythonFiles.Scenes.LoginScene import LoginScene +from PythonFiles.Scenes.ScanScene import ScanScene +from PythonFiles.Scenes.SplashScene import SplashScene +from PythonFiles.Scenes.TestSummaryScene import TestSummaryScene +from PythonFiles.Scenes.AddUserScene import AddUserScene +from PythonFiles.Scenes.PostScanScene import PostScanScene +from PythonFiles.Scenes.LocalUploadScene import LocalUploadScene +from PythonFiles.update_config import update_config +from PythonFiles.Scenes.CameraScene import CameraScene +import logging +import os +import PythonFiles + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.GUIWindow') + +################################################################################# + + +# Create a class for creating the basic GUI Window to be called by the main function to +# instantiate the actual object +class GUIWindow(): + + ################################################# + + def __init__(self, board_cfg, main_path): + # Create the window named "master_window" + # global makes master_window global and therefore accessible outside the function + global master_window + master_window = tk.Tk() + master_window.report_callback_exception = self.log_callback_exception + + master_window.title("Photo Taking Window") + + # Creates the size of the window and disables resizing + master_window.geometry("1350x850+25+100") + master_window.pack_propagate(1) + + #resizing master_frame, keeping sidebar same width + master_window.grid_columnconfigure(0, weight=0) # Make the sidebar resizable + master_window.grid_columnconfigure(1, weight=1) # Make the master frame resizable + master_window.grid_rowconfigure(0, weight=1) + + # Variables necessary for the help popup + self.all_text = "No help available for this scene." + self.label_text = tk.StringVar() + + self.main_path = main_path + + # Following line prevents the window from being resizable + # master_window.resizable(0,0) + + # Removes the tkinter logo from the window + # master_window.wm_attributes('-toolwindow', 'True') + + # Creates and packs a frame that exists on top of the master_frame + master_frame = tk.Frame(master_window, width = 1350, height = 850) + master_frame.grid(column = 0, row = 0, columnspan = 4, sticky="nsew") + + self.master_frame = master_frame + + # Object for taking care of instantiation of different test types + self.gui_cfg = GUIConfig(board_cfg) + + # Creates the "Storage System" for the data during testing + self.data_holder = DataHolder(self.gui_cfg, self.main_path) + + ################################################# + # Creates all the different frames in layers # + ################################################# + + # At top so it can be referenced by other frames' code... Order of creation matters + self.test_summary_frame = TestSummaryScene(self, master_frame, self.data_holder) + self.test_summary_frame.grid(row=0, column=0, sticky='nsew') + + self.post_scan_frame = PostScanScene(self, master_frame, self.data_holder) + self.post_scan_frame.grid(row=0, column=0, sticky='nsew') + + self.local_upload_frame = LocalUploadScene(self, master_frame, self.data_holder) + self.local_upload_frame.grid(row=0, column=0, sticky='nsew') + + self.login_frame = LoginScene(self, master_frame, self.data_holder) + self.login_frame.grid(row=0, column=0, sticky='nsew') + + self.scan_frame = ScanScene(self, master_frame, self.data_holder) + self.scan_frame.grid(row=0, column=0, sticky='nsew') + + self.add_user_frame = AddUserScene(self, master_frame, self.data_holder) + self.add_user_frame.grid(row=0,column=0, sticky='nsew') + + # Near bottom so it can reference other frames with its code + self.splash_frame = SplashScene(self, master_frame) + self.splash_frame.grid(row=0, column=0, sticky='nsew') + + self.camera_frame = CameraScene(self, master_frame, self.data_holder, 0) + self.camera_frame.grid(row=0, column=0, sticky='nsew') + + logger.info("All frames have been created.") + + # indices to denote photo number and camera number to know when to finish + self.camera_index = 0 + self.photo_index = 0 + # two bools, one indicates that a photo is to be retaken + # the other indicates whether the photo has been retaken or not + self.retake = False + self.retaken = False + + ################################################# + # End Frame Creation # + ################################################# + + # Tells the master window that its exit window button is being given a new function + master_window.protocol('WM_DELETE_WINDOW', self.exit_function) + + # Sets the current frame to the splash frame + self.set_frame_splash_frame() + + master_frame.after(500, self.set_frame_login_frame) + + master_window.mainloop() + + def log_callback_exception(self, exc_type, exc_value, exc_traceback): + logger.error("Exception in Tkinter callback", exc_info=(exc_type, exc_value, exc_traceback)) + + + ################################################# + + # is called after a board is entered + # sets the config to either Wagon or Engine depending on the SN entered + def update_config(self): + full = self.data_holder.get_full_ID() + new_cfg = update_config(full) + self.gui_cfg = new_cfg + + ################################################# + + # function for retaking the photo at the end, takes in index of desired photo + def retake_photo(self, photo_index): + logger.info("Retaking photo {}".format(photo_index)) + # need to decrease the index by 1 since next frame will increment + self.camera_index = photo_index-1 + self.photo_index = photo_index-1 + + # two bools, one indicates that a photo is to be retaken + # the other indicates whether the photo has been retaken or not + self.retake = True + self.retaken = False + self.next_frame_camera_frame() + + + ################################################# + + def set_frame_login_frame(self): + logger.info('Setting frame to login_frame') + + # resets retake variables if the login screen is opened + self.retake = False + self.retaken = False + + self.login_frame.update_frame(self) + self.set_frame(self.login_frame) + + ################################################# + + def first_frame_camera_frame(self): + + # Trick for bypassing the increment in "next_frame_camera_frame" + self.camera_index = -1 + self.photo_index = -1 + + self.next_frame_camera_frame() + + + def next_frame_camera_frame(self): + self.camera_index += 1 + self.photo_index += 1 + photo_list = self.data_holder.get_photo_list() + + # goes back to summary after photo has been retaken + if self.retake == True and self.retaken == True: + self.set_frame_test_summary() + else: + # number of photos is determined in the data holder + if (self.camera_index < len(photo_list)): + self.set_frame_camera_frame(self.camera_index) + else: + self.set_frame_test_summary() + + + def set_frame_camera_frame(self, index): + logger.info("Going to camera frame #{}".format(index)) + self.camera_frame.set_text(index) + self.camera_frame.update_preview() + self.set_frame(self.camera_frame) + + ################################################# + + def set_frame_scan_frame(self): + logger.info("Setting frame to scan_frame") + self.camera_index = 0 + self.photo_index = 0 + self.retake = False + self.retaken = False + self.scan_frame.is_current_scene = True + self.set_frame(self.scan_frame) + self.scan_frame.scan_QR_code(master_window) + + ################################################# + + def set_frame_postscan(self): + logger.info("Setting frame to postscan_frame") + self.post_scan_frame.update_frame() + self.set_frame(self.post_scan_frame) + + ################################################# + + def set_frame_splash_frame(self): + logger.info("Setting frame to splash_frame") + self.set_frame(self.splash_frame) + + ################################################# + + def set_frame_upload_local_photos(self): + logger.info("Setting frame to local_photo_upload_frame") + self.set_frame(self.local_upload_frame) + self.local_upload_frame.get_local_boards(self) + + ################################################# + + def set_frame_test_summary(self): + logger.info("Setting frame to test_summary_frame") + self.test_summary_frame.update_frame() + self.set_frame(self.test_summary_frame) + + def set_frame_add_user_frame(self): + logger.info("Setting frame to add_user_frame") + self.set_frame(self.add_user_frame) + + ################################################# + + # Called to change the frame to the argument _frame + def set_frame(self, _frame): + + ############################################################################# + # The Following Code Determines What Buttons Are Visible On The Side Bar # + ############################################################################# + + #Binding return button to next frame + try: + bind_func = _frame.get_submit_action() + _frame.bind_all("", lambda event: bind_func(_frame.get_parent())) + except: + logger.warning("No bind function for " + str(_frame)) + + try: + bind_func_2 = _frame.snapshot + _frame.bind_all("", lambda event: bind_func_2()) + except: + pass + + + # Hides the submit button on scan frame until an entry is given to the computer + if (_frame is not self.scan_frame): + self.scan_frame.is_current_scene = False + self.scan_frame.hide_submit_button() + + self.set_help_text(_frame) + ############################################################################# + # End Button Visibility Code # + ############################################################################# + + # Raises the passed in frame to be the current frame + _frame.tkraise() + + ################################################# + + + # New function for clicking on the exit button + def exit_function(self): + + # Creates a popup to confirm whether or not to exit out of the window + self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') + self.popup.title("Exit Window") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + + # Creates frame in the new window + frm_popup = tk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = tk.Label( + frm_popup, + text = "Are you sure you would like to exit?", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) + + # Creates yes and no buttons for exiting + btn_yes = tk.Button( + frm_popup, + width = 12, + height = 2, + text = "Yes", + relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.destroy_function() + ) + btn_yes.grid(column = 0, row = 1) + + btn_no = tk.Button( + frm_popup, + width = 12, + height = 2, + text = "No", + relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.destroy_popup() + ) + btn_no.grid(column = 1, row = 1) + + + + + ################################################# + + # Called when the no button is pressed to destroy popup and return you to the main window + def destroy_popup(self): + try: + self.popup.destroy() + except: + logger.error("GUIWindow: Unable to close the popup") + ################################################# + + def remove_all_widgets(self): + self.inspection_frame.remove_widgets(self) + self.add_user_frame.remove_widgets(self) + + ################################################# + + + def help_popup(self, current_window): + + logger.info("The user has requested a help window") + logger.info("Opening a help menu for {}".format(type(current_window))) + + # Creates a popup to confirm whether or not to exit out of the window + self.popup = tk.Toplevel() + self.popup.title("Help Window") + self.popup.geometry("650x650+500+300") + + self.mycanvas = tk.Canvas(self.popup, background="#808080", width=630, height =650) + self.viewingFrame = tk.Frame(self.mycanvas, width = 200, height = 200) + self.scroller = ttk.Scrollbar(self.popup, orient="vertical", command=self.mycanvas.yview) + self.mycanvas.configure(yscrollcommand=self.scroller.set) + + + + self.canvas_window = self.mycanvas.create_window((4,4), window=self.viewingFrame, anchor='nw', tags="self.viewingFrame") + + + self.viewingFrame.bind("", self.onFrameConfigure) + self.mycanvas.bind("", self.onCanvasConfigure) + + self.viewingFrame.bind('', self.onEnter) + self.viewingFrame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + + self.set_help_text(current_window) + + # Creates label in the frame + lbl_popup = tk.Label( + self.viewingFrame, + textvariable = self.label_text, + font = ('Arial', 11) + ) + lbl_popup.grid(column = 0, row = 0, pady = 5, padx = 50) + + + self.mycanvas.pack(side="right") + self.scroller.pack(side="left", fill="both", expand=True) + + + + + ############################################# + + def set_help_text(self, current_window): + + # Help text from file + file = open("{}/HGCAL_Help/{}_help.txt".format(PythonFiles.__path__[0], type(current_window).__name__)) + self.all_text = file.read() + + self.label_text.set(self.all_text) + + + + ################################################# + + def onFrameConfigure(self, event): + '''Reset the scroll region to encompass the inner frame''' + self.mycanvas.configure(scrollregion=self.mycanvas.bbox("all")) #whenever the size of the frame changes, alter the scroll region respectively. + + def onCanvasConfigure(self, event): + '''Reset the canvas window to encompass inner frame when required''' + canvas_width = event.width + self.mycanvas.itemconfig(self, width = canvas_width) #whenever the size of the canvas changes alter the window region respectively. + + + ################################################# + + + def onMouseWheel(self, event): # cross platform scroll wheel event + if event.num == 4: + self.mycanvas.yview_scroll( -1, "units" ) + elif event.num == 5: + self.mycanvas.yview_scroll( 1, "units" ) + + def onEnter(self, event): # bind wheel events when the cursor enters the control + self.mycanvas.bind_all("", self.onMouseWheel) + self.mycanvas.bind_all("", self.onMouseWheel) + + def onLeave(self, event): # unbind wheel events when the cursorl leaves the control + self.mycanvas.unbind_all("") + self.mycanvas.unbind_all("") + + + + ################################################# + + # Called when the yes button is pressed to destroy both windows + def destroy_function(self, event=None): + try: + self.camera_frame.remove_widgets(self) + + logger.info("Exiting the GUI.") + + master_window.update() + self.popup.update() + + self.scan_frame.kill_processes() + + # Destroys the popup and master window + self.popup.destroy() + self.popup.quit() + + master_window.destroy() + master_window.quit() + + + logging.info("The application has exited successfully.") + except Exception as e: + logging.exception(e) + logging.error("The application has failed to close.") + + exit() + + + + + + ################################################ + + +################################################################################# diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/AddUserScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/AddUserScene_help.txt new file mode 100644 index 00000000..a306b4cc --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/AddUserScene_help.txt @@ -0,0 +1,40 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Add User Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Add User Scene can only be accessed via the Login Scene's "Add User" + button. This scene is responsible for adding another user's name into the + database so that the Login Scene's next call to the database will have the + new user's information. + + To add a user, type the user's name into the top text field. Be careful, + this is a case-sensitive field. Then, type in the administrative password + below. This is also a case-sensitive field. After the information has been + entered, press the "Submit" button. This will bring you back to the Login + Scene. + + + Note: If you add a user name and it does not show up on the Login Scene, + try adding the user again. There is a chance that you have entered + the password incorrectly. If you are sure your credentials are + correct, there might be an issue setting/accessing the data stored + in the database. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/CameraScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/CameraScene_help.txt new file mode 100644 index 00000000..c5fda873 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/CameraScene_help.txt @@ -0,0 +1,44 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Camera Scene + + Date Updated: 7/17/23 + + +---------------------------------------------------------------------------------------- + + + The Camera Scene is a scene unique to the Photo Taking GUI. It is + responsible for showing the preview screen of the camera attached to + the system. The preview takes up a majority of the screen. + + If there seems to be issues with this scene, make sure that the camera + is attached to the system and is enabled. This may require entering + the Raspberry Pi Config settings to re-enable the camera because it is + not automatically enabled. + + This scene has three buttons in the top bar: "Snapshot", "Help", and + "Submit". The "Snapshot" button takes a picture of the current preview + that is shown and saves it to the device temporarily until it can be + uploaded to the database. The "Help" button brings you to this manual. + The "Submit" button will send you to the next scene where you can + confirm that the picture you took looks correct. + + Note: In the current state of the application, you will stay on the + Camera Scene until you press the submit button, even if you take a + picture via the "Snapshot" button. This may change in the versions to + come. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/LocalUploadScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/LocalUploadScene_help.txt new file mode 100644 index 00000000..6face833 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/LocalUploadScene_help.txt @@ -0,0 +1,24 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Local Upload Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + This scene is used to upload photos that are saved locally to the raspberry + pi's disk in the case of an upload error. + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/LoginScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/LoginScene_help.txt new file mode 100644 index 00000000..27559a93 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/LoginScene_help.txt @@ -0,0 +1,36 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Login Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Login Scene is displayed directly after the Splash Scene as well as + via the "logout" buttons on many scenes. The purpose of this scene is to + have the user log into the system using the credentials stored on the + web database. When this scene is shown, there is a call to the web + database to grab the login options. + + Users must select an option from the list of users provided in the drop- + down box. After selecting the desired user's name, the submit button will + bring you to the next scene, which is the Scan Scene. + + If there is not an option for a specific user, you can select + the "Add User" button to bring you to the Add User Scene. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/PostScanScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/PostScanScene_help.txt new file mode 100644 index 00000000..f6d1ab51 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/PostScanScene_help.txt @@ -0,0 +1,25 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Summary Scene + + Date Updated: 9/26/23 + + +---------------------------------------------------------------------------------------- + + The Scan Summary Scene gives information about the board: whether it has + been checked in, whether it has had tests run on it, etc. Once you are sure + the information displayed is correct, click Proceed. + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/ScanScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/ScanScene_help.txt new file mode 100644 index 00000000..34384fc4 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/ScanScene_help.txt @@ -0,0 +1,62 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Scan Scene is displayed after the Login Scene. It also marks the + beginning of testing a new board (either wagon or engine). This scene + is responsible for gathering the serial identification number, which + is often displayed directly on the board via a QR code. + + The submit button (which leads to the next scene) is grayed out until + the serial id box has been filled. This box can be filled in two + different ways. + + The most efficient way to fill the text field is to + scan the box using an EventListener scanner attached to the computer. + The scanner should be attached to the computer that is actively + running the GUI. After successfully scanning a QR code, the scanner + should beep, and the text should be filled into the field. Notice that + the text field will become grayed out after information has been + scanned into it. After scanning, if you would like to change the + serial number entered, you will need to press the "Rescan" button to + reactivate the scanner. + + The scanner runs on a completely separate process from the GUI, which + can cause issues with sections of code not completing correctly. In + some cases, the scanner may be stuck in a constant state of searching + for QR code information. If this is the case, be sure to press the + button that says "Report Bug" and report the issue. + + If the physical scanner is not available, you are also able to manually + enter the serial identification into the box. Note that this will not + require pressing the "Rescan" button nor will it gray out the text field. + + After the entry of the serial ID matches the board that you would like + to test, select the "Submit" button in order to move onto the next scene. + Before moving to the next scene, the GUI will request more information + about your board by sending the serial ID to the database. If the serial + ID is recognized as invalid, errors may occur within the GUI. + + + Note: If the serial ID is recognized and the board has been tested + before, the GUI may skip some of the previously passed tests + in order to save time. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/SplashScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/SplashScene_help.txt new file mode 100644 index 00000000..6a3c91a7 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/SplashScene_help.txt @@ -0,0 +1,36 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Splash Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Splash Scene is displayed when the graphical user interface (GUI) is first + ran. The information provides credit to the students largely responsible + for the creation of this interface. The three students listed, along with + their home institutions are as follows: + + - Bryan Crossman (University of Minnesota) + - Andrew Kirzeder (Bethel University) + - Garrett Schindler (Bethel University) + + This scene should only be displayed for a short period of time before + moving on to the next scene. The next scene should be the Login Scene. + No buttons are required to be pressed on this screen. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt new file mode 100644 index 00000000..7b454595 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt @@ -0,0 +1,53 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test In Progress Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Test In Progress Scene is displayed while you are running a test on + the testing machine. When working correctly, the test machine will send + dialog back to the GUI describing the current testing process. This type + of logging is run through a connection queue. + + If the GUI does not receive an initial response from the test stand after + 10 seconds, the GUI will throw a timeout error (popup window). This error + message will say "TestInProgressScene: Process timed out after 10 seconds". + Upon pressing the "OK" button, you will be brought back to the Login Scene + where you can restart the process. + + If the previous error persists for more than one occurance, you will need + to check the status of the REPServer that has been launched on the test + stand. If the server is not running on the test stand, the GUI will never + be able to receive a response, therefore always throwing this error. + + The GUI cannot be closed while the Test In Progress Scene is on the + screen. If you gry to quit the application by clicking the "X" in the top, + right corner, a popup window will show saying that "You cannot quit the + application during a test!". This is to prevent corruption of test data. + + There is a "Stop" button in the middle of the application that tries to + stop the test. If able to successfully stop the test, the GUI will go to + the next available test. If there are no more available tests, then + the next scene will be the Test Summary Scene. + + If the test has been completed successfully and the results have been + received by the GUI, then the GUI will automatically go to the next scene. + The next scene will either be the next Test Scene (if available) or will + be the Test Summary Scene (if the last test has been finished). + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestScene_help.txt new file mode 100644 index 00000000..35f67ad9 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestScene_help.txt @@ -0,0 +1,45 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Test Scene is a very important scene, as it is the last scene you will + see before a test is run. It is important to ensure that all of the + information shown on this screen is correct before proceeding. + + There are three boxes of information displayed on this screen: "Tester", + "Serial Number", and "Current Test". The first should display your name, + the second should display the serial ID of the board that you would like + to test (make sure that it is plugged into the test stand at this point), + and the third should display the name of the test that is going to be run. + + If any of this information is incorrect, then you will need to proceed to + the previous scenes in order to change this information. If your username + is incorrect, you will need to press the "Logout" button to return to the + Login Scene. If the board serial number is incorrect or if you would like + to switch which board you are testing, you will need to press the "Change + Board" button to go back to the Scan Scene. + + If all of the information looks correct, pressing the "Confirm" button + will send you to the Test In Progress Scene. This button will also tell + the testing machine to begin the test (which test is specified by the name + listed under the "Current Test"). + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestSummaryScene_help.txt b/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestSummaryScene_help.txt new file mode 100644 index 00000000..04cf6d44 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/HGCAL_Help/TestSummaryScene_help.txt @@ -0,0 +1,50 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test Summary Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Test Summary Scene is responsible for showing all of the results that + the GUI has gathered. The information is stored in a DataHolder object + while it is gathered throughout the process. The Test Summary Scene + displays the attributes of the DataHolder object to the user to ensure + that all of the information has been stored correctly. + + The top of the screen should display the tester's name as well as the + serial identification number for the board that is being tested. + + Each of the tests that are listed for the board are displayed below. The + GUI keeps track of whether or not certain tests have been completed. + The information displayed for each test is the "Test Name", "Test Status", + and "Pass/Fail". These display which type of test, whether or not it has + been run, and whether or not it has passed the test. Notice that a pass + is signified by a green checkmark and a fail is signified by a red x. + + To the right of the displayed information are two buttons; "More Info" and + "Retest". The "More Info" button will open a popup window with JSON + information that the tester has sent back to the GUI. If you want to know + more about the specific test results, press this button. The "Retest" + button will send you back to the Test Scene for the designated test. + Upon finishing this retest, the GUI will send you back to the Test + Summary Scene. + + Upon reaching the Test Summary Scene, the DataHolder object is uploaded to + the database. When you complete a retest for a specified board, the + information stored in the database for that board will be updated. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PhotoTakingGUI/PythonFiles/Images/Bethel_Logo.png b/PhotoTakingGUI/PythonFiles/Images/Bethel_Logo.png new file mode 100644 index 00000000..4d6bac22 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/Bethel_Logo.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/EngineExample.png b/PhotoTakingGUI/PythonFiles/Images/EngineExample.png new file mode 100644 index 00000000..a6e5ce9c Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/EngineExample.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/EnginePhoto.png b/PhotoTakingGUI/PythonFiles/Images/EnginePhoto.png new file mode 100644 index 00000000..af544ad6 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/EnginePhoto.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/GreenCheckMark.png b/PhotoTakingGUI/PythonFiles/Images/GreenCheckMark.png new file mode 100644 index 00000000..ba7aaeb9 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/GreenCheckMark.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/QRimage.png b/PhotoTakingGUI/PythonFiles/Images/QRimage.png new file mode 100644 index 00000000..bf953464 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/QRimage.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/RedX.png b/PhotoTakingGUI/PythonFiles/Images/RedX.png new file mode 100644 index 00000000..3e7a2410 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/RedX.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/UMNLOGO.png b/PhotoTakingGUI/PythonFiles/Images/UMNLOGO.png new file mode 100644 index 00000000..57226e37 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/UMNLOGO.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/UMN_Logo.png b/PhotoTakingGUI/PythonFiles/Images/UMN_Logo.png new file mode 100644 index 00000000..028e5b32 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/UMN_Logo.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/WagonExample.png b/PhotoTakingGUI/PythonFiles/Images/WagonExample.png new file mode 100644 index 00000000..393cfbd8 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/WagonExample.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/captured_image0.png b/PhotoTakingGUI/PythonFiles/Images/captured_image0.png new file mode 100644 index 00000000..033c1880 Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/captured_image0.png differ diff --git a/PhotoTakingGUI/PythonFiles/Images/captured_image1.png b/PhotoTakingGUI/PythonFiles/Images/captured_image1.png new file mode 100644 index 00000000..a26978bc Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Images/captured_image1.png differ diff --git a/PhotoTakingGUI/PythonFiles/Scanner/Makefile b/PhotoTakingGUI/PythonFiles/Scanner/Makefile new file mode 100644 index 00000000..d9158415 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scanner/Makefile @@ -0,0 +1,30 @@ +# +# ©2015 Symbol Technologies LLC. All rights reserved. +# + +CC = g++ +CXXFLAGS = -Wall -g -I/usr/include/zebra-scanner -I./include/ +LFLAGS = -L/usr/lib/zebra-scanner/corescanner -I./include/ +LIBS = -lcs-client -lcs-common +SRCS = ./src/main.cpp ./src/EventListener.cpp +OBJS = $(SRCS:.cpp=.o) +MAIN = ./bin/runScanner + +.PHONY: depend clean + +all: $(MAIN) + +$(MAIN): $(OBJS) + $(CC) $(CXXFLAGS) $(INCLUDES) -o $(MAIN) $(OBJS) $(LFLAGS) $(LIBS) + +.c.o: + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +.cpp.o: + $(CC) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +clean: + $(RM) ./src/*.o *~ $(MAIN) + +depend: $(SRC) + makedepend $(INCLUDES) $^ diff --git a/PhotoTakingGUI/PythonFiles/Scanner/__init__.py b/PhotoTakingGUI/PythonFiles/Scanner/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PhotoTakingGUI/PythonFiles/Scanner/bin/__init__.py b/PhotoTakingGUI/PythonFiles/Scanner/bin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PhotoTakingGUI/PythonFiles/Scanner/bin/runScanner b/PhotoTakingGUI/PythonFiles/Scanner/bin/runScanner new file mode 100755 index 00000000..ff1a850d Binary files /dev/null and b/PhotoTakingGUI/PythonFiles/Scanner/bin/runScanner differ diff --git a/PhotoTakingGUI/PythonFiles/Scanner/include/EventListener.h b/PhotoTakingGUI/PythonFiles/Scanner/include/EventListener.h new file mode 100644 index 00000000..bd753b39 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scanner/include/EventListener.h @@ -0,0 +1,48 @@ +#ifndef EVENTLISTENER_H_ +#define EVENTLISTENER_H_ + +#include "zebra-scanner/CsIEventListenerXml.h" +#include "zebra-scanner/CsUserDefs.h" +#include "zebra-scanner/CsBarcodeTypes.h" +#include "zebra-scanner/Cslibcorescanner_xml.h" +#include + +class EventListener : public IEventListenerXml +{ + public: + explicit EventListener(); + virtual ~EventListener(); + + virtual void OnVideoEvent( short eventType, + int size, char* sfvideoData, + int dataLength, + std::string& pScannerData + ); + + virtual void OnImageEvent( short eventType, + int size, short imageFormat, + char* sfimageData, + int dataLength, + std::string& pScannerData + ); + + virtual void OnBinaryDataEvent( short eventType, int size, short dataFormat, + unsigned char* sfBinaryData, std::string& pScannerData); + virtual void OnBarcodeEvent( short eventType, std::string& pscanData ); + virtual void OnPNPEvent( short eventType, std::string ppnpData ); + virtual void OnCommandResponseEvent( short status, std::string& prspData ); + virtual void OnScannerNotification( short notificationType, std::string& pScannerData); + virtual void OnIOEvent( short type, unsigned char data ); + virtual void OnScanRMDEvent( short eventType, std::string& prmdData ); + virtual void OnDisconnect(); + + void Open(); + void GetScanners(); + void Close(); + bool GetToClose(); + + private: + bool toClose_; +}; + +#endif diff --git a/PhotoTakingGUI/PythonFiles/Scanner/include/main.h b/PhotoTakingGUI/PythonFiles/Scanner/include/main.h new file mode 100644 index 00000000..8159206e --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scanner/include/main.h @@ -0,0 +1,4 @@ +#ifndef MAIN_H_ +#define MAIN_H_ + +#endif diff --git a/PhotoTakingGUI/PythonFiles/Scanner/python/__init__.py b/PhotoTakingGUI/PythonFiles/Scanner/python/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PhotoTakingGUI/PythonFiles/Scanner/python/get_barcodes.py b/PhotoTakingGUI/PythonFiles/Scanner/python/get_barcodes.py new file mode 100644 index 00000000..f3b7ce54 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scanner/python/get_barcodes.py @@ -0,0 +1,64 @@ +import subprocess +import time +import signal +import ctypes +import os +#import PythonFiles +libc = ctypes.CDLL("libc.so.6") + +from multiprocessing import Process, Manager, Pipe +import logging + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scanner.python.get_barcodes') + +# these functions are run individually in ScanScene + +# decodes hexadecimal into the serial number +def decode(hex_str): + serial = "" + for h in hex_str.split(" "): + serial += bytes.fromhex(h).decode("ASCII") + + return serial + +# parses the hexadecimal value grabbed +def parse_xml(inXML): + if "<" not in inXML: + return + else: + splitting = inXML.split("datalabel") + return decode(splitting[1][1:-2]) + +def set_pdeathsig(sig = signal.SIGTERM): + def callable(): + return libc.prctl(1, sig) + return callable + +def scan(path): + proc = subprocess.Popen('{}/PythonFiles/Scanner/bin/runScanner'.format(path), stdout=subprocess.PIPE, preexec_fn=set_pdeathsig(signal.SIGTERM)) + logger.info("Starting scanner") + return proc + +def listen(serial, proc): + for line in proc.stdout: + if line is not None: + serial.append(line.strip().decode('utf-8')) + return + + +def run_scanner(): + manager = Manager() + serial = manager.list() + + proc = scan() + listener = Process(target=listen, args=(serial, proc)) + + listener.start() + + # holds until something is scanned + listener.join() + + logger.info('Scanner: %s' % parse_xml(serial[0])) + +if __name__=="__main__": + run_scanner() diff --git a/PhotoTakingGUI/PythonFiles/Scanner/src/EventListener.cpp b/PhotoTakingGUI/PythonFiles/Scanner/src/EventListener.cpp new file mode 100644 index 00000000..29172248 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scanner/src/EventListener.cpp @@ -0,0 +1,107 @@ +#include "../include/EventListener.h" +#include + +using namespace std; + +EventListener::EventListener() +{ + this->toClose_ = false; +} + +EventListener::~EventListener() +{ + Close(); +} + +void EventListener::Open() +{ + StatusID status; + + ::Open(this, SCANNER_TYPE_ALL, &status); + + std::string inXML = + "11"; + std::string outXML = ""; + + ::ExecCommand(CMD_REGISTER_FOR_EVENTS, inXML, outXML, &status); + +} + +void EventListener::Close() +{ + StatusID status; + ::Close(0, &status); +} + +void EventListener::GetScanners() +{ + unsigned short number; + std::vector scannerIDs; + std::string outXML; + StatusID status; + + std::cout << "Getting total number of scanners connected..." << std::endl; + + ::GetScanners(&number, &scannerIDs, outXML, &status); + + std::cout << "Total number of scanners connected: " << number << std::endl; + std::cout << outXML << std::endl; +} + +void EventListener::OnBarcodeEvent( short eventType, std::string& pscanData ) +{ + //std::cout << "Barcode Detected" << std::endl; + //std::cout << "Out XML:" << std::endl; + std::cout << pscanData << std::endl; + toClose_ = true; + std::cout << toClose_ << std::endl; +} + +bool EventListener::GetToClose() +{ + return toClose_; +} + +void EventListener::OnDisconnect() +{ + cout << "OnDisconnect" << endl; +} + +void EventListener::OnImageEvent( short eventType, int size, short imageFormat, + char* sfimageData, int dataLength, std::string& pScannerData ) +{ + cout << "OnImageEvent" << endl; +} +void EventListener::OnVideoEvent( short eventType, int size, char* sfvideoData, int + dataLength, std::string& pScannerData ) +{ + cout << "OnVideoEvent" << endl; +} +void EventListener::OnPNPEvent( short eventType, std::string ppnpData ) +{ +} +void EventListener::OnCommandResponseEvent( short status, std::string& prspData ) +{ + cout << endl << "Scanner data: " << prspData << endl; + cout << "OnCommandResponseEvent" << endl; + cout << prspData << endl; +} +void EventListener::OnScannerNotification( short notificationType, std::string& + pScannerData ) +{ + cout << endl << "Scanner event data: " << pScannerData << endl; + cout << "OnScannerNotification" << endl; +} +void EventListener::OnIOEvent( short type, unsigned char data ) +{ + cout << "OnIOEvent" << endl; +} +void EventListener::OnScanRMDEvent( short eventType, std::string& prmdData ) +{ + cout << "OnScanRMDEvent" << endl; + cout << "Out XML " << endl; + cout << prmdData << endl; +} +void EventListener::OnBinaryDataEvent( short eventType, int size, short dataFormat, unsigned char* sfBinaryData, std::string& pScannerData) +{ +} diff --git a/PhotoTakingGUI/PythonFiles/Scanner/src/main.cpp b/PhotoTakingGUI/PythonFiles/Scanner/src/main.cpp new file mode 100644 index 00000000..6a52ca4a --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scanner/src/main.cpp @@ -0,0 +1,28 @@ +#include "../include/EventListener.h" +#include "../include/main.h" + +#include +#include + +int main(void) +{ + + EventListener el; + el.Open(); + + //el.GetScanners(); + + //std::cout << "Checking if you have a scanner plugged in..." << std::endl; + + //std::cout << "Please scan barcode or type 'q' to quit: " << std::endl; + + //std::cout << "33 32 30 35 39 39 39 39 39 39 30 30 30 30 31" << std::endl; + + while(!el.GetToClose()) + { + //std::cout << "Still running..." << std::endl; + //usleep(1000000); + } + + return 0; +} diff --git a/PhotoTakingGUI/PythonFiles/Scenes/AddUserScene.py b/PhotoTakingGUI/PythonFiles/Scenes/AddUserScene.py new file mode 100644 index 00000000..7c5b3359 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/AddUserScene.py @@ -0,0 +1,235 @@ + +################################################################################# + +# importing necessary modules +import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os + +################################################################################# + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scene.AddUserScene') + +# Creates a class that is called by the GUIWindow. +# GUIWindow instantiates an object called add_user_scene. +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. + +class AddUserScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + super().__init__(master_frame, width=850, height=500) + self.data_holder = data_holder + self.update_frame(parent) + self.create_style(parent) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + # Creating the title for the window + lbl_title = ttk.Label( + self, + text="Add User", + font=('Arial', '24') + ) + lbl_title.pack(pady=(50,0)) + + # Creating entry box for new user's name + self.new_user_name = "" + self.user_entry = ttk.Entry( + self, + textvariable= self.new_user_name, + font=('Arial', '15') + ) + self.user_entry.pack(pady=30) + + # Creating the title for the window + password_label = ttk.Label( + self, + text="Enter Admin Password", + font=('Arial', '20') + ) + password_label.pack(pady=(10,0)) + + # Creating entry box for new user's name + self.password = "" + self.user_password = ttk.Entry( + self, + textvariable= self.password, + font=('Arial', '15'), + show = "*" + ) + self.user_password.pack(pady=30) + + # Creating the submit button + self.btn_submit = ttk.Button( + self, + text="Submit", + #padx = 50, + #pady = 10, + #relief=tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.pack() + + # Creating the cancel button + self.btn_submit = ttk.Button( + self, + text="Cancel", + #padx = 50, + #pady = 10, + #relief=tk.RAISED, + command= lambda: self.btn_cancel_action(parent) + ) + self.btn_submit.pack() + + # Forces frame to stay the size of the main_window + # rather than adjusting to the size of the widgets + self.pack_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + + + + ################################################# + + # Creates the function for the submit button command + # @params "_parent" is also a parent like "parent", but it is a different "parent", + # passes in GUIWindow + def btn_submit_action(self, _parent): + + self.new_user_name = self.user_entry.get() + self.password = self.user_password.get() + + # Popup to confirm that a new user is added into the DB + cnfm_pop = ConfirmPopup(_parent, self.data_holder, self.new_user_name, self.password) + + def remove_widgets(self, _parent): + for widget in self.winfo_children(): + widget.destroy() + + + ################################################# + + def btn_cancel_action(self, _parent): + + _parent.set_frame_login_frame() + + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + +################################################################################# + + +class ConfirmPopup(): + + ################################################# + + def __init__(self, parent, data_holder, new_user_name, password): + self.confirm_popup(data_holder, new_user_name, password) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder, new_user_name, password): + self.data_holder = data_holder + self.new_user_name = new_user_name + self.password = password + logger.info("Confirming that the user wants to add {} to the database.".format(self.new_user_name)) + # Creates a popup to ask whether or not to retry the test + self.popup = ttk.Toplevel() + self.popup.title("New User Name") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = " You are about to add {} as a user \n Are you sure? ".format(self.new_user_name), + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) + + # Creates retry and continue buttons + btn_retry = ttk.Button( + frm_popup, + width = 8, + #height = 2, + text = "Cancel", + #relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.cancel_function() + ) + btn_retry.grid(column = 0, row = 1) + + btn_continue = ttk.Button( + frm_popup, + width = 8, + #height = 2, + text = "Confirm", + #relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.continue_function(self.parent) + ) + btn_continue.grid(column = 1, row = 1) + + + ################################################# + + # Called when the "cancel" button is selected + def cancel_function(self): + self.popup.destroy() + + ################################################# + + # Called to continue on in the testing procedure + def continue_function(self, _parent): + self.popup.destroy() + + # Adding a new user name to data_holder/DB + self.data_holder.add_new_user_name(self.new_user_name, self.password) + # Changes frame to scan_frame + _parent.set_frame_login_frame() + + + +################################################################################# diff --git a/PhotoTakingGUI/PythonFiles/Scenes/CameraScene.py b/PhotoTakingGUI/PythonFiles/Scenes/CameraScene.py new file mode 100755 index 00000000..ab420898 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/CameraScene.py @@ -0,0 +1,356 @@ +''' + CAMERA_SCENE +------------------ +Instructions: +1. Ensure a version of python has been installed (created on Python 3.11.4) +2. Run the command "pip install Pillow" +3. Run the command "pip install opencv-python" +------------------ +''' +import PythonFiles +import json, logging +from picamera2 import Picamera2, Preview +import tkinter as tk +import tkinter.ttk as ttk +import cv2 +import PIL.Image, PIL.ImageTk +from PIL import ImageTk as iTK +from PIL import ImageChops +import time +import os +import numpy as np + +from libcamera import controls + +global camera +camera = Picamera2() + +################################################################################# + +# Instantiating logging +# Code that should go in every file in the GUI(s) +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.CameraScene') + + +# Frame class for basic webcam functionality +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data +class CameraScene(ttk.Frame): + + def __init__(self, parent, master_frame, data_holder, video_source=0 ): + + # variables to determine what to do with photo and prompting text + self.photo_packed = False + self.flip = False + + self.master_frame = master_frame + + # Call to the super class's constructor + # Super class is the tk.Frame class + super().__init__(master_frame, width = 1105, height = 850) + + self.data_holder = data_holder + self.parent = parent + + # Add the style + self.create_style(parent) + + # Frame for the buttons + btn_frame=ttk.Frame(self, width = 800) + btn_frame.pack(anchor="nw") + + # Snapshot button + self.btn_snapshot=ttk.Button(btn_frame, text="Snapshot",width=20, command=self.snapshot) + self.btn_snapshot.pack(side="left", padx=10, pady=10) + + # Help button + self.btn_proses=ttk.Button( + btn_frame, + text="Help", + width=10, + #relief = tk.RAISED, + command= lambda: self.help_action(parent), + ) + self.btn_proses.pack(side="left", padx=10, pady=10) + + self.btn_about=ttk.Button( + btn_frame, + text="Submit", + width=10, + command= lambda: self.submit_button_action(parent), + ) + self.btn_about.pack(side="right", padx=10, pady=10) + + self.long_desc_label_text = tk.StringVar() + self.long_desc_label_text.set("Photo Description") + + self.long_desc_label = ttk.Label( + master= btn_frame, + textvariable = self.long_desc_label_text, + font = ('Arial', 24), + ) + self.long_desc_label.pack(side="right", padx=(20, 90), pady=10) + + self.desc_label_text = tk.StringVar() + self.desc_label_text.set("Photo Type") + + self.desc_label = ttk.Label( + master= btn_frame, + textvariable = self.desc_label_text, + font = ('Arial', 24), + ) + self.desc_label.pack(side="right", padx=(90, 20), pady=10) + + # Prevents the frame from shrinking + self.pack_propagate(0) + + + # How long in between photo-frames on the GUI + self.delay=10 + + self.camera_created = False + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + def update_preview(self): + + # gets rid of any old image + if self.photo_packed == True: + self.Engine_label.destroy() + self.photo_packed = False + + + # Prevents the camera from being started twice + # If the camera is started twice, throws exceptions + if self.camera_created == False: + + self.camera_config = camera.create_still_configuration(main={"size": (2304, 1296)}, lores={'size': (854, 480)}, display='lores') + + camera.configure(self.camera_config) + + camera.start_preview(Preview.DRM, x = 900, y = 200, width = 854, height = 480) + + camera.start() + + self.camera_created = True + + camera.set_controls( {"AfMode" : controls.AfModeEnum.Continuous} ) + + + + ################################################# + + # Tells GUIWindow to open up the help popup + def help_action(self, _parent): + _parent.help_popup(self) + + ################################################# + + # Updates the description for the image from the config file + def set_text(self, index): + self.current_index = index + + updated_title = self.data_holder.get_photo_list()[index]["name"] + updated_description = self.data_holder.get_photo_list()[index]["desc_short"] + + self.desc_label_text.set(updated_title) + self.long_desc_label_text.set(updated_description) + + if self.flip == True: + self.flip_label.destroy() + self.flip = False + + # adds text saying to flip the board over if it's the first time the bottom picture is being taken + if self.parent.retake == True: + pass + elif updated_title == 'Bottom': + self.flip_label = ttk.Label( + master = self, + text = 'Flip Board Over', + font = ('Arial', 20), + ) + self.flip_label.pack() + self.flip = True + + + ################################################# + + # Saves a picture of the currently shown camera + def snapshot(self): + # sets up a name for the file + shortened_pn = "captured_image{}.png".format(self.current_index) + self.photo_name = "{}/Images/{}".format(PythonFiles.__path__[0], shortened_pn) + + # Cannot be called unless camera is already started + logger.info("Capturing image") + self.image = camera.switch_mode_and_capture_image(shortened_pn) + min_area = 25000 + padding = 50 + + img_rgb = np.array(self.image) + if img_rgb.shape[-1] == 4: + img_rgb = img_rgb[:, : , :3] + height, width, _ = img_rgb.shape + hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV) + + lower_green = np.array([25, 20, 20]) + upper_green = np.array([95, 255, 255]) + mask_green = cv2.inRange(hsv, lower_green, upper_green) + + lower_red1 = np.array([0, 40, 40]) + upper_red1 = np.array([10, 255, 255]) + mask_red1 = cv2.inRange(hsv, lower_red1, upper_red1) + + lower_red2 = np.array([170, 40, 40]) + upper_red2 = np.array([180, 255, 255]) + mask_red2 = cv2.inRange(hsv, lower_red2, upper_red2) + + mask_red = cv2.bitwise_or(mask_red1, mask_red2) + pcb_mask = cv2.bitwise_or(mask_green, mask_red) + + def select_best_contour(mask): + + kernel = np.ones((5, 5), np.uint8) + clean_mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) + clean_mask = cv2.morphologyEx(clean_mask, cv2.MORPH_CLOSE, kernel) + + # select edges around non white objects + contours, _ = cv2.findContours(clean_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + best = None + best_score = 0 + + for c in contours: + area = cv2.contourArea(c) + hull = cv2.convexHull(c) + x, y, w, h = cv2.boundingRect(hull) + # filter out small dust particles and such + if area < min_area: + continue + + hull_area = cv2.contourArea(hull) + if hull_area == 0: + continue + + # how solid is the object? closer to 1 = more solid + solidity = area / hull_area + score = solidity * area + # saves the largest and most solid object, should always be the board + if score > best_score: + best = (x, y, w, h) + best_score = score + + return best + + best = select_best_contour(pcb_mask) + + if best is None: + gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) + blur = cv2.GaussianBlur(gray, (5, 5), 0) + + med = np.median(blur) + lower = int(max(0, 0.66 * med)) + upper = int(min(255, 1.33 * med)) + edges = cv2.Canny(blur, lower, upper) + + edges = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=1) + closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, np.ones((9, 9), np.uint8), iterations=2) + + best = select_best_contour(closed) + + if best is None: + logger.error("Image couldn't be cropped.") + + else: + x, y, w, h = best + # don't go over the bounds of the image + x_start = max(x - padding, 0) + y_start = max(y - padding, 0) + x_end = min(x + w + padding, width) + y_end = min(y + h + padding, height) + + imageBox = (x_start, y_start, x_end, y_end) + self.image = self.image.crop(imageBox) + + # stores the image in the data holder + # doesn't try to write it to disk, uses more ram but saves time + self.data_holder.image_holder[self.photo_name] = self.image + # scales the image to fit the gui window after it's been cropped + # this doesn't affect the size of the actual image that goes into the Database + width = int(self.image.size[0]*(800/self.image.size[1])) + self.Engine_image = self.image.resize((width, 800), PIL.Image.LANCZOS) + if width > 1000: + height = int(self.image.size[1]*(1000/self.image.size[0])) + self.Engine_image = self.image.resize((1000, height), PIL.Image.LANCZOS) + self.Engine_PhotoImage = iTK.PhotoImage(self.Engine_image) + + # if the flip prompt exists, destroy it + if self.flip == True: + self.flip_label.destroy() + self.flip = False + + # if there isn't an image currently displayed, add one + if self.photo_packed == False: + self.Engine_label = ttk.Label(self) + self.Engine_label.configure(image=self.Engine_PhotoImage) + self.Engine_label.image = self.Engine_PhotoImage + + self.Engine_label.pack() + self.photo_packed = True + # else, replace the previous image + # this is used when retaking the image before clicking submit + else: + self.Engine_label.configure(image=self.Engine_PhotoImage) + self.Engine_label.image = self.Engine_PhotoImage + + self.data_holder.image_data.append(self.photo_name) + + def continuous_update(self): + + time.sleep(1) + for i in range(100): + + self.snapshot() + + time.sleep(1) + + # goes to the next screen + def submit_button_action(self, _parent): + try: + camera.stop_preview() + camera.stop() + self.camera_created = False + except: + pass + + # if a photo is being retaken, set the retaken value to true + if self.parent.retake == True: + self.parent.retaken = True + + self.parent.next_frame_camera_frame() + + def get_submit_action(self): + return self.submit_button_action + + def get_parent(self): + return self.parent + + + # Closes the video camera with the "release()" command + # Important for closing gracefully + def __del__(self): + pass + + def remove_widgets(self, parent): + camera.close() + self.__del__() diff --git a/PhotoTakingGUI/PythonFiles/Scenes/LocalUploadScene.py b/PhotoTakingGUI/PythonFiles/Scenes/LocalUploadScene.py new file mode 100644 index 00000000..b13c20af --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/LocalUploadScene.py @@ -0,0 +1,223 @@ + +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter.ttk as ttk +import tkinter as tk +import sys, time +from tkinter import * +from tkinter import messagebox as mb +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + + +################################################################################# + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.LocalUploadScene') + + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class LocalUploadScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + + self.data_holder = data_holder + + self.EXIT_CODE = 0 + + self.master_frame = master_frame + + super().__init__(self.master_frame, width=1300-213, height = 700) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.create_style(parent) + + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + def get_local_boards(self, parent): + + self.local_boards_list = [] + + for self.dirpath, dirnames, filenames in os.walk("{}/PythonFiles/Images".format(parent.main_path)): + for f in filenames: + fp = os.path.join(self.dirpath, f) + name = fp[59:] + if "320" in name: + sn, view = name.split('_') + self.local_boards_list.append([fp, sn, os.path.splitext(view)[0]]) + + self.update_board_tree() + + # Creates the GUI itself + def initialize_GUI(self, parent, master_frame): + + # reskinned frame from the shipping GUI + Scan_Board_Prompt_Frame = ttk.Frame(self, width = 1105, height = 650) + Scan_Board_Prompt_Frame.grid(column=0, row = 0, sticky='nsew') + + #resizing + Scan_Board_Prompt_Frame.grid_columnconfigure(0, weight=1) + Scan_Board_Prompt_Frame.grid_columnconfigure(1, weight=1) + Scan_Board_Prompt_Frame.grid_rowconfigure(1, weight=2) + #Scan_Board_Prompt_Frame.grid_rowconfigure(1, weight=1) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=0) + + # creates a Frame for the Treeview + self.boards_scanned_frame = ttk.Frame(Scan_Board_Prompt_Frame, borderwidth=2, relief="sunken") + self.boards_scanned_frame.grid(column=0, row=1, rowspan = 10, sticky='nsew', padx = 20, pady = 20) + + #creating treeview for boards scanned + self.board_tree = ttk.Treeview( + self.boards_scanned_frame, + selectmode = 'browse', + ) + self.board_tree["columns"] = ("id", "board_barcode", "board_view") + self.board_tree.column('#0', width = 0, stretch = 'no') + self.board_tree.column("id", anchor = 'center', width=50, stretch='no') + self.board_tree.column("board_barcode", anchor = 'center', width=300, stretch='yes') + self.board_tree.column("board_view", anchor = 'center', width=200, stretch='yes') + + self.board_tree.heading('id', text = "#", anchor = 'center') + self.board_tree.heading('board_barcode', text = "Board Barcode", anchor = 'center') + self.board_tree.heading('board_view', text = "View", anchor = 'center') + + self.board_tree.grid(column = 0, row = 0, sticky='nsew') + + # creates a Label Variable, different customization options + self.lbl_scan = ttk.Label( + master= Scan_Board_Prompt_Frame, + text = "List of all locally saved boards", + font = ('Arial', 20) + ) + self.lbl_scan.grid(column=0, columnspan = 2, row=0, sticky='we', padx=10, pady=5) + + # Entry for the full id to be displayed. Upon Scan, update and disable? + #global ent_full + + # Creating intial value in entry box + self.user_text = tk.StringVar(self) + + # Remove button creation + self.btn_rm = ttk.Button( + Scan_Board_Prompt_Frame, + text = "Remove Board", + command = lambda: self.btn_rm_action(parent) + ) + self.btn_rm.grid(column=1, row=1, padx = 10, pady = 5) + + # save to db button creation + self.btn_save_to_db = ttk.Button( + Scan_Board_Prompt_Frame, + text = "Upload Boards", + command = lambda: self.btn_save_to_db_action(parent) + ) + self.btn_save_to_db.grid(column = 1, row = 3, padx = 10, pady = 50) + + # Creating frame for logout button (DEV) + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 0, row = 5, columnspan=2, sticky= 'nsew') + + # Creating the finish scanning button + btn_cancel = ttk.Button( + frm_logout, + text = "Cancel", + command = lambda: self.btn_fin_scan_action(parent) + ) + btn_cancel.grid(column=0, columnspan = 2, row=4, padx=10, pady=50) + + # Creating the help button + btn_help = ttk.Button( + Scan_Board_Prompt_Frame, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.grid(column=1, row=2, padx=10, pady=10) + + ################################################# + + def update_board_tree(self): + for item in self.board_tree.get_children(): + self.board_tree.delete(item) + i = 0 + for board in self.local_boards_list: + i = i + 1 + self.board_tree.insert("", "end", values = (i, board[1], board[2])) + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + def btn_save_to_db_action(self, _parent): + self.data_holder.add_local_boards_to_db(self.local_boards.list) + logger.info("Adding locally saved images uploaded to database.") + + ################################################# + + # Function for the remove button + def btn_rm_action(self, _parent): + selection = self.board_tree.selection() + select = self.board_tree.item(selection, "values") + + rm_action = mb.askyesno( + message=(f'Are you sure you want to remove {select}?'), + icon='warning', + title='Remove' + ) + if rm_action == True: + self.local_boards_list.remove([self.dirpath + "/" + select[1] + "_" + select[2] + ".png", select[1], select[2]]) + self.update_board_tree() + os.remove(self.dirpath + "/" + select[1] + "_" + select[2] + ".png") + logger.info("LocalUploadScene: %s_%s.png deleted." % (select[1], select[2])) + + ################################################# + + # finish scanning action + def btn_fin_scan_action(self, _parent): + self.EXIT_CODE = 1 + + if self.use_scanner: + self.listener.terminate() + self.scanner.terminate() + + self.data_holder.clear_add_boards_to_db_list() + + # Send user back to login frame + _parent.set_frame_login_frame() + + self.EXIT_CODE = 0 + + ################################################# diff --git a/PhotoTakingGUI/PythonFiles/Scenes/LoginScene.py b/PhotoTakingGUI/PythonFiles/Scenes/LoginScene.py new file mode 100644 index 00000000..75e436dd --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/LoginScene.py @@ -0,0 +1,174 @@ +################################################################################# + +# importing necessary modules +import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os +from PIL import Image, ImageTk + +################################################################################# + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.LoginScene') + +# Creates a class that is called by the GUIWindow. +# GUIWindow instantiates an object called login_frame. +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class LoginScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + + super().__init__(master_frame, style = 'LoginScene.TFrame', width = 1350, height = 850) + self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + self.parent = parent + + def create_style(self, _parent): + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + #self.s.configure('LoginScene.TFrame', background = 'sand') + + + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + img = ImageTk.PhotoImage(Image.open("{}/PythonFiles/Images/UMNLOGO.png".format(parent.main_path))) + bg_label = ttk.Label(self, image = img) + bg_label.place(x=275, y=250, relwidth=1, relheight=1) + bg_label.image = img + + # Creating a list of users for dropdown menu + User_List = self.data_holder.get_all_users() + + # Creating the title for the window + lbl_title = ttk.Label( + self, + text="Please Select Your Name" + ) + lbl_title.configure(font = ('Arial', 72)) + lbl_title.pack(pady=75) + + # Creating intial value in dropdown menu + self.user_selected = tk.StringVar(self) + self.user_selected.set("") # default value is empty + + # Creating the dropdown menu itself + self.opt_user_dropdown = ttk.OptionMenu( + self, + self.user_selected, # Tells option menu to use the created initial value + 'Select User', + *User_List # Tells the dropdown menu to use every index in the User_List list + ) + self.opt_user_dropdown.pack(pady=(0,20)) + self.opt_user_dropdown.config(width = 14) + + # Traces when the user selects an option in the dropdown menu + # When an option is selected, it calls the show_submit_button function + self.user_selected.trace( + 'w', + lambda *args: self.show_submit_button() + ) + + # Creating the submit button + # It does not get enabled until the user selects an option menu option + self.btn_submit = ttk.Button( + self, + text="Submit", + command= lambda: self.btn_submit_action(parent) + ) + + + self.btn_submit.pack(pady = (0,20)) + self.btn_submit.config(state = 'disabled', width = 12) + + # Creating the add user button + self.btn_add_user = ttk.Button( + self, + text="Add User", + command= lambda: self.btn_add_user_action(parent) + ) + self.btn_add_user.pack(pady=20) + self.btn_add_user.config(width = 12) + + self.btn_upload_photos = ttk.Button( + self, + text="Upload Locally Saved Images", + command = lambda: self.btn_upload_action(parent) + ) + self.btn_upload_photos.pack(pady=20) + + # Creating the help button + self.btn_help = ttk.Button( + self, + text = "Help", + command = lambda: self.help_action(parent) + ) + self.btn_help.config(width = 12) + self.btn_help.pack(anchor = 's', padx = 10, pady = 20) + + + # Forces frame to stay the size of the main_window + # rather than adjusting to the size of the widgets + self.pack_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + + ################################################# + + # Creates the function for the submit button command + # @params "_parent" is also a parent like "parent", but it is a different "parent", + # passes in GUIWindow + def btn_submit_action(self, _parent): + # Sets the user_ID in the data_holder to the selected user + self.data_holder.set_user_ID(self.user_selected.get()) + # Changes frame to scan_frame + _parent.set_frame_scan_frame() + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + def btn_upload_action(self, _parent): + _parent.set_frame_upload_local_photos() + + def btn_add_user_action(self, _parent): + _parent.set_frame_add_user_frame() + + ################################################# + + # A function to pack the submit button + def show_submit_button(self): + self.btn_submit.config(state = 'active') + + ################################################# + + +################################################################################# diff --git a/PhotoTakingGUI/PythonFiles/Scenes/PostScanScene.py b/PhotoTakingGUI/PythonFiles/Scenes/PostScanScene.py new file mode 100644 index 00000000..eff3a33f --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/PostScanScene.py @@ -0,0 +1,264 @@ +################################################################################# + +import PythonFiles +import json, logging +import tkinter as tk +import tkinter.ttk as ttk +from PIL import ImageTk as iTK +from PIL import Image +from matplotlib.pyplot import table +from pyparsing import col +import PythonFiles +import os +import datetime + +################################################################################# + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.PostScanScene') + +# Frame that shows up after board has been entered with info about the board +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data + +class PostScanScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + + # Call to the super class's constructor + # Super class is the tk.Frame class + self.data_holder = data_holder + + self.master_frame = master_frame + + super().__init__(self.master_frame, width = 1300-213, height = 700) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.parent = parent + + self.create_frame(parent) + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def create_frame(self, parent): + self.create_style(parent) + + try: + for widget in self.winfo_children(): + widget.destroy() + except: + logger.warning("PostScanScene: Widgets could not be found and/or destroyed (making room for new widgets.") + else: + logger.info("PostScanScene: Widgets destroyed successfully (making room for new widgets).") + + + self.canvas = tk.Canvas(self) + self.frame = ttk.Frame(self.canvas, width=800, height=500) + self.scroller = ttk.Scrollbar(self, orient='vertical', command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.scroller.set) + + self.canvas.grid(row = 0, column = 0, sticky='nsew') + self.scroller.grid(row=0, column=1, sticky='nsw') + self.window = self.canvas.create_window((0,0), window=self.frame, anchor='nw', tags='self.frame') + + #resizing + self.frame.pack(fill='both', expand=True) + + self.frame.bind('', self.onFrameConfigure) + self.frame.bind('', self.onEnter) + self.frame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + self.frame.grid_columnconfigure(1, weight = 1) + self.frame.grid_columnconfigure(2, weight = 1) + self.grid_columnconfigure(0, weight = 1) + self.grid_rowconfigure(0, weight = 1) + + if self.data_holder.data_dict['prev_results']: + + # Adds the title to the Summary Frame + self.title = ttk.Label( + self.frame, + text = "Board Scanned!", + font = ('Arial', 32) + ) + self.title.grid(row= 0, column= 1, pady = 20) + + # Adds Board Full ID to the SummaryFrame + self.id = ttk.Label( + self.frame, + text = str(self.data_holder.data_dict['current_full_ID']), + font=('Arial',32,'bold') + ) + self.id.grid(row= 0, column= 2, pady = 20) + + green_check = Image.open("{}/Images/GreenCheckMark.png".format(PythonFiles.__path__[0])) + green_check = green_check.resize((75, 75), Image.LANCZOS) + green_check = iTK.PhotoImage(green_check) + + redx = Image.open('{}/Images/RedX.png'.format(PythonFiles.__path__[0])) + redx = redx.resize((75, 75), Image.LANCZOS) + redx = iTK.PhotoImage(redx) + # adds previously run tests to the canvas with pass/fail info + try: + if self.data_holder.data_dict['test_names']: + res_dict = {} + for n in self.data_holder.data_dict['test_names']: + res_dict[n] = [] + for idx,el in enumerate(self.data_holder.data_dict['prev_results']): + res_dict[el[0]] = el[1] + + for idx,el in enumerate(res_dict.keys()): + self.lbl_res = ttk.Label( + self.frame, + text = str(el) + ': ', + font=('Arial',24) + ) + self.lbl_res.grid(row=idx+5, column=1, pady = 7) + if res_dict[el] == 'Passed': + self.lbl_img = ttk.Label( + self.frame, + image = green_check, + font=('Arial',24) + ) + self.lbl_img.image=green_check + self.lbl_img.grid(row=idx+5, column=2, pady = 7) + elif res_dict[el] == 'Failed': + self.lbl_img = ttk.Label( + self.frame, + image = redx, + font=('Arial',24) + ) + self.lbl_img.image=redx + self.lbl_img.grid(row=idx+5, column=2, pady = 7) + else: + self.lbl_res = ttk.Label( + self.frame, + text = 'This test has not been run.', + font=('Arial',24) + ) + self.lbl_res.grid(row=idx+5, column=2, pady = 7) + + else: + self.lbl_res = ttk.Label( + self.frame, + text = str(self.data_holder.data_dict['prev_results']), + font=('Arial',24) + ) + self.lbl_res.grid(row=2, column=1) + + except Exception as e: + logging.exception(e) + self.lbl_full = ttk.Label( + self, + text = 'Error, No Results', + font=('Arial', 24) + ) + self.lbl_full.grid(row = 2, column =1, pady = 10) + + # Creating the proceed button + proceed_button = ttk.Button( + self, + #relief = tk.RAISED, + text = "Proceed", + command = lambda: self.btn_proceed_action(parent) + ) + proceed_button.grid(sticky = 's', padx = 10, pady = 25) + + else: + self.lbl_1 = ttk.Label( + self, + text = "This board hasn't been checked in.", + font=('Arial', 32) + ) + self.lbl_1.grid(row = 0, column =0, pady = 10, sticky = 'n' ) + + self.lbl_2 = ttk.Label( + self, + text = "Please visit the check in and inspection station.", + font=('Arial', 32) + ) + self.lbl_2.grid(row = 1, column =0, pady = 10, sticky = 'n') + + logout_frm = ttk.Frame(self) + logout_frm.grid(sticky = 'se') + + #creating the next board buttom + next_board_button = ttk.Button( + logout_frm, + text = "Change Boards", + command = lambda: self.btn_NextBoard_action(parent) + ) + next_board_button.grid(row=0, column=0, padx = 10, pady = 10) + + + # Creating the logout button + btn_logout = ttk.Button( + logout_frm, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.grid(row=1, column=0, padx = 10, pady = 10) + + + + ################################################# + + def btn_proceed_action(self, _parent): + _parent.first_frame_camera_frame() + + def btn_NextBoard_action(self, parent): + parent.set_frame_scan_frame() + + def btn_logout_action(self, parent): + parent.set_frame_login_frame() + + def get_submit_action(self): + return self.btn_proceed_action + + def get_parent(self): + return self.parent + + + ################################################# + + def update_frame(self): + self.create_frame(self.parent) + + ################################################# + + def onFrameConfigure(self, event): + self.canvas.configure(scrollregion=self.canvas.bbox('all')) + + def onMouseWheel(self, event): + if event.num == 4: + self.canvas.yview_scroll(-1, 'units') + elif event.num == 5: + self.canvas.yview_scroll(1, 'units') + + def onEnter(self, event): + self.canvas.bind_all('', self.onMouseWheel) + self.canvas.bind_all('', self.onMouseWheel) + + def onLeave(self, event): + self.canvas.unbind_all('') + self.canvas.unbind_all('') + + + +################################################################################# diff --git a/PhotoTakingGUI/PythonFiles/Scenes/ScanScene.py b/PhotoTakingGUI/PythonFiles/Scenes/ScanScene.py new file mode 100644 index 00000000..e3342663 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/ScanScene.py @@ -0,0 +1,364 @@ +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter as tk +import tkinter.ttk as ttk +import sys, time +from tkinter import * +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + + +################################################################################# + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.ScanScene') + + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class ScanScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + self.data_holder = data_holder + self.is_current_scene = False + self.EXIT_CODE = 0 + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) + + def create_style(self, _parent): + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + def scan_QR_code(self, master_window): + + self.ent_full.config(state = 'normal') + self.ent_full.delete(0,END) + self.master_window = master_window + self.hide_rescan_button() + + #sys.path.insert(1,'/home/hgcal/WagonTest/Scanner/python') + + from ..Scanner.python.get_barcodes import scan, listen, parse_xml + + manager = mp.Manager() + full_id = manager.list() + + self.ent_full.config(state = 'normal') + + self.scanner = scan(self.parent.main_path) + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + while 1 > 0: + + try: + self.master_window.update() + except: + pass + if not len(full_id) == 0: + # takes in the full id scanned, parses the hexadecimal in ASCII + # and sends it to the data holder + self.data_holder.set_full_ID(parse_xml(full_id[0])) + + self.listener.terminate() + self.scanner.terminate() + + self.ent_full.delete(0,END) + self.ent_full.insert(0, str(self.data_holder.get_full_ID())) + self.ent_full.config(state = 'disabled') + self.show_rescan_button() + break + + elif self.EXIT_CODE: + logging.info("Exit code received on the ScanScene. Terminating processes.") + self.listener.terminate() + self.scanner.terminate() + logging.info("ScanScene Processes terminated successfully.") + break + else: + time.sleep(.01) + + logging.info("ScanScene: Scan complete.") + + # Creates the GUI itself + def initialize_GUI(self, parent, master_frame): + + self.master_frame = master_frame + self.parent = parent + + super().__init__(self.master_frame,style = 'ScanScene.TFrame', width = 1350, height = 850) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.create_style(parent) + + lbl_frm = ttk.Frame(self) + lbl_frm.grid(column=1, row = 0 ) + + + logging.info("ScanScene: Frame has been created.") + # Create a photoimage object of the QR Code + QR_image = Image.open("{}/Images/WagonExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label = ttk.Label(lbl_frm, image=QR_PhotoImage) + QR_label.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label.grid(column=0, row = 0, sticky='new', pady = (0, 45)) + + # Create a photoimage object of the QR Code + QR_image2 = Image.open("{}/Images/EngineExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image2) + QR_label2 = ttk.Label(lbl_frm, image=QR_PhotoImage) + QR_label2.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label2.grid(column=0, row = 1, sticky='new') + + Scan_Board_Prompt_Frame = ttk.Frame(self) + Scan_Board_Prompt_Frame.grid(column=0, row = 0, sticky='nsew') + + Scan_Board_Prompt_Frame.grid_columnconfigure(0, weight=1) + Scan_Board_Prompt_Frame.grid_columnconfigure(1, weight=1) + QR_label.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(1, weight=1) + + # creates a Label Variable, different customization options + lbl_scan = ttk.Label( + master= Scan_Board_Prompt_Frame, + text = "Scan the QR Code on the Board", + font = ('Arial',36) + ) + lbl_scan.pack(padx = 50, pady = (85, 100)) + + # Create a label to label the entry box + lbl_full = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Full ID:", + font = ('Arial', 18) + ) + lbl_full.pack(padx = 20, pady = 15) + + # Creating intial value in entry box + user_text = tk.StringVar(self) + + # Creates an entry box + self.ent_full = ttk.Entry( + Scan_Board_Prompt_Frame, + font = ('Arial', 16), + textvariable= user_text, + width = 50 + ) + self.ent_full.pack(padx = (75, 50)) + + #self.ent_snum.mark_set("insert", "%d.%d" % (1, 1)) + + # Traces an input to show the submit button once text is inside the entry box + user_text.trace( + "w", + lambda name, + index, + mode, + sv=user_text: self.show_submit_button() + ) + + # Rescan button creation + self.btn_rescan = ttk.Button( + Scan_Board_Prompt_Frame, + text="Rescan", + #padx = 20, + #pady =10, + #relief = tk.RAISED, + command = lambda: self.scan_QR_code(self.master_window) + ) + self.btn_rescan.pack(pady=30) + + # Submit button creation + self.btn_submit = ttk.Button( + Scan_Board_Prompt_Frame, + text="Submit", + #padx = 20, + #pady = 10, + #relief = tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.pack() + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=0, row = 1) + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 1, row = 1, sticky= 'se') + + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.pack(anchor = 'se', padx = 10, pady = 10) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 's', padx = 10) + + + + + # Locks frame size to the master_frame size + self.grid_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + ################################################# + + + ################################################# + + # Function for the submit button + def btn_submit_action(self, _parent): + + self.EXIT_CODE = 0 + + self.data_holder.set_full_ID(self.ent_full.get()) + self.data_holder.check_if_new_board() + _parent.update_config() + _parent.set_frame_postscan() + + ################################################# + + # Function for the log out button + def btn_logout_action(self, _parent): + + self.EXIT_CODE = 1 + self.listener.terminate() + self.scanner.terminate() + + + # Send user back to login frame + _parent.set_frame_login_frame() + master_frame.after(500, self.set_frame_login_frame) + + self.EXIT_CODE = 0 + + ################################################# + + # Function to activate the submit button + def show_submit_button(self): + self.data_holder.decode_label(self.ent_full.get()) + self.btn_submit["state"] = "active" + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to disable to the submit button + def hide_submit_button(self): + self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to activate the rescan button + def show_rescan_button(self): + self.btn_rescan["state"] = "active" + + ################################################# + + # Function to disable to the rescan button + def hide_rescan_button(self): + self.btn_rescan["state"] = "disabled" + + ################################################# + + def kill_processes(self): + logger.info("ScanScene: Terminating scanner proceses.") + try: + self.scanner.kill() + self.listener.terminate() + self.EXIT_CODE = 1 + except: + logger.info("ScanScene: Processes could not be terminated.") diff --git a/PhotoTakingGUI/PythonFiles/Scenes/SplashScene.py b/PhotoTakingGUI/PythonFiles/Scenes/SplashScene.py new file mode 100644 index 00000000..b92b5b22 --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/SplashScene.py @@ -0,0 +1,80 @@ +################################################################################ + +# Importing necessary modules +import tkinter as tk +import tkinter.ttk as ttk +from PIL import ImageTk as iTK +from PIL import Image +import logging +logging.getLogger('PIL').setLevel(logging.WARNING) +import PythonFiles +import os + +################################################################################# + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.SplashScene') + +class SplashScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame): + self.initialize_GUI(parent, master_frame) + + ################################################# + + def initialize_GUI(self, parent, master_frame): + super().__init__(master_frame, width = 1105, height = 850) + + self.create_style(parent) + + logging.info("SplashScene: Frame has been initialized.") + # Creating Bethel Logo + img_bethel_logo = Image.open("{}/Images/Bethel_Logo.png".format(PythonFiles.__path__[0])) + img_bethel_logo = img_bethel_logo.resize((250,100), Image.LANCZOS) + phimg_bethel_logo = iTK.PhotoImage(img_bethel_logo) + lbl_bethel_logo = ttk.Label(self, image=phimg_bethel_logo, width=250) + lbl_bethel_logo.image = phimg_bethel_logo + + lbl_bethel_logo.grid(row=0, column= 0, padx = 50, pady = 100) + + # Creating UMN Logo + img_umn_logo = Image.open('{}/Images/UMN_Logo.png'.format(PythonFiles.__path__[0])) + img_umn_logo = img_umn_logo.resize((250,100), Image.LANCZOS) + phimg_umn_logo = iTK.PhotoImage(img_umn_logo) + lbl_umn_logo = ttk.Label(self, image=phimg_umn_logo, width=250) + lbl_umn_logo.image = phimg_umn_logo + + lbl_umn_logo.grid(row = 0 , column = 2, padx = 50, pady = 100) + + # Creating label for names + lbl_names = ttk.Label( + self, + text = ' Created by:\n \n Bryan Crossman, \n Andrew Kirzeder, \n Garrett Schindler, \n & \n Rand Bovard', + font = ('Arial', 15) + ) + lbl_names.grid(row = 1, column = 1) + + self.grid_propagate(0) + + ################################################# + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + +################################################################################# diff --git a/PhotoTakingGUI/PythonFiles/Scenes/TestSummaryScene.py b/PhotoTakingGUI/PythonFiles/Scenes/TestSummaryScene.py new file mode 100644 index 00000000..1bc589bf --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/Scenes/TestSummaryScene.py @@ -0,0 +1,192 @@ +################################################################################# + +import PythonFiles +import json, logging +import tkinter as tk +import tkinter.ttk as ttk +from PIL import ImageTk as iTK +from PIL import Image +from matplotlib.pyplot import table +from pyparsing import col +import PythonFiles +import os + +################################################################################# + + +logger = logging.getLogger('HGCAL_Photo.PythonFiles.Scenes.TestSummaryScene') + +# Frame that shows all of the final test results +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data + +class TestSummaryScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + + # Call to the super class's constructor + # Super class is the tk.Frame class + super().__init__(master_frame, width = 1350, height = 850) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.data_holder = data_holder + + self.parent = parent + + self.create_frame(parent) + + self.create_style(parent) + + # Fits the frame to set size rather than interior widgets + self.grid_propagate(0) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + ################################################# + + def create_frame(self, parent): + + self.blank_frame = ttk.Frame(self) + self.blank_frame.grid(row = 0, column = 0, padx = 80, pady = 20) + + self.columnconfigure(0, weight = 1) + self.columnconfigure(1, weight = 1) + self.columnconfigure(2, weight = 1) + self.columnconfigure(3, weight = 1) + + # Adds the title to the TestSummary Frame + self.title = ttk.Label( + self, + #fg='#0d0d0d', + text = "Photo Taking Finished!", + font=('Arial',32,'bold') + ) + self.title.grid(row= 0, column= 1, pady = 20) + + self.retake_prompt = ttk.Label( + self, + text = "Press an image to retake it.", + font=('Arial',20) + ) + self.retake_prompt.grid(row=1,column=1) + + row_offset = 0 + + + # Tries to add all of the images to the final screen + # different formatting depending on the number of photos taken + # images can be clicked to retake that photo + for i, photo in enumerate(self.data_holder.image_holder): + try: + Board_image = self.data_holder.image_holder[photo] + Board_image = Board_image.resize((350, 200), Image.LANCZOS) + Board_PhotoImage = iTK.PhotoImage(Board_image) + if i == 0: + retake_1 = ttk.Button( + self, + image = Board_PhotoImage, + command = lambda: self.btn_retake_action(parent, 0) + ) + retake_1.image = Board_PhotoImage + retake_1.grid(column = i+1, row = 2) + if i == 1: + retake_2 = ttk.Button( + self, + image = Board_PhotoImage, + command = lambda: self.btn_retake_action(parent, 1) + ) + retake_2.image = Board_PhotoImage + retake_2.grid(column = i+1, row = 2) + + except Exception as e: + logging.debug("TestSummaryScene: Could not find captured_image.") + logging.debug("Exception: {}".format(e)) + next + + + # Adds Board full id to the TestSummaryFrame + self.lbl_full = ttk.Label( + self, + text = "Full ID: " + str(self.data_holder.data_dict['current_full_ID']), + font=('Arial', 32) + ) + self.lbl_full.grid(column = 1, row = 3 + row_offset, pady = 10) + + # Adds Tester Name to the TestSummary Frame + self.lbl_tester = ttk.Label( + self, + text = "Tester: " + self.data_holder.data_dict['user_ID'], + font=('Arial', 24) + ) + self.lbl_tester.grid(column = 1, row = 4 + row_offset, pady = 10) + + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 1, row = 6 + row_offset, pady = 20) + + + # Creating the next board button + next_board_button = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Submit and Go to Next Board", + command = lambda: self.btn_NextBoard_action(parent) + ) + next_board_button.pack(anchor = 'ne', padx = 10, pady = 10) + + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.pack(anchor = 'n', padx = 10, pady = 20) + + + + + ################################################# + + def btn_NextBoard_action(self, parent): + # this function saves the images + self.data_holder.send_image() + parent.set_frame_scan_frame() + + def btn_logout_action(self, parent): + parent.set_frame_login_frame() + + def btn_retake_action(self, parent, photo_index): + parent.retake_photo(photo_index) + + def get_submit_action(self): + return self.btn_NextBoard_action + + def get_parent(self): + return self.parent + + + + ################################################# + + def update_frame(self): + self.create_frame(self.parent) + + + +################################################################################# diff --git a/PhotoTakingGUI/PythonFiles/__init__.py b/PhotoTakingGUI/PythonFiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PhotoTakingGUI/PythonFiles/update_config.py b/PhotoTakingGUI/PythonFiles/update_config.py new file mode 100644 index 00000000..033de3fc --- /dev/null +++ b/PhotoTakingGUI/PythonFiles/update_config.py @@ -0,0 +1,32 @@ +from PythonFiles.GUIConfig import GUIConfig +import yaml +from pathlib import Path + + +def update_config(full_id): + + # sets the config to wagon or engine based on the 4th character of the board's full id + if full_id[3] in ('W', 'S'): + #from TestConfigs.Wagon_cfg import masterCfg + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Wagon_cfg.yaml")) + board_cfg = masterCfg + + elif full_id[3] == 'E': + #from TestConfigs.Engine_cfg import masterCfg + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Engine_cfg.yaml")) + board_cfg = masterCfg + + elif full_id[3:9] in ('ZPLMEZ','ZPLMZ2'): + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Engine_cfg.yaml")) + board_cfg = masterCfg + + else: + #from TestConfigs.Wagon_cfg import masterCfg + masterCfg = import_yaml(open(Path(__file__).parent.parent / "Configs/Wagon_cfg.yaml")) + board_cfg = masterCfg + + return GUIConfig(board_cfg) + +def import_yaml(filename): + + return yaml.safe_load(filename) diff --git a/PhotoTakingGUI/README.md b/PhotoTakingGUI/README.md new file mode 100644 index 00000000..3d59a767 --- /dev/null +++ b/PhotoTakingGUI/README.md @@ -0,0 +1,20 @@ +# Photo Taking GUI + +Graphical user interface responsible for running the photo taking of both wagons and engines. The interface is largely based on the [HGCAL Tester GUI](https://github.com/UMN-CMS/HGCALTestGUI). Instead of running tests on a wagon/engine test stand, this GUI utilizes a camera to take a photo of the specified electrical board. This information is then stored in a database for data monitoring. This software is specifically built for use on [Raspberry Pi OS](https://www.raspberrypi.com/software/) but all packages work across Windows and Linux devices with some tweaks. + +### Extra Camera Installation +If you are using a Raspberry Pi Camera Module 3, you will need to make extra installations in order to run the code. Documentation links are found below: + +- [The Picamera2 Library](https://datasheets.raspberrypi.com/camera/picamera2-manual.pdf?_gl=1*seefj*_ga*MTQ0NTI3MzQ3OC4xNjg5ODYwNjkw*_ga_22FD70LWDS*MTY4OTg2MjM2Ny4xLjEuMTY4OTg2MzMyOS4wLjAuMA..) +- [libcamera Documentation](https://www.raspberrypi.com/documentation/computers/camera_software.html#getting-started) +- [Updating Raspberry Pi OS](https://www.raspberrypi.com/documentation/computers/os.html#using-apt) + + + + +## Running the GUI +After all of the packages have been installed and the camera is plugged into the Raspberry Pi, you will be able to run the program. +``` +cd ~/path/to/PhotoTakingGUI +python3 MainFunctionVI.py + diff --git a/PhotoTakingGUI/RaspberryPiCameraDownloads.sh b/PhotoTakingGUI/RaspberryPiCameraDownloads.sh new file mode 100755 index 00000000..1d157e13 --- /dev/null +++ b/PhotoTakingGUI/RaspberryPiCameraDownloads.sh @@ -0,0 +1,20 @@ +#!/bin/bash + + +# Required installs to get the Raspberry PI Camera in Tkinter +pip3 install tkinter + +apt-get install python3-pil python3-pil.imagetk + +apt install python3-matplotlib python3-tk + +pip3 install pyzmq + +pip3 install -U numpy + +apt install -y python3-picamera2 + + +# Version type matters here + +pip3 install opencv-contrib-python==4.5.3.56 diff --git a/PythonFiles/Data/DBSender.py b/PythonFiles/Data/DBSender.py new file mode 100644 index 00000000..498b848b --- /dev/null +++ b/PythonFiles/Data/DBSender.py @@ -0,0 +1,281 @@ +import requests +import json +import socket +import logging +from pathlib import Path +import logging +# from read_barcode import read_barcode + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Data.DBSender') + +# python scripts run from here are on the machine that contains the server and database +class DBSender(): + + def __init__(self, gui_cfg): + self.gui_cfg = gui_cfg + + # Predefined URL for the database + self.db_url = self.gui_cfg.getDBInfo("baseURL") + + # If True, use database + # If False, run in "offline" mode + self.use_database = self.gui_cfg.get_if_use_DB() + + def attempt_admin_access(self, password): + r = requests.post('{}/connect_admin.py'.format(self.db_url), data={'password': password}) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error('There was an issue with the web API script `connect_admin.py`. The website may be down.') + logger.debug(r.text) + + for i in range(begin, end): + if lines[i] == 'Success': + return True + else: + return False + + def decode_label(self, full_id): + + if len(full_id) != 15: + logger.warning('Invalid label scanned') + label_info = None + else: + r = requests.post('{}/decode_label.py'.format(self.db_url), data={'label': full_id}) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error('There was an issue with the web API script `decode_label.py`. Check that the label library has been updated for the web API.') + logger.debug(r.text) + + temp = [] + + for i in range(begin, end): + temp.append(lines[i]) + + label_info = {'Major Type': temp[0], 'Subtype': temp[1], 'SN': temp[2]} + + return label_info + + + def add_new_user_ID(self, user_ID, passwd): + + if (self.use_database): + + try: + r = requests.post('{}/add_tester2.py'.format(self.db_url), data= {'person_name':user_ID, 'password': passwd}) + except Exception as e: + logger.error("Unable to add the user to the database. Username: {}. Check to see if your password is correct.".format(user_ID)) + logger.debug(r.text) + + + # If not using the database, use this... + else: + pass + + + # Returns an acceptable list of usernames from the database + def get_usernames(self): + if (self.use_database): + r = requests.get('{}/get_usernames.py'.format(self.db_url)) + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error('There was an issue with the web API script `get_usernames.py`. There is likely a syntax error in an associated web API script.') + logger.debug(r.text) + + usernames = [] + + for i in range(begin, end): + temp = lines[i] + usernames.append(temp) + + return usernames + + # If not using database... + else: + + return self.gui_cfg.getUsers() + + + # Returns a list of booleans + # Whether or not DB has passing results + def get_previous_test_results(self, full_id): + + r = requests.post('{}/get_previous_test_results.py'.format(self.db_url), data={"full_id": str(full_id)}) + + lines = r.text.split('\n') + + try: + begin1 = lines.index("Begin1") + 1 + end1 = lines.index("End1") + begin2 = lines.index("Begin2") + 1 + end2 = lines.index("End2") + begin3 = lines.index("Begin3") + 1 + end3 = lines.index("End3") + except: + logger.error('There was an issue with the web API script `get_previous_test_results.py`. There is likely a syntax error in an associated web API script.') + logger.debug(r.text) + + tests_run = [] + outcomes = [] + poss_tests = [] + + for i in range(begin1, end1): + tests_run.append(lines[i]) + for i in range(begin2, end2): + outcomes.append(lines[i]) + for i in range(begin3, end3): + poss_tests.append(lines[i]) + + tests_passed = [] + for i in range(len(tests_run)): + tests_passed.append([tests_run[i], outcomes[i]]) + + return tests_passed, poss_tests + + + # Posts a new board with passed in full id + def add_new_board(self, full, user_id, comments): + r = requests.post('{}/add_module2.py'.format(self.db_url), data={"full_id": str(full)}) + r = requests.post('{}/board_checkin2.py'.format(self.db_url), data={"full_id": str(full), 'person_id': str(user_id), 'comments': str(comments)}) + + + try: + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error('There was an issue with the web API scripts `add_module2.py` or `board_checkin2.py`. There is likely a syntax error in an associated web API script.') + logger.debug(r.text) + + in_id = None + + for i in range(begin, end): + in_id = lines[i] + except: + logger.warning("Tried checking in a board that was already checked in") + in_id = None + + return in_id + + def is_new_board(self, full): + r = requests.post('{}/is_new_board.py'.format(self.db_url), data={"full_id": str(full)}) + + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error('There was an issue with the web API script `is_new_board.py`. There is likely a syntax error in an associated web API script.') + logger.debug(r.text) + + + for i in range(begin, end): + + if lines[i] == "True": + return True + elif lines[i] == "False": + return False + + def set_component_info(self, info_dict): + r = requests.post('{}/set_component_info.py'.format(self.db_url), data = info_dict) + + def add_test_stand_info(self, info_dict): + r = requests.post('{}/add_test_station_info.py'.format(self.db_url), data = info_dict) + + lines = r.text.split('\n') + + begin = lines.index("Begin") + 1 + end = lines.index("End") + + for i in range(begin, end): + return lines[i] + + def get_tester_config(self, teststand): + r = requests.post('{}/get_tester_config_id.py'.format(self.db_url), data={'test_stand':teststand}) + + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + + for i in range(begin, end): + return lines[i] + + except: + logger.warning('Tried to fetch tester configuration from DB... none was found.') + return None + + + def add_test_json(self, json_file, config): + + with open(json_file) as load_file: + results = json.load(load_file) + + test_attach = results.pop('data', None) + datafile_name = "{}/JSONFiles/sending.json".format(str(Path.home().absolute())) + + results['test_type'] = results['name'] + results['full_id'] = results['board_sn'] + results['successful'] = int(results['pass']) + results['config_id'] = config + + with open(datafile_name, 'w') as datafile: + json.dump(test_attach, datafile) + + datafile = open(datafile_name, "rb") + + attach_data = {'attach1': datafile} + + if (self.use_database): + r = requests.post('{}/add_test_json.py'.format(self.db_url), data = results, files = attach_data) + else: + pass + + + # Returns a list of all different types of tests + def get_test_list(self): + if (self.use_database): + r = requests.get('{}/get_test_types.py'.format(self.db_url)) + + lines = r.text.split('\n') + + try: + begin = lines.index("Begin") + 1 + end = lines.index("End") + except: + logger.error('There was an issue with the web API script `get_test_types.py`. There is likely a syntax error in an associated web API script.') + logger.debug(r.text) + + tests = [] + + for i in range(begin, end): + temp = lines[i][1:-1].split(",") + temp[0] = str(temp[0][1:-1]) + temp[1] = int(temp[1]) + tests.append(temp) + + return tests + + else: + + blank_tests = [] + for i in enumerate(self.gui_cfg.getNumTest()): + blank_tests.append("Test{}".format(i)) + + return blank_tests + diff --git a/PythonFiles/Data/DataHolder.py b/PythonFiles/Data/DataHolder.py index 46857080..e100dac8 100644 --- a/PythonFiles/Data/DataHolder.py +++ b/PythonFiles/Data/DataHolder.py @@ -1,132 +1,517 @@ -################################################################################# +################################################################################ +import json, logging, socket, PythonFiles, copy, os +import requests +from PythonFiles.Data.DBSender import DBSender +from PythonFiles.update_config import update_config +from pathlib import Path +import yaml +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Data.DataHolder') class DataHolder(): ################################################# # List of the variables being held by data holder - def __init__(self): - self.user_ID = "" # Tester's Name - self.test_stand = "" # Test stand for the unit - self.current_serial_ID = -1 # Unit Serial Number - self.test1_completed = False # Whether the test has been completed - self.test2_completed = False # Whether the test has been completed - self.test3_completed = False # Whether the test has been completed - self.test4_completed = False # Whether the test has been completed - self.test1_pass = False # Whether the test has been passed - self.test2_pass = False # Whether the test has been passed - self.test3_pass = False # Whether the test has been passed - self.test4_pass = False # Whether the test has been passed + def __init__(self, gui_cfg): + + (Path.home() / "JSONFiles").mkdir(exist_ok=True, parents=True) + + # Object for taking care of instantiation for different test types + self.gui_cfg = gui_cfg + + self.curpath = os.path.dirname(os.path.abspath(__file__)) + + # Object that sends information to the database + self.data_sender = DBSender(gui_cfg) + #self.dbclient = DBSendClient() + use_db = self.gui_cfg.get_if_use_DB() + + if use_db: + self.test_list = self.data_sender.get_test_list() + + gui_tests = self.gui_cfg.getTests() + db_test_names = dict(self.test_list) + self.index_gui_to_db = {} + for i,x in enumerate(gui_tests): + if x['name'] in db_test_names.keys(): + self.index_gui_to_db[i] = x['name'] + #self.index_gui_to_db = {i : db_test_names[x["name"]] for i,x in enumerate(gui_tests)} + + else: + gui_tests = self.gui_cfg.getTests() + self.index_gui_to_db = {} + for i,x in enumerate(gui_tests): + self.index_gui_to_db[i] = x['name'] + #self.index_gui_to_db = [i for i,x in enumerate(self.gui_cfg.getTests())] + + #dictionary of info to be held + self.data_dict = { + 'user_ID': "_", + 'test_stand': str(socket.gethostname()), + 'current_full_ID': "-1BAD", + 'queue': None, + 'comments': "_", + 'prev_results': None, + 'test_names': None, + 'checkin_id': None, + 'tests_run': [i for i in range(self.getNumTest())], + } + # adds tests to dictionary to be marked as complete + for i in range(self.gui_cfg.getNumTest()): + self.data_dict["test{}_completed".format(i)] = False + self.data_dict["test{}_pass".format(i)] = False + + for i in range(self.gui_cfg.getNumPhysicalTest()): + self.data_dict['physical{}_completed'.format(i)] = False + self.data_dict['physical{}_pass'.format(i)] = False + + self.data_lists = { + 'test_results': [], + 'test_completion': [], + 'physical_results': [], + 'physical_completion': [], + } + + self.total_test_num = 0 + + self.ptest_criteria = {} + self.ptest_names = self.gui_cfg.getPhysicalNames() + + self.label_info = None + + self.config_id = self.data_sender.get_tester_config(self.data_dict['test_stand']) + + self.admin = False + self.password = None + + # TODO Initialize with correct value from config + self.tester_type = self.gui_cfg.getGUIType() + + self.wagon_tester_info = { + 'Kria': None, + 'Tester': None, + 'Interposer': None, + 'Wagon Wheel 1': None, + 'Wagon Wheel 2': None, + 'Wagon Wheel 3': None, + 'Wagon Wheel 4': None, + 'num_wagon_wheels': 0, + 'interposer_type': None, + } + + self.engine_tester_info = { + 'ZCU': None, + 'Test Bridge 1': None, + 'Test Bridge 2': None, + 'VTRX 1': None, + 'VTRX 2': None, + 'HD Interposer': None, + 'East Interposer': None, + 'West Interposer': None, + 'Major Type': None, + } + + + # adds each test to data list for results and completion status to be added + for i in range(self.gui_cfg.getNumPhysicalTest()): + self.data_lists['physical_results'].append(self.data_dict['physical{}_pass'.format(i)]) + self.data_lists['physical_completion'].append(self.data_dict['physical{}_completed'.format(i)]) + + temp_dict = { + '{}'.format(i) : self.gui_cfg.getPhysicalTestRequirements(i), + } + + self.ptest_criteria.update(temp_dict) + + self.total_test_num = self.total_test_num + 1 + + for i in range(self.gui_cfg.getNumTest()): + self.data_lists['test_results'].append(self.data_dict['test{}_pass'.format(i)]) + self.data_lists['test_completion'].append(self.data_dict['test{}_completed'.format(i)]) + + self.total_test_num = self.total_test_num + 1 + ################################################# - # Clears the data by reseting all the values to the initial state - def clear_DataHolder(self): - self.__init__() + def get_total_test_num(self): + return self.total_test_num + + def get_use_scanner(self): + return self.gui_cfg.getUseScanner() + + def get_check_dict(self, idx): + return self.all_checkboxes[idx] + + def get_comment_dict(self, idx): + return self.all_comments[idx] + + def set_comment_dict(self, idx, val): + self.all_comments[idx] = val + + def add_new_user_name(self, user_ID): + self.data_dict['user_ID'] = user_ID + + is_new_user_ID = True + + for item in self.get_all_users(): + if self.data_dict['user_ID'] == item: + is_new_user_ID = False + + if is_new_user_ID: + self.data_sender.add_new_user_ID(self.data_dict['user_ID'], self.password) + + def get_physical_criteria(self, num): + return self.ptest_criteria[num] + + + # when a board gets entered, this function checks if it's new + def check_if_new_board(self): + logger.info("Checking if full id is a new board...") + + full = self.get_full_ID() + #returns true if the board is new, false if not + is_new_board = self.data_sender.is_new_board(full) + + if is_new_board == True: + logger.info("Board is new") + + else: + logger.info("Board has been checked in, getting previous results") + # if the board is not new, this returns the previous testing information on the board + prev_results, test_names = self.data_sender.get_previous_test_results(full) + if prev_results: + self.data_dict['test_names'] = test_names + self.data_dict['prev_results'] = prev_results + else: + self.data_dict['test_names'] = None + self.data_dict['prev_results'] = 'No tests have been run on this board.' + + def decode_label(self, full_id): + self.label_info = self.data_sender.decode_label(full_id) + + def get_test_results(self): + prev_results, test_names = self.data_sender.get_previous_test_results(self.get_full_ID()) + res_dict = {} + for n in test_names: + res_dict[n] = [] + for idx,el in enumerate(prev_results): + res_dict[el[0]] = el[1] + return res_dict + + + ################################################# + + + def set_user_ID(self, user_ID): + + self.data_dict['user_ID'] = user_ID + logger.info("User ID set to %s" % user_ID) + + ################################################## + + def set_full_ID(self, full): + self.data_dict['current_full_ID'] = full + if self.gui_cfg.getSerialCheckSafe(): + new_cfg = update_config(full) + self.gui_cfg = new_cfg + self.data_holder_new_test() + self.data_sender = DBSender(self.gui_cfg) + logger.info("Full ID set to {}".format(full)) + + + ################################################## + + def get_full_ID(self): + return self.data_dict['current_full_ID'] + + ################################################# + + def attempt_admin_access(self, password): + logger.info("User attempting admin access.") + admin_connected = self.data_sender.attempt_admin_access(password) + if admin_connected == True: + logger.info("Admin access was successful") + self.admin = True + self.password = password + else: + logger.info("Admin access was denied") + + def upload_test_stand_info(self): + if self.tester_type == 'Wagon': + info_dict = {'kria': self.wagon_tester_info['Kria'], + 'tester': self.wagon_tester_info['Tester'], + 'interposer': self.wagon_tester_info['Interposer'], + 'interposer_type': self.wagon_tester_info['interposer_type'], + 'wheel_1': self.wagon_tester_info['Wagon Wheel 1'], + 'wheel_2': self.wagon_tester_info['Wagon Wheel 2'], + 'wheel_3': self.wagon_tester_info['Wagon Wheel 3'], + 'wheel_4': self.wagon_tester_info['Wagon Wheel 4'], + 'test_stand': self.data_dict['test_stand'], + } + if self.tester_type == 'Engine': + info_dict = {'ZCU': self.engine_tester_info['ZCU'], + 'east_interposer': self.engine_tester_info['East Interposer'], + 'west_interposer': self.engine_tester_info['West Interposer'], + 'hd_interposer': self.engine_tester_info['HD Interposer'], + 'bridge_1': self.engine_tester_info['Test Bridge 1'], + 'bridge_2': self.engine_tester_info['Test Bridge 2'], + 'vtrx_1': self.engine_tester_info['VTRX 1'], + 'vtrx_2': self.engine_tester_info['VTRX 2'], + 'test_stand': self.data_dict['test_stand'], + } + + logger.info("Setting tester configuration") + self.config_id = self.data_sender.add_test_stand_info(info_dict) + + def set_component_info(self, label, working, comments): + info_dict = {'label': label, 'working': working, 'comments': comments} + + if self.tester_type == 'Wagon': + wagon_cfg = yaml.safe_load(open('{}/../../Configs/Wagon_cfg.yaml'.format(self.curpath),"r")) + db_url = wagon_cfg['DBInfo']['baseURL'] + + if self.tester_type == 'Engine': + engine_cfg = yaml.safe_load(open('{}/../../Configs/Engine_cfg.yaml'.format(self.curpath),"r")) + db_url = engine_cfg['DBInfo']['baseURL'] + + logger.info("Setting tester component information") + self.data_sender.set_component_info(info_dict, db_url) ################################################# # Future method to send data to the database - def send_to_DB(self): - pass - + def send_all_to_DB(self): + + person_ID = self.data_dict['user_ID'] + comments = self.data_dict['comments'] + full_id = self.get_full_ID() + + + logger.info("Sending results to database.") + for i in range(len(self.data_dict['tests_run'])): + temp = 0 + if self.data_lists['test_results'][i]: + temp = 1 + info_dict = {"full_id":full_id,"tester": person_ID, "test_type": self.index_gui_to_db[self.tests_run[i]], "successful": temp, "comments": comments} + with open("{}/JSONFiles/storage.json".format(str(Path.home().absolute())), "w") as outfile: + logger.debug(info_dict) + json.dump(info_dict, outfile) + self.data_sender.add_test_json("{}/JSONFiles/storage.json".format(str(Path.home().absolute()))) + #message = "add_test_json;{'json_file': {}/JSONFiles/storage.json, ''}" + logger.info("All results sent to database.") ################################################# - # Prints all the variable values inside data_holder - def print(self): - print("user_ID: ", self.user_ID) - print("test_stand: ", self.test_stand) - print("current_serial_ID: ", self.current_serial_ID) - print("test1_completed: ", self.test1_completed) - print("test2_completed: ", self.test2_completed) - print("test3_completed: ", self.test3_completed) - print("test4_completed: ", self.test4_completed) - print("test1_pass: ", self.test1_pass) - print("test2_pass: ", self.test2_pass) - print("test3_pass: ", self.test3_pass) - print("test4_pass: ", self.test4_pass) + def send_to_DB(self, test_run): + logger.info("Uploading results...") + + index = test_run + + test_names = self.gui_cfg.getTestNames() + + file_path_list = [] + + for name in test_names: + file_path_list.append("{}/JSONFiles/Current_{}_JSON.json".format(str(Path.home().absolute()), name.replace(" ", "").replace("/", ""))) + + # Converts self.test_results[index] into 1/0 instead of bool + temp = 0 + if self.data_lists['test_results'][index]: + temp = 1 + + if self.config_id: + info_dict = {"full_id":self.get_full_ID(),"tester": self.data_dict['user_ID'], "test_type": self.index_gui_to_db[self.data_dict['tests_run'][index]], "successful": temp, "comments": self.data_dict['comments'], 'config':self.config_id} + else: + info_dict = {"full_id":self.get_full_ID(),"tester": self.data_dict['user_ID'], "test_type": self.index_gui_to_db[self.data_dict['tests_run'][index]], "successful": temp, "comments": self.data_dict['comments']} + + with open("{}/JSONFiles/storage.json".format(str(Path.home().absolute())), "w") as outfile: + logger.debug(str(info_dict)) + json.dump(info_dict, outfile) + + self.data_sender.add_test_json(file_path_list[index], self.config_id) + logger.info("Test results sent to database successfully.") + + self.data_dict['comments'] = '_' ################################################# - # Gets all the variables from data_holder - def get(self): - return ( - self.user_ID + - self.test_stand + - str(self.current_serial_ID) + - str(self.test1_completed) + - str(self.test2_completed) + - str(self.test3_completed) + - str(self.test4_completed) + - str(self.test1_pass) + - str(self.test2_pass) + - str(self.test3_pass) + - str(self.test4_pass) - ) + def get_all_users(self): + users_list = self.data_sender.get_usernames() + return users_list + + + ################################################# + + def update_from_json_string(self, imported_json_string): + json_string = imported_json_string.replace("'", '"') + json_string = json_string.replace('True', 'true') + json_string = json_string.replace('False', 'false') + json_dict = json.loads(json_string) + + test_names = self.gui_cfg.getTestNames() + test_type = test_names[self.current_test_idx] + + with open("{}/JSONFiles/Current_{}_JSON.json".format(str(Path.home().absolute()), test_names[self.current_test_idx].replace(" ", "").replace("/", "")), "w") as file: + json.dump(json_dict, file) + self.data_dict['test{}_completed'.format(self.current_test_idx)] = True + self.data_dict['test{}_pass'.format(self.current_test_idx)] = json_dict["pass"] + comments = json_dict.get('comments', '_') + self.data_dict['comments'] = comments + + # Updates the lists + for i in range(self.gui_cfg.getNumTest()): + self.data_lists['test_results'][i] = self.data_dict['test{}_pass'.format(i)] + self.data_lists['test_completion'][i] = self.data_dict['test{}_completed'.format(i)] + + if self.gui_cfg.get_if_use_DB(): + self.send_to_DB(self.current_test_idx) + + ################################################ + + def add_inspection_to_comments(self): + if self.inspection_data['board_chipped_bent']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Board is chipped or bent." + if self.inspection_data['wagon_connection_pin_bent']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Wagon connnection pin is bent." + if self.inspection_data['engine_connection_pin_bent']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " Engine connection pin is bent." + if self.inspection_data['visual_scratches']: + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " There are visual scratches on the board." + if self.inspection_data['inspection_comments'] != "_": + if self.data_dict['comments'] == "_": + self.data_dict['comments'] = "" + self.data_dict['comments'] = self.data_dict['comments'] + " User comments: " + self.inspection_data['inspection_comments'] ################################################ + def setOtherZippers(self, zips): + self.other_zippers = zips - def update_from_json_string(self, imported_json_string): - json_dict = json.loads(imported_json_string) + def passOtherZippers(self): + + placeholder = { + "name": None, + "board_sn": None, + "tester": self.data_dict['user_ID'], + "pass": True, + "data": { + "tested_sn": self.data_dict['current_full_ID'] + }, + "comments": "Passed by default" + } + + for zipper in self.other_zippers: + + placeholder['board_sn'] = zipper + + for test in self.gui_cfg.getTestNames(): + + placeholder['name'] = test + + with open("{}/JSONFiles/default_storage.json".format(str(Path.home().absolute())), "w") as outfile: + json.dump(placeholder, outfile) + + self.data_sender.add_test_json("{}/JSONFiles/default_storage.json".format(str(Path.home().absolute())), self.config_id) + + + ################################################ + + # Tracking the test index in another place and propagating to the config + def setTestIdx(self, test_idx): + self.current_test_idx = test_idx + + def getNumTest(self): + return self.gui_cfg.getNumTest() + + def getTestNames(self): + return self.gui_cfg.getTestNames() + + def getNumPhysicalTest(self): + return self.gui_cfg.getNumPhysicalTest() + + def getPhysicalNames(self): + return self.gui_cfg.getPhysicalNames() + + ################################################ + + def getGUIcfg(self): + return self.gui_cfg + + ################################################ + + # resets the data holder when a new board is scanned + # Keeps the login information stored, full id has already been changed + def data_holder_new_test(self): + + self.data_dict = { + 'user_ID': self.data_dict['user_ID'], + 'test_stand': str(socket.gethostname()), + 'current_full_ID': self.data_dict['current_full_ID'], + 'queue': self.data_dict['queue'], + 'comments': "_", + 'prev_results': None, + 'test_names': None, + 'checkin_id': None, + 'tests_run': [i for i in range(self.getNumTest())], + } + + self.data_lists = { + 'test_results': [], + 'test_completion': [], + 'physical_results': [], + 'physical_completion': [], + } + + self.total_test_num = 0 + + self.label_info = None + + for i in range(self.gui_cfg.getNumTest()): + self.data_dict['test{}_completed'.format(i)] = False + self.data_dict['test{}_pass'.format(i)] = False + + for i in range(self.gui_cfg.getNumPhysicalTest()): + self.data_dict['physical{}_completed'.format(i)] = False + self.data_dict['physical{}_pass'.format(i)] = False + + + self.ptest_criteria = {} + self.ptest_names = self.gui_cfg.getPhysicalNames() + + for i in range(self.gui_cfg.getNumPhysicalTest()): + self.data_lists['physical_results'].append(self.data_dict['physical{}_pass'.format(i)]) + self.data_lists['physical_completion'].append(self.data_dict['physical{}_completed'.format(i)]) + + temp_dict = { + '{}'.format(i) : self.gui_cfg.getPhysicalTestRequirements(i), + } + + self.ptest_criteria.update(temp_dict) + + self.total_test_num = self.total_test_num + 1 - test_type = json_dict["name"] - if test_type == "GenRes Test": - with open("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/Current_GenRes_JSON.json", "w") as file: - json.dump(json_dict, file) - - self.user_ID = json_dict["tester"] - self.current_serial_ID = json_dict["board_sn"] - self.test1_completed = True - self.test1_pass = json_dict["pass"] - - elif test_type == "IDRes Test": - with open("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/Current_IDRes_JSON.json", "w") as file: - json.dump(json_dict, file) - - self.user_ID = json_dict["tester"] - self.current_serial_ID = json_dict["board_sn"] - self.test2_completed = True - self.test2_pass = json_dict["pass"] - - - elif test_type == "IIC Test": - with open("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/Current_IIC_JSON.json", "w") as file: - json.dump(json_dict, file) - - self.user_ID = json_dict["tester"] - self.current_serial_ID = json_dict["board_sn"] - self.test3_completed = True - self.test3_pass = json_dict["pass"] - - elif test_type == "Bit Error Rate Test": - with open("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/Current_BERT_JSON.json", "w") as file: - json.dump(json_dict, file) - - self.user_ID = json_dict["tester"] - self.current_serial_ID = json_dict["board_sn"] - self.test4_completed = True - self.test4_pass = json_dict["pass"] + for i in range(self.gui_cfg.getNumTest()): + self.data_lists['test_results'].append(self.data_dict['test{}_pass'.format(i)]) + self.data_lists['test_completion'].append(self.data_dict['test{}_completed'.format(i)]) + + self.total_test_num = self.total_test_num + 1 + + logger.info("DataHolder Information has been reset for a new board.") - - def reset_data_holder(self): - self.user_ID = "" - self.test_stand = "" - self.current_serial_ID = -1 - self.test1_completed = False - self.test2_completed = False - self.test3_completed = False - self.test4_completed = False - self.test1_pass = False - self.test2_pass = False - self.test3_pass = False - self.test4_pass = False ################################################ + ################################################################################# -################################################################################# diff --git a/PythonFiles/Data/__init__.py b/PythonFiles/Data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/GUIConfig.py b/PythonFiles/GUIConfig.py new file mode 100644 index 00000000..21de49ec --- /dev/null +++ b/PythonFiles/GUIConfig.py @@ -0,0 +1,94 @@ +import logging +# Class to handle creation of different types of GUIs based on which board we want to test +# This class will hold all of the frame information and order them accordingly + +logger = logging.getLogger('HGCALTestGUI') + + +# Responsible for interfacing with the configuration file +class GUIConfig(): + + # Loads in a config file with board type name + # Information about board tests and database are stored within the config + def __init__(self, board_cfg): + self.board_cfg = board_cfg + self.current_idx = 1 + + self.configure() + + + # Create the GUI instance based off testing information + def configure(self): + + # Possibly do something special here if need be + + logger.info("Instance of {} GUI created.".format(self.getGUIType())) + + + # Get serial check safe attribute + def getSerialCheckSafe(self): + return self.board_cfg["SerialCheckSafe"] + + + # Get number of tests to define order of scenes and sidebar + def getNumTest(self): + return len(self.board_cfg["Test"]) + + # Get the number of tests that require physical input + def getNumPhysicalTest(self): + return len(self.board_cfg["PhysicalTest"]) + + # Returns the information necessary for physical test + # Formatted as a dictionary + def getPhysicalTestRequirements(self, num): + index = 0 + for ptest in self.board_cfg["PhysicalTest"]: + if index == num: + return ptest + + return None + + def getUseScanner(self): + return self.board_cfg["UsingScanner"] + + def getTests(self): + return self.board_cfg["Test"] + + def getPhysicalTests(self): + return self.board_cfg["PhysicalTest"] + + # Get database info for getting and posting test results + def getDBInfo(self, key=None): + if key is None: + return self.board_cfg["DBInfo"] + else: + return self.board_cfg["DBInfo"][key] + + # Returns true if the database should be used + def get_if_use_DB(self): + return self.board_cfg['DBInfo']['use_database'] + + def getGUIType(self): + return self.board_cfg["GUIType"] + + def getTestHandler(self): + return self.board_cfg["TestHandler"] + + def getUsers(self): + return self.board_cfg["People"] + + ################################ + + # Returns the names for the physical tests from config + def getPhysicalNames(self): + try: + return [test["name"] for test in self.board_cfg["PhysicalTest"]] + except: + return [] + + def getTestNames(self): + try: + return [test["name"] for test in self.board_cfg["Test"]] + except: + logger.error("Unable to return test names from config. Check to see if test['name'] is empty") + return [] diff --git a/PythonFiles/GUIWindow.py b/PythonFiles/GUIWindow.py index f5d606c4..56bf3356 100644 --- a/PythonFiles/GUIWindow.py +++ b/PythonFiles/GUIWindow.py @@ -3,15 +3,19 @@ # Importing all neccessary modules from pickle import NONE import tkinter as tk -from turtle import bgcolor +#from turtle import bgcolor import multiprocessing as mp import logging +import os #from pyparsing import trace_parse_action # Importing all the neccessary files and classes from them +import PythonFiles +from PythonFiles.GUIConfig import GUIConfig from PythonFiles.Scenes.SidebarScene import SidebarScene from PythonFiles.Scenes.LoginScene import LoginScene from PythonFiles.Scenes.ScanScene import ScanScene +from PythonFiles.Scenes.ScanManyScene import ScanManyScene from PythonFiles.TestFailedPopup import TestFailedPopup from PythonFiles.Scenes.TestSummaryScene import TestSummaryScene from PythonFiles.Scenes.TestScene import * @@ -19,13 +23,27 @@ from PythonFiles.Data.DataHolder import DataHolder from PythonFiles.Scenes.SplashScene import SplashScene from PythonFiles.Scenes.TestInProgressScene import * - -################################################################################# +from PythonFiles.Scenes.AddUserScene import AddUserScene +from PythonFiles.Scenes.PostScanScene import PostScanScene +from PythonFiles.Scenes.AdminScene import AdminScene +from PythonFiles.Scenes.AdminScanScene import AdminScanScene +from PythonFiles.Scenes.AdminScanScene import interposer_Popup +from PythonFiles.Scenes.AdminScanScene import finished_Popup +from PythonFiles.Scenes.ComponentScanScene import TesterComponentScene +from PythonFiles.update_config import update_config +import webbrowser +import sys + +from PythonFiles.Scenes.ThermalTestConfigScene import ThermalTestConfigScene +from PythonFiles.Scenes.ThermalTestSetupResultsScene import ThermalTestSetupResultsScene +from PythonFiles.Scenes.ThermalTestBeginScene import ThermalTestBeginScene +from PythonFiles.Scenes.ThermalTestInProgressScene import ThermalTestInProgressScene +from PythonFiles.Scenes.ThermalTestFinalResultsScene import ThermalTestFinalResultsScene +################################################################################# -FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' -logging.basicConfig(filename="/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/logs/GUIWindow.log", filemode = 'w', format=FORMAT, level=logging.DEBUG) +logger = logging.getLogger('HGCALTestGUI.PythonFiles.GUIWindow') # Create a class for creating the basic GUI Window to be called by the main function to # instantiate the actual object @@ -33,35 +51,64 @@ class GUIWindow(): ################################################# - def __init__(self, conn, queue): + def __init__(self, conn, conn_trigger, queue, board_cfg, main_path): + self.conn = conn + self.conn_trigger = conn_trigger self.queue = queue - + self.retry_attempt = False + self.completed_window_alive = False + self.current_test_index = 0 + self.gui_cfg = GUIConfig(board_cfg) + self.main_path = main_path + # Create the window named "self.master_window" - # global makes self.master_window global and therefore accessible outside the function self.master_window = tk.Tk() - self.master_window.title("Bethel Interns' Window") - # Creates the size of the window and disables resizing - self.master_window.geometry("1063x500+25+100") - self.master_window.resizable(0,0) + self.master_window.title("HGCAL Test Window") + + self.master_window.report_callback_exception = self.log_callback_exception + + # Creates the size of the window + width = self.master_window.winfo_screenwidth() + height = self.master_window.winfo_screenheight() + + self.master_window.geometry("{}x{}".format(width, height)) + self.master_window.pack_propagate(1) + + #resizing master_frame, keeping sidebar same width + self.master_window.grid_columnconfigure(0, weight=0) # Make the sidebar resizable + self.master_window.grid_columnconfigure(1, weight=1) # Make the master frame resizable + self.master_window.grid_rowconfigure(0, weight=1) + + # Variables necessary for the help popup + self.all_text = "No help available for this scene." + self.label_text = tk.StringVar() + + self.run_all_tests_bool = False + + # resizable with following code commented out + #self.master_window.resizable(0,0) # Removes the tkinter logo from the window # self.master_window.wm_attributes('-toolwindow', 'True') + # Creates and packs a frame that exists on top of the master_frame - self.master_frame = tk.Frame(self.master_window, width=850, height= 500) - self.master_frame.grid(column = 1, row = 0, columnspan = 4) + self.master_frame = tk.Frame(self.master_window, width=1400-225, height=900) + self.master_frame.grid(column = 1, row = 0, columnspan = 4, sticky="nsew") # Creates a frame to house the sidebar on self.master_window - sidebar_frame = tk.Frame(self.master_window, width = 213, height = 500) - sidebar_frame.grid(column = 0 , row = 0) + sidebar_frame = tk.Frame(self.master_window, width = 225, height=900) + sidebar_frame.grid(column = 0 , row = 0, sticky="nsw") + # Creates the "Storage System" for the data during testing - self.data_holder = DataHolder() + self.data_holder = DataHolder(self.gui_cfg) + self.data_holder.data_dict['queue'] = queue # Creates all the widgets on the sidebar self.sidebar = SidebarScene(self, sidebar_frame, self.data_holder) - self.sidebar.pack() + self.sidebar.grid(row=0, column=0, sticky="nsew") ################################################# # Creates all the different frames in layers # @@ -70,192 +117,417 @@ def __init__(self, conn, queue): # At top so it can be referenced by other frames' code... Order of creation matters self.test_summary_frame = TestSummaryScene(self, self.master_frame, self.data_holder) - self.test_summary_frame.grid(row=0, column=0) + self.test_summary_frame.grid(row=0, column=0, sticky='nsew') self.login_frame = LoginScene(self, self.master_frame, self.data_holder) - self.login_frame.grid(row=0, column=0) - - self.scan_frame = ScanScene(self, self.master_frame, self.data_holder) - self.scan_frame.grid(row=0, column=0) - - self.test1_frame= Test1Scene(self, self.master_frame, self.data_holder, - "General Resistance Test", - queue - ) - self.test1_frame.grid(row=0, column=0) - - self.test2_frame= Test2Scene(self, self.master_frame, self.data_holder, - "ID Resistor Test", - queue - ) - self.test2_frame.grid(row=0, column=0) - - self.test3_frame= Test3Scene(self, self.master_frame, self.data_holder, - "I2C Comm. Test", - queue - ) - self.test3_frame.grid(row=0, column=0) - - self.test4_frame= Test4Scene(self, self.master_frame, self.data_holder, - "Bit Rate Test", - queue - ) - self.test4_frame.grid(row=0, column=0) - - self.test_in_progress_frame = TestInProgressScene(self, self.master_frame, self.data_holder, queue) - self.test_in_progress_frame.grid(row=0, column=0) - - - # Near bottom so it can reference other frames with its code + self.login_frame.grid(row=0, column=0, sticky = 'nsew') + + self.post_scan_frame = PostScanScene(self, self.master_frame, self.data_holder) + self.post_scan_frame.grid(row=0, column=0, sticky = 'nsew') + + self.scan_frame = ScanScene(self, self.master_frame, self.data_holder) + self.scan_frame.grid(row=0, column=0, sticky = 'nsew') + + self.scan_many_frame = ScanManyScene(self, self.master_frame, self.data_holder) + self.scan_many_frame.grid(row=0, column=0, sticky = 'nsew') + + self.test_in_progress_frame = TestInProgressScene(self, self.master_frame, self.data_holder, queue, conn) + self.test_in_progress_frame.grid(row=0, column=0, sticky = 'nsew') + + self.admin_scan_frame = AdminScanScene(self, self.master_frame, self.data_holder) + self.admin_scan_frame.grid(row=0, column=0, sticky = 'nsew') + + self.admin_frame = AdminScene(self, self.master_frame, self.data_holder) + self.admin_frame.grid(row=0, column=0, sticky = 'nsew') + + self.tester_component_frame = TesterComponentScene(self, self.master_frame, self.data_holder) + self.tester_component_frame.grid(row=0, column=0, sticky = 'nsew') + + self.add_user_frame = AddUserScene(self, self.master_frame, self.data_holder) + self.add_user_frame.grid(row=0, column=0, sticky= 'nsew') + self.splash_frame = SplashScene(self, self.master_frame) - self.splash_frame.grid(row=0, column=0) + self.splash_frame.grid(row=0, column=0, sticky = 'nsew') + + if (self.data_holder.tester_type == 'Thermal'): + self.thermal_in_progress_frame = ThermalTestInProgressScene(self, self.master_frame, self.data_holder, queue, self.conn_trigger) + self.thermal_in_progress_frame.grid(row=0, column=0, sticky='nsew') + + self.thermal_begin_frame = ThermalTestBeginScene(self, self.master_frame, self.data_holder, queue, conn) + self.thermal_begin_frame.grid(row=0, column=0, sticky='nsew') + + self.thermal_config_frame = ThermalTestConfigScene(self, self.master_frame, self.data_holder, queue, self.conn_trigger) + self.thermal_config_frame.grid(row=0, column=0, sticky='nsew') + + self.thermal_setup_results_frame = ThermalTestSetupResultsScene(self, self.master_frame, self.data_holder, queue, conn) + self.thermal_setup_results_frame.grid(row=0, column=0, sticky='nsew') + + self.thermal_final_results_frame = ThermalTestFinalResultsScene(self, self.master_frame, self.data_holder, queue, conn) + self.thermal_final_results_frame.grid(row=0, column=0, sticky='nsew') + ################################################# # End Frame Creation # ################################################# - logging.info("GUIWindow: All frames have been created.") - + logger.info("All frames have been created.") # Tells the master window that its exit window button is being given a new function self.master_window.protocol('WM_DELETE_WINDOW', self.exit_function) # Sets the current frame to the splash frame self.set_frame_splash_frame() + self.master_frame.update() + self.master_frame.after(100, self.set_frame_login_frame) + + self.master_window.mainloop() + + def log_callback_exception(self, exc_type, exc_value, exc_traceback): + logger.error("Exception in Tkinter callback", exc_info=(exc_type, exc_value, exc_traceback)) + + def create_style(self): - self.master_frame.after(500, self.set_frame_login_frame) + self.s = tk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(self.main_path)) + self.s.tk.call('package', 'require', 'awdark') + self.s.theme_use('awdark') - self.master_window.mainloop() + ################################################# + + def create_test_frames(self, queue): + # Generalize test frames to use testing config + # Grab list of tests from config file and create one scene for each test + # Tests are indexed starting at 1 and using the order of the list in the config + self.test_frames = [] + test_list = self.gui_cfg.getTests() + physical_list = self.gui_cfg.getPhysicalTests() + + offset = 0 + + # For the digital tests + for test_idx,test in enumerate(test_list): + + self.test_frames.append(TestScene(self, self.master_frame, self.data_holder, test["name"], test["desc_short"], test["desc_long"], queue, self.conn_trigger, test_idx)) + self.test_frames[test_idx + offset].grid(row=0, column=0, sticky='nsew') + + + ################################################# + + def update_config(self): + #switch between Wagon and Engine config depending on the full id entered + full = self.data_holder.get_full_ID() + if not self.gui_cfg.getSerialCheckSafe(): + return + new_cfg = update_config(full) + self.gui_cfg = new_cfg + + ################################################# + + + def run_all_tests(self): + + logger.info("Run all tests method has been selected.") + + self.running_all_idx = 0 + self.current_test_index = 0 + self.data_holder.setTestIdx(self.current_test_index) + self.run_all_tests_bool = True + cur_name = self.gui_cfg.getTests()[self.current_test_index]['name'] + + test_client = REQClient(self.gui_cfg, cur_name.strip().replace(" ", ""), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID'], self.conn_trigger) + #test_client = REQClient(self.gui_cfg, 'test{}'.format(self.running_all_idx), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID'], self.conn_trigger) + #test_client = REQClient('test{}'.format(self.running_all_idx), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID']) + self.set_frame_test_in_progress(self.queue) + + + ################################################# + + def set_frame_add_user_frame(self): + + logger.info("Setting frame to add_user_frame") + + self.add_user_frame.update_frame(self) + self.set_frame(self.add_user_frame) + + ################################################# + + def set_frame_admin_frame(self): + logger.info("Setting frame to admin_frame") + + self.admin_frame.update_frame(self) + self.set_frame(self.admin_frame) + + def set_frame_admin_scan(self): + logger.info("Setting frame to admin_scan_frame") + + self.component_index = 0 + + self.admin_scan_frame.is_current_scene = True + self.admin_scan_frame.update_frame(self, self.component_index) + self.set_frame(self.admin_scan_frame) + self.admin_scan_frame.scan_QR_code(self.master_window) + + logger.debug('Component Index: ' + str(self.component_index)) + + def next_frame_admin_scan(self): + self.component_index += 1 + logger.debug('Component Index: ' + str(self.component_index)) + + if self.data_holder.tester_type == 'Wagon': + if self.component_index < 3+int(self.data_holder.wagon_tester_info['num_wagon_wheels']): + self.admin_scan_frame.update_frame(self, self.component_index) + if list(self.data_holder.wagon_tester_info)[self.component_index] == 'Interposer': + interposer_popup = interposer_Popup(self, self.data_holder) + + self.admin_scan_frame.scan_QR_code(self.master_window) + else: + finished_Popup(self, self.data_holder) + logger.debug(self.data_holder.wagon_tester_info) + + self.data_holder.upload_test_stand_info() + self.set_frame_login_frame() + + if self.data_holder.tester_type == 'Engine': + if self.component_index == 5: + if self.data_holder.engine_tester_info["Major Type"] == "LD": + self.component_index += 1 + + if self.component_index == 6: + if self.data_holder.engine_tester_info["Major Type"] == "HD": + self.component_index = 10 + + + if self.component_index < 8: + self.admin_scan_frame.update_frame(self, self.component_index) + self.admin_scan_frame.scan_QR_code(self.master_window) + else: + finished_Popup(self, self.data_holder) + logger.debug(self.data_holder.engine_tester_info) + + self.data_holder.upload_test_stand_info() + self.set_frame_login_frame() + + ################################################# + + + def set_frame_tester_component_frame(self): + + logger.info("Setting frame to tester_component_frame") + + self.tester_component_frame.is_current_scene = True + self.set_frame(self.tester_component_frame) + self.tester_component_frame.scan_QR_code(self.master_window) ################################################# def set_frame_login_frame(self): - self.set_frame(self.login_frame) - - logging.debug("GUIWindow: The frame has been set to login_frame.") + logger.info("The frame has been set to login_frame") + + self.sidebar.clean_up_btns() + self.sidebar.update_sidebar(self) + self.login_frame.update_frame(self) + self.set_frame(self.login_frame) + ################################################# def set_frame_scan_frame(self): + if (self.data_holder.tester_type == 'Thermal'): + logger.info("set_frame_scan_frame has been bypassed, setting Thermal Config Frame") + self.set_frame_thermal_config() + else: + logger.info("Setting frame to scan_frame") + + self.scan_frame.is_current_scene = True + self.set_frame(self.scan_frame) + self.scan_frame.scan_QR_code(self.master_window) + + ################################################# - self.scan_frame.is_current_scene = True - self.set_frame(self.scan_frame) - self.scan_frame.scan_QR_code(self.master_window) + def set_frame_scan_many_frame(self): - logging.debug("GUIWindow: The frame has been set to scan_frame." + logger.info("Setting frame to scan_many_frame") + + self.scan_many_frame.is_current_scene = True + self.set_frame(self.scan_many_frame) + self.scan_many_frame.scan_QR_code(self.master_window) ################################################# def set_frame_splash_frame(self): + logger.info("Setting frame to splash_frame") + self.set_frame(self.splash_frame) # Disables all buttons when the splash frame is the only frame self.sidebar.disable_all_btns() - logging.debug("GUIWindow: The frame has been set to splash_frame." - ################################################# - def set_frame_test_summary(self): - self.test_summary_frame.update_frame() - self.check_if_test_passed() - self.set_frame(self.test_summary_frame) + def set_frame_postscan(self): + + logger.info("Setting frame to post_scan_frame") - logging.debug("GUIWindow: The frame has been set to test_summary_frame.") + self.post_scan_frame.update_frame() + self.set_frame(self.post_scan_frame) ################################################# - def set_frame_test1(self): - self.test1_frame.update_frame(self) - self.set_frame(self.test1_frame) + # Used to be the visual inspection method - logging.debug("GUIWindow: The frame has been set to test1_frame.") ################################################# - def set_frame_test2(self): - self.test2_frame.update_frame(self) - self.set_frame(self.test2_frame) + def scan_frame_progress(self): + self.go_to_next_test() + - logging.debug("GUIWindow: The frame has been set to test2_frame.") ################################################# - def set_frame_test3(self): - self.test3_frame.update_frame(self) - self.set_frame(self.test3_frame) - logging.debug("GUIWindow: The frame has been set to test3_frame.") + def set_frame_test_summary(self): + + logger.info("Setting frame to test_summary_frame") + + self.test_summary_frame.update_frame() + self.set_frame(self.test_summary_frame) + ################################################# - def set_frame_test4(self): - self.test4_frame.update_frame(self) - self.set_frame(self.test4_frame) + def set_frame_test(self, test_idx): + + logger.info("Setting frame to test {}".format(test_idx)) + + self.current_test_index = test_idx + self.data_holder.setTestIdx(test_idx) - logging.debug("GUIWindow: The frame has been set to test4_frame.") + selected_test_frame = self.test_frames[test_idx] + + self.set_frame(selected_test_frame) + + ################################################# + # Navigation for the Thermal Testing GUI + + def set_frame_thermal_begin(self): + logger.info("Setting frame to thermal_begin_frame") + self.set_frame(self.thermal_begin_frame) + + def set_frame_thermal_config(self): + logger.info("Setting frame to thermal_config_frame") + self.set_frame(self.thermal_config_frame) + + def set_frame_thermal_final_results(self): + logger.info("Setting frame to thermal_final_results_frame.") + self.set_frame(self.thermal_final_results_frame) + # self.thermal_final_results_frame.send_REQ(self.master_window, self.queue, self) + self.thermal_final_results_frame.send_REQ(self) + + def set_frame_thermal_test_in_progress(self): + logger.info("Setting frame to thermal_test_in_progress_frame.") + self.thermal_in_progress_frame.update_frame(self) + self.thermal_in_progress_frame.update_timer() + self.set_frame(self.thermal_in_progress_frame) + + def set_frame_thermal_setup_results(self): + logger.info("Setting frame to thermal_setup_results_frame.") + self.thermal_setup_results_frame.update_frame(self) + self.set_frame(self.thermal_setup_results_frame) + self.thermal_setup_results_frame.begin_update(self.master_window, self.queue, self) + ################################################# def set_frame_test_in_progress(self, queue): + + logger.info("Setting frame to test_in_progress_frame") + + self.test_in_progress_frame.remove_stop_txt() self.set_frame(self.test_in_progress_frame) + self.sidebar.disable_all_btns() - self.test_in_progress_frame.begin_update(self.master_window, queue) + passed = self.test_in_progress_frame.begin_update(self.master_window, queue, self) self.go_to_next_test() - logging.debug("GUIWindow: The frame has been set to test_in_progress_frame.") ################################################# - def check_if_test_passed(self): - logging.debug("GUIWindow: The method check_if_test_passed(self) has been called. This method is empty.") - ################################################# + def return_to_current_test(self): + self.current_test_index -= 1 + self.running_all_idx -= 1 + self.set_frame_test(self.current_test_index) + + self.data_holder.setTestIdx(self.current_test_index) def go_to_next_test(self): - - # Array of potentially uncompleted tests - test_completed_list = [ - self.data_holder.test1_completed, - self.data_holder.test2_completed, - self.data_holder.test3_completed, - self.data_holder.test4_completed - ] - + + # Updates the sidebar every time the frame is set + self.sidebar.clean_up_btns() + self.sidebar.update_sidebar(self) - test_incomplete = False + total_num_tests = self.data_holder.total_test_num + num_digital = self.data_holder.getNumTest() + #num_physical = self.data_holder.getNumPhysicalTest() - logging.info("GUIWindow: Testing which tests have been completed.") - # Checks tells the function which frame to set based on what frame is currently up - for index, test in enumerate(test_completed_list): - if test == True: - pass + if not self.run_all_tests_bool: + if (self.current_test_index < total_num_tests): + cur_name = self.gui_cfg.getTests()[self.current_test_index]['name'] + logger.debug('Current test is: %s' % cur_name) + self.set_frame_test(self.current_test_index) + self.current_test_index += 1 else: - test_incomplete = True - if (index ==0): - self.set_frame_test1() - elif (index == 1): - self.set_frame_test2() - elif (index == 2): - self.set_frame_test3() - elif (index == 3): - self.set_frame_test4() - break + self.set_frame_test_summary() + + else: + self.running_all_idx += 1 + if (self.running_all_idx < total_num_tests): + self.current_test_index += 1 + self.data_holder.setTestIdx(self.running_all_idx) + + gui_cfg = self.data_holder.getGUIcfg() - # Tests if all the tests have been completed - # if true, brings user to Test Summary Frame rather than the next test - if (not test_incomplete): - self.set_frame_test_summary() + cur_name = gui_cfg.getTests()[self.current_test_index]['name'] + logger.debug('Current test is: %s' % cur_name) + + test_client = REQClient(self.gui_cfg, cur_name.strip().replace(" ", ""), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID'], self.conn_trigger) + #test_client = REQClient(gui_cfg, 'test{}'.format(self.running_all_idx), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID'], self.conn_trigger) + self.set_frame_test_in_progress(self.queue) + + + else: + self.run_all_tests_bool = False + self.set_frame_test_summary() + + + def reset_board(self): + self.current_test_index = 0 + self.set_frame_scan_frame() - - self.check_if_test_passed() ################################################# # Called to change the frame to the argument _frame def set_frame(self, _frame): - + + #Binding return button to next frame + try: + bind_func = _frame.get_submit_action() + _frame.bind_all("", lambda event: bind_func(_frame.get_parent())) + except: + logger.warning("No bind function for " + str(_frame)) + + try: + bind_func_2 = _frame.run_all_action + _frame.bind_all("", lambda event: bind_func_2(_frame.get_parent())) + except Exception as e: + pass + + # Updates the sidebar every time the frame is set + self.sidebar.clean_up_btns() self.sidebar.update_sidebar(self) # If frame is test_in_progress frame, disable the close program button @@ -295,38 +567,27 @@ def set_frame(self, _frame): ############################################################################# # End Button Visibility Code # ############################################################################# - - logging.debug("GUIWindow: Sidebar buttons have been updated.") - - # Brings up the test_failed popup if the test is false, continues on if not - if _frame == self.test2_frame: - if self.data_holder.test1_pass == False: - TestFailedPopup(self, self.test1_frame) - if _frame == self.test3_frame: - if self.data_holder.test2_pass == False: - TestFailedPopup(self, self.test2_frame) - if _frame == self.test4_frame: - if self.data_holder.test3_pass == False: - TestFailedPopup(self, self.test3_frame) - if _frame == self.test_summary_frame: - if self.data_holder.test4_pass == False: - TestFailedPopup(self, self.test4_frame) # Raises the passed in frame to be the current frame _frame.tkraise() - logging.info("GUIWindow: The frame has been raised.") + self.set_help_text(_frame) + + self.master_frame.update() + self.master_window.update() ################################################# def unable_to_exit(self): - logging.debug("GUIWindow: The user tried to exit during a test in progress.") + logger.warning("The user tried to exit during a test in progress.") # Creates a popup to confirm whether or not to exit out of the window self.popup = tk.Toplevel() - # popup.wm_attributes('-toolwindow', 'True') + self.popup.create_style(self) + + # popup.wm_attributes('-toolwindow', 'True') self.popup.title("Exit Window") self.popup.geometry("300x150+500+300") self.popup.grab_set() @@ -348,41 +609,250 @@ def unable_to_exit(self): btn_ok = tk.Button( frm_popup, width = 12, - height = 2, text = "OK", font = ('Arial', 12), - relief = tk.RAISED, command = lambda: self.destroy_popup() ) btn_ok.grid(column = 0, row = 1, columnspan=2) + + ################################################# + # Creates the popup window to show the text for current scene + def help_popup(self, current_window): + + logger.info("The user requested a help window") + logger.info("Opening help menu for {}".format(type(current_window))) + + # Creates a popup to confirm whether or not to exit out of the window + self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') + self.popup.title("Help Window") + self.popup.geometry("650x650+500+300") + + # "grab_set()" makes sure that you cannot do anything else while this window is open + #self.popup.grab_set() + + self.mycanvas = tk.Canvas(self.popup, background="#808080", width=630, height =650) + self.viewingFrame = tk.Frame(self.mycanvas, width = 200, height = 200) + self.scroller = tk.Scrollbar(self.popup, orient="vertical", command=self.mycanvas.yview) + self.mycanvas.configure(yscrollcommand=self.scroller.set) + + + + self.canvas_window = self.mycanvas.create_window((4,4), window=self.viewingFrame, anchor='nw', tags="self.viewingFrame") + + + self.viewingFrame.bind("", self.onFrameConfigure) + self.mycanvas.bind("", self.onCanvasConfigure) + + self.viewingFrame.bind('', self.onEnter) + self.viewingFrame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + + self.set_help_text(current_window) + + # Creates frame in the new window + #frm_popup = tk.Frame(self.mycanvas) + #frm_popup.pack() + + + # Creates label in the frame + lbl_popup = tk.Label( + self.viewingFrame, + textvariable = self.label_text, + font = ('Arial', 11) + ) + lbl_popup.grid(column = 0, row = 0, pady = 5, padx = 50) + + + self.mycanvas.pack(side="right") + self.scroller.pack(side="left", fill="both", expand=True) + + + #btn_ok = tk.Button( + # frm_popup, + # width = 8, + # height = 2, + # text = "OK", + # font = ('Arial', 8), + # relief = tk.RAISED, + # command = lambda: self.destroy_popup() + #) + #btn_ok.grid(column = 0, row = 0) + + + ############################################# + + + def set_help_text(self, current_window): + + + # Help from file + file = open("{}/HGCAL_Help/{}_help.txt".format(PythonFiles.__path__[0], type(current_window).__name__)) + self.all_text = file.read() + + #print("\nall_text: ", self.all_text) + + self.label_text.set(self.all_text) + + ################################################# + + def onFrameConfigure(self, event): + '''Reset the scroll region to encompass the inner frame''' + self.mycanvas.configure(scrollregion=self.mycanvas.bbox("all")) #whenever the size of the frame changes, alter the scroll region respectively. + + def onCanvasConfigure(self, event): + '''Reset the canvas window to encompass inner frame when required''' + canvas_width = event.width + self.mycanvas.itemconfig(self, width = canvas_width) #whenever the size of the canvas changes alter the window region respectively. + + + ################################################# + ################################################# + + + def onMouseWheel(self, event): # cross platform scroll wheel event + if event.num == 4: + self.mycanvas.yview_scroll( -1, "units" ) + elif event.num == 5: + self.mycanvas.yview_scroll( 1, "units" ) + + def onEnter(self, event): # bind wheel events when the cursor enters the control + self.mycanvas.bind_all("", self.onMouseWheel) + self.mycanvas.bind_all("", self.onMouseWheel) + + def onLeave(self, event): # unbind wheel events when the cursorl leaves the control + self.mycanvas.unbind_all("") + self.mycanvas.unbind_all("") + + + + + ################################################# + + def report_bug(self, current_window): + url = 'https://github.com/UMN-CMS/HGCALTestGUI/issues' + webbrowser.open(url, new = 1) + + ################################################# + + # Called when a test is skipped because it has been previously passed + def completed_window_popup(self): + + self.completed_window_alive = True + + # Creates a popup to inform user about the passing of a test + self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') + self.popup.title("Information Window") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + + # Creates frame in the new window + frm_popup = tk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = tk.Label( + frm_popup, + text = "A test has been skipped because it\n has been previously passed.", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, pady = 25) + + # Creates yes and no buttons for exiting + btn_okay = tk.Button( + frm_popup, + width = 12, + height = 2, + text = "OK", + relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.destroy_popup() + ) + btn_okay.grid(column = 0, row = 1) + + def test_error_popup(self, message): + + self.completed_window_alive = True + + # Creates a popup to inform user about the passing of a test + self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') + self.popup.title("Error Window") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + + # Creates frame in the new window + frm_popup = tk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = tk.Label( + frm_popup, + text = message, + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, pady = 25) + + # Creates yes and no buttons for exiting + btn_okay = tk.Button( + frm_popup, + width = 12, + height = 2, + text = "OK", + relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.destroy_popup() + ) + btn_okay.grid(column = 0, row = 1) + + + # Called when the no button is pressed to destroy popup and return you to the main window def destroy_popup(self): - self.popup.destroy() - logging.debug("GUIWindow: The popup has been destroyed.") + try: + self.popup.destroy() + self.completed_window_alive = False + except: + logger.error("The popup was unable to be destroyed.") + # New function for clicking on the exit button def exit_function(self): # Creates a popup to confirm whether or not to exit out of the window self.popup = tk.Toplevel() + # popup.wm_attributes('-toolwindow', 'True') self.popup.title("Exit Window") - self.popup.geometry("300x150+500+300") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) self.popup.grab_set() - + # Creates frame in the new window - frm_popup = tk.Frame(self.popup) - frm_popup.pack() + frm_popup = tk.Frame(self.popup, width = 300, height = 200) + frm_popup.grid() + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + frm_popup.grid_rowconfigure(2, weight=1) # Creates label in the frame lbl_popup = tk.Label( frm_popup, text = "Are you sure you would like to exit?", - font = ('Arial', 13) + font = ('Arial', 14) ) lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) @@ -390,10 +860,7 @@ def exit_function(self): btn_yes = tk.Button( frm_popup, width = 12, - height = 2, text = "Yes", - relief = tk.RAISED, - font = ('Arial', 12), command = lambda: self.destroy_function() ) btn_yes.grid(column = 0, row = 1) @@ -401,47 +868,49 @@ def exit_function(self): btn_no = tk.Button( frm_popup, width = 12, - height = 2, text = "No", - relief = tk.RAISED, - font = ('Arial', 12), command = lambda: self.destroy_popup() ) btn_no.grid(column = 1, row = 1) + ################################################# + # Function for stopping tests gracefully + def stop_tests(self): + #self.sidebar.disable_all_btns() + self.run_all_tests_bool = False # Called when the yes button is pressed to destroy both windows def destroy_function(self): - #self.master_window.eval('::ttk::CancelRepeat') - - logging.info("GUIWindow: Exiting the GUI.") + try: + logger.info("Exiting the GUI.") - self.master_window.update() - self.popup.update() - - #popup.quit() - #self.master_window.quit() + self.master_window.update() + self.popup.update() - if self.scan_frame.is_current_scene == True: - self.test_in_progress_frame.close_prgbar() + #if self.scan_frame.is_current_scene == True: + #self.test_in_progress_frame.close_prgbar() self.scan_frame.kill_processes() + self.admin_scan_frame.kill_processes() - # Destroys the popup and master window - self.popup.destroy() - self.popup.quit() + # Destroys the popup and master window + self.popup.destroy() + self.popup.quit() - self.master_window.destroy() - self.master_window.quit() + self.master_window.destroy() + self.master_window.quit() - # Ensures the application closes with the exit button - #exit_main() + logger.info("The application has exited successfully.") + except Exception as e: + logger.exception(e) + logger.error("The application has failed to close.") + if self.retry_attempt == False: + logger.info("Retrying...") + self.retry_attempt = True - print("Leaving tester. Goodbye!") ################################################# - ################################################################################# diff --git a/PythonFiles/HGCAL_Help/AddUserScene_help.txt b/PythonFiles/HGCAL_Help/AddUserScene_help.txt new file mode 100644 index 00000000..a306b4cc --- /dev/null +++ b/PythonFiles/HGCAL_Help/AddUserScene_help.txt @@ -0,0 +1,40 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Add User Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Add User Scene can only be accessed via the Login Scene's "Add User" + button. This scene is responsible for adding another user's name into the + database so that the Login Scene's next call to the database will have the + new user's information. + + To add a user, type the user's name into the top text field. Be careful, + this is a case-sensitive field. Then, type in the administrative password + below. This is also a case-sensitive field. After the information has been + entered, press the "Submit" button. This will bring you back to the Login + Scene. + + + Note: If you add a user name and it does not show up on the Login Scene, + try adding the user again. There is a chance that you have entered + the password incorrectly. If you are sure your credentials are + correct, there might be an issue setting/accessing the data stored + in the database. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/AdminScanScene_help.txt b/PythonFiles/HGCAL_Help/AdminScanScene_help.txt new file mode 100644 index 00000000..34384fc4 --- /dev/null +++ b/PythonFiles/HGCAL_Help/AdminScanScene_help.txt @@ -0,0 +1,62 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Scan Scene is displayed after the Login Scene. It also marks the + beginning of testing a new board (either wagon or engine). This scene + is responsible for gathering the serial identification number, which + is often displayed directly on the board via a QR code. + + The submit button (which leads to the next scene) is grayed out until + the serial id box has been filled. This box can be filled in two + different ways. + + The most efficient way to fill the text field is to + scan the box using an EventListener scanner attached to the computer. + The scanner should be attached to the computer that is actively + running the GUI. After successfully scanning a QR code, the scanner + should beep, and the text should be filled into the field. Notice that + the text field will become grayed out after information has been + scanned into it. After scanning, if you would like to change the + serial number entered, you will need to press the "Rescan" button to + reactivate the scanner. + + The scanner runs on a completely separate process from the GUI, which + can cause issues with sections of code not completing correctly. In + some cases, the scanner may be stuck in a constant state of searching + for QR code information. If this is the case, be sure to press the + button that says "Report Bug" and report the issue. + + If the physical scanner is not available, you are also able to manually + enter the serial identification into the box. Note that this will not + require pressing the "Rescan" button nor will it gray out the text field. + + After the entry of the serial ID matches the board that you would like + to test, select the "Submit" button in order to move onto the next scene. + Before moving to the next scene, the GUI will request more information + about your board by sending the serial ID to the database. If the serial + ID is recognized as invalid, errors may occur within the GUI. + + + Note: If the serial ID is recognized and the board has been tested + before, the GUI may skip some of the previously passed tests + in order to save time. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/AdminScene_help.txt b/PythonFiles/HGCAL_Help/AdminScene_help.txt new file mode 100644 index 00000000..60b98b2a --- /dev/null +++ b/PythonFiles/HGCAL_Help/AdminScene_help.txt @@ -0,0 +1,22 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Admin Scene + + Date Updated: 7/02/24 + + +---------------------------------------------------------------------------------------- + + The Admin Scene gives access to admin tools. + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/LoginScene_help.txt b/PythonFiles/HGCAL_Help/LoginScene_help.txt new file mode 100644 index 00000000..27559a93 --- /dev/null +++ b/PythonFiles/HGCAL_Help/LoginScene_help.txt @@ -0,0 +1,36 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Login Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Login Scene is displayed directly after the Splash Scene as well as + via the "logout" buttons on many scenes. The purpose of this scene is to + have the user log into the system using the credentials stored on the + web database. When this scene is shown, there is a call to the web + database to grab the login options. + + Users must select an option from the list of users provided in the drop- + down box. After selecting the desired user's name, the submit button will + bring you to the next scene, which is the Scan Scene. + + If there is not an option for a specific user, you can select + the "Add User" button to bring you to the Add User Scene. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/PostScanScene_help.txt b/PythonFiles/HGCAL_Help/PostScanScene_help.txt new file mode 100644 index 00000000..f6d1ab51 --- /dev/null +++ b/PythonFiles/HGCAL_Help/PostScanScene_help.txt @@ -0,0 +1,25 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Summary Scene + + Date Updated: 9/26/23 + + +---------------------------------------------------------------------------------------- + + The Scan Summary Scene gives information about the board: whether it has + been checked in, whether it has had tests run on it, etc. Once you are sure + the information displayed is correct, click Proceed. + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/ScanManyScene_help.txt b/PythonFiles/HGCAL_Help/ScanManyScene_help.txt new file mode 100644 index 00000000..5e32bce7 --- /dev/null +++ b/PythonFiles/HGCAL_Help/ScanManyScene_help.txt @@ -0,0 +1 @@ +Hi there :) Just scan diff --git a/PythonFiles/HGCAL_Help/ScanScene_help.txt b/PythonFiles/HGCAL_Help/ScanScene_help.txt new file mode 100644 index 00000000..34384fc4 --- /dev/null +++ b/PythonFiles/HGCAL_Help/ScanScene_help.txt @@ -0,0 +1,62 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Scan Scene is displayed after the Login Scene. It also marks the + beginning of testing a new board (either wagon or engine). This scene + is responsible for gathering the serial identification number, which + is often displayed directly on the board via a QR code. + + The submit button (which leads to the next scene) is grayed out until + the serial id box has been filled. This box can be filled in two + different ways. + + The most efficient way to fill the text field is to + scan the box using an EventListener scanner attached to the computer. + The scanner should be attached to the computer that is actively + running the GUI. After successfully scanning a QR code, the scanner + should beep, and the text should be filled into the field. Notice that + the text field will become grayed out after information has been + scanned into it. After scanning, if you would like to change the + serial number entered, you will need to press the "Rescan" button to + reactivate the scanner. + + The scanner runs on a completely separate process from the GUI, which + can cause issues with sections of code not completing correctly. In + some cases, the scanner may be stuck in a constant state of searching + for QR code information. If this is the case, be sure to press the + button that says "Report Bug" and report the issue. + + If the physical scanner is not available, you are also able to manually + enter the serial identification into the box. Note that this will not + require pressing the "Rescan" button nor will it gray out the text field. + + After the entry of the serial ID matches the board that you would like + to test, select the "Submit" button in order to move onto the next scene. + Before moving to the next scene, the GUI will request more information + about your board by sending the serial ID to the database. If the serial + ID is recognized as invalid, errors may occur within the GUI. + + + Note: If the serial ID is recognized and the board has been tested + before, the GUI may skip some of the previously passed tests + in order to save time. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/SplashScene_help.txt b/PythonFiles/HGCAL_Help/SplashScene_help.txt new file mode 100644 index 00000000..6a3c91a7 --- /dev/null +++ b/PythonFiles/HGCAL_Help/SplashScene_help.txt @@ -0,0 +1,36 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Splash Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Splash Scene is displayed when the graphical user interface (GUI) is first + ran. The information provides credit to the students largely responsible + for the creation of this interface. The three students listed, along with + their home institutions are as follows: + + - Bryan Crossman (University of Minnesota) + - Andrew Kirzeder (Bethel University) + - Garrett Schindler (Bethel University) + + This scene should only be displayed for a short period of time before + moving on to the next scene. The next scene should be the Login Scene. + No buttons are required to be pressed on this screen. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt b/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt new file mode 100644 index 00000000..7b454595 --- /dev/null +++ b/PythonFiles/HGCAL_Help/TestInProgressScene_help.txt @@ -0,0 +1,53 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test In Progress Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Test In Progress Scene is displayed while you are running a test on + the testing machine. When working correctly, the test machine will send + dialog back to the GUI describing the current testing process. This type + of logging is run through a connection queue. + + If the GUI does not receive an initial response from the test stand after + 10 seconds, the GUI will throw a timeout error (popup window). This error + message will say "TestInProgressScene: Process timed out after 10 seconds". + Upon pressing the "OK" button, you will be brought back to the Login Scene + where you can restart the process. + + If the previous error persists for more than one occurance, you will need + to check the status of the REPServer that has been launched on the test + stand. If the server is not running on the test stand, the GUI will never + be able to receive a response, therefore always throwing this error. + + The GUI cannot be closed while the Test In Progress Scene is on the + screen. If you gry to quit the application by clicking the "X" in the top, + right corner, a popup window will show saying that "You cannot quit the + application during a test!". This is to prevent corruption of test data. + + There is a "Stop" button in the middle of the application that tries to + stop the test. If able to successfully stop the test, the GUI will go to + the next available test. If there are no more available tests, then + the next scene will be the Test Summary Scene. + + If the test has been completed successfully and the results have been + received by the GUI, then the GUI will automatically go to the next scene. + The next scene will either be the next Test Scene (if available) or will + be the Test Summary Scene (if the last test has been finished). + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/TestScene_help.txt b/PythonFiles/HGCAL_Help/TestScene_help.txt new file mode 100644 index 00000000..35f67ad9 --- /dev/null +++ b/PythonFiles/HGCAL_Help/TestScene_help.txt @@ -0,0 +1,45 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + + The Test Scene is a very important scene, as it is the last scene you will + see before a test is run. It is important to ensure that all of the + information shown on this screen is correct before proceeding. + + There are three boxes of information displayed on this screen: "Tester", + "Serial Number", and "Current Test". The first should display your name, + the second should display the serial ID of the board that you would like + to test (make sure that it is plugged into the test stand at this point), + and the third should display the name of the test that is going to be run. + + If any of this information is incorrect, then you will need to proceed to + the previous scenes in order to change this information. If your username + is incorrect, you will need to press the "Logout" button to return to the + Login Scene. If the board serial number is incorrect or if you would like + to switch which board you are testing, you will need to press the "Change + Board" button to go back to the Scan Scene. + + If all of the information looks correct, pressing the "Confirm" button + will send you to the Test In Progress Scene. This button will also tell + the testing machine to begin the test (which test is specified by the name + listed under the "Current Test"). + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/TestSummaryScene_help.txt b/PythonFiles/HGCAL_Help/TestSummaryScene_help.txt new file mode 100644 index 00000000..04cf6d44 --- /dev/null +++ b/PythonFiles/HGCAL_Help/TestSummaryScene_help.txt @@ -0,0 +1,50 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Test Summary Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Test Summary Scene is responsible for showing all of the results that + the GUI has gathered. The information is stored in a DataHolder object + while it is gathered throughout the process. The Test Summary Scene + displays the attributes of the DataHolder object to the user to ensure + that all of the information has been stored correctly. + + The top of the screen should display the tester's name as well as the + serial identification number for the board that is being tested. + + Each of the tests that are listed for the board are displayed below. The + GUI keeps track of whether or not certain tests have been completed. + The information displayed for each test is the "Test Name", "Test Status", + and "Pass/Fail". These display which type of test, whether or not it has + been run, and whether or not it has passed the test. Notice that a pass + is signified by a green checkmark and a fail is signified by a red x. + + To the right of the displayed information are two buttons; "More Info" and + "Retest". The "More Info" button will open a popup window with JSON + information that the tester has sent back to the GUI. If you want to know + more about the specific test results, press this button. The "Retest" + button will send you back to the Test Scene for the designated test. + Upon finishing this retest, the GUI will send you back to the Test + Summary Scene. + + Upon reaching the Test Summary Scene, the DataHolder object is uploaded to + the database. When you complete a retest for a specified board, the + information stored in the database for that board will be updated. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/TesterComponentScene_help.txt b/PythonFiles/HGCAL_Help/TesterComponentScene_help.txt new file mode 100644 index 00000000..34384fc4 --- /dev/null +++ b/PythonFiles/HGCAL_Help/TesterComponentScene_help.txt @@ -0,0 +1,62 @@ +---------------------------------------------------------------------------------------- + + HGCAL GUI HELP + +---------------------------------------------------------------------------------------- + + + Scene: Scan Scene + + Date Updated: 6/16/23 + + +---------------------------------------------------------------------------------------- + + The Scan Scene is displayed after the Login Scene. It also marks the + beginning of testing a new board (either wagon or engine). This scene + is responsible for gathering the serial identification number, which + is often displayed directly on the board via a QR code. + + The submit button (which leads to the next scene) is grayed out until + the serial id box has been filled. This box can be filled in two + different ways. + + The most efficient way to fill the text field is to + scan the box using an EventListener scanner attached to the computer. + The scanner should be attached to the computer that is actively + running the GUI. After successfully scanning a QR code, the scanner + should beep, and the text should be filled into the field. Notice that + the text field will become grayed out after information has been + scanned into it. After scanning, if you would like to change the + serial number entered, you will need to press the "Rescan" button to + reactivate the scanner. + + The scanner runs on a completely separate process from the GUI, which + can cause issues with sections of code not completing correctly. In + some cases, the scanner may be stuck in a constant state of searching + for QR code information. If this is the case, be sure to press the + button that says "Report Bug" and report the issue. + + If the physical scanner is not available, you are also able to manually + enter the serial identification into the box. Note that this will not + require pressing the "Rescan" button nor will it gray out the text field. + + After the entry of the serial ID matches the board that you would like + to test, select the "Submit" button in order to move onto the next scene. + Before moving to the next scene, the GUI will request more information + about your board by sending the serial ID to the database. If the serial + ID is recognized as invalid, errors may occur within the GUI. + + + Note: If the serial ID is recognized and the board has been tested + before, the GUI may skip some of the previously passed tests + in order to save time. + + + + + + +--------------------------------------------------------------------------------------- + + diff --git a/PythonFiles/HGCAL_Help/ThermalTestBeginScene_help.txt b/PythonFiles/HGCAL_Help/ThermalTestBeginScene_help.txt new file mode 100644 index 00000000..b901cdb8 --- /dev/null +++ b/PythonFiles/HGCAL_Help/ThermalTestBeginScene_help.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/PythonFiles/HGCAL_Help/ThermalTestConfigScene_help.txt b/PythonFiles/HGCAL_Help/ThermalTestConfigScene_help.txt new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/HGCAL_Help/ThermalTestFinalResultsScene_help.txt b/PythonFiles/HGCAL_Help/ThermalTestFinalResultsScene_help.txt new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/HGCAL_Help/ThermalTestInProgressScene_help.txt b/PythonFiles/HGCAL_Help/ThermalTestInProgressScene_help.txt new file mode 100644 index 00000000..af107469 --- /dev/null +++ b/PythonFiles/HGCAL_Help/ThermalTestInProgressScene_help.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/PythonFiles/HGCAL_Help/ThermalTestSetupResultsScene_help.txt b/PythonFiles/HGCAL_Help/ThermalTestSetupResultsScene_help.txt new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/Images/EngineExample.png b/PythonFiles/Images/EngineExample.png new file mode 100644 index 00000000..a6e5ce9c Binary files /dev/null and b/PythonFiles/Images/EngineExample.png differ diff --git a/PythonFiles/Images/WagonExample.png b/PythonFiles/Images/WagonExample.png new file mode 100644 index 00000000..393cfbd8 Binary files /dev/null and b/PythonFiles/Images/WagonExample.png differ diff --git a/PythonFiles/Images/__init__.py b/PythonFiles/Images/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/Images/not_yet_run.png b/PythonFiles/Images/not_yet_run.png new file mode 100644 index 00000000..25ac3291 Binary files /dev/null and b/PythonFiles/Images/not_yet_run.png differ diff --git a/PythonFiles/Images/spinner.gif b/PythonFiles/Images/spinner.gif new file mode 100644 index 00000000..aa0e006f Binary files /dev/null and b/PythonFiles/Images/spinner.gif differ diff --git a/PythonFiles/JSONFiles/DummyJSONTest.JSON b/PythonFiles/JSONFiles/DummyJSONTest.JSON deleted file mode 100644 index bcd432af..00000000 --- a/PythonFiles/JSONFiles/DummyJSONTest.JSON +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "IIC Check", - "board_sn": 32031100299999, - "tester": "Bryan", - "pass": true, - "data": { - "num_iic_checks": 10000, - "num_iic_correct": 10000 - } -} \ No newline at end of file diff --git a/PythonFiles/JSONFiles/GarrettJSONTest.JSON b/PythonFiles/JSONFiles/GarrettJSONTest.JSON deleted file mode 100644 index ae452658..00000000 --- a/PythonFiles/JSONFiles/GarrettJSONTest.JSON +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Battery Test", - "board_sn": 32031100222999, - "tester": "Garrett", - "pass": true, - "data": { - "Voltage": 9, - "Resistance": 100, - "Current": 0.09 - } -} \ No newline at end of file diff --git a/PythonFiles/JSONFiles/__init__.py b/PythonFiles/JSONFiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/JSONFiles/testingJSON.JSON b/PythonFiles/JSONFiles/testingJSON.JSON deleted file mode 100644 index ae452658..00000000 --- a/PythonFiles/JSONFiles/testingJSON.JSON +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Battery Test", - "board_sn": 32031100222999, - "tester": "Garrett", - "pass": true, - "data": { - "Voltage": 9, - "Resistance": 100, - "Current": 0.09 - } -} \ No newline at end of file diff --git a/PythonFiles/Scanner/__init__.py b/PythonFiles/Scanner/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/Scanner/bin/__init__.py b/PythonFiles/Scanner/bin/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/Scanner/bin/runScanner b/PythonFiles/Scanner/bin/runScanner deleted file mode 100755 index 025e6a8d..00000000 Binary files a/PythonFiles/Scanner/bin/runScanner and /dev/null differ diff --git a/PythonFiles/Scanner/python/get_barcodes.py b/PythonFiles/Scanner/python/get_barcodes.py index f7f57867..23b34d96 100644 --- a/PythonFiles/Scanner/python/get_barcodes.py +++ b/PythonFiles/Scanner/python/get_barcodes.py @@ -2,7 +2,12 @@ import time import signal import ctypes +from pathlib import Path +#import PythonFiles libc = ctypes.CDLL("libc.so.6") +import logging + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scanner.python.get_barcodes') from multiprocessing import Process, Manager, Pipe @@ -26,8 +31,8 @@ def callable(): return callable def scan(): - proc = subprocess.Popen('/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/Scanner/bin/runScanner', stdout=subprocess.PIPE, preexec_fn=set_pdeathsig(signal.SIGTERM)) - print("Starting scanner") + proc = subprocess.Popen(Path(__file__).parent.parent / 'bin/runScanner', stdout=subprocess.PIPE, preexec_fn=set_pdeathsig(signal.SIGTERM)) + logger.info('Starting scanner') return proc #for line in proc.stdout: # if line is not None: @@ -37,7 +42,6 @@ def scan(): def listen(serial, proc): for line in proc.stdout: if line is not None: - print(line.strip().decode('utf-8')) serial.append(line.strip().decode('utf-8')) return @@ -60,7 +64,7 @@ def run_scanner(): listener.join() - print(parse_xml(serial[0])) + logger.info('Scanner: %s' % parse_xml(serial[0])) if __name__=="__main__": run_scanner() diff --git a/PythonFiles/Scanner/src/EventListener.o b/PythonFiles/Scanner/src/EventListener.o deleted file mode 100644 index f7aad93d..00000000 Binary files a/PythonFiles/Scanner/src/EventListener.o and /dev/null differ diff --git a/PythonFiles/Scanner/src/main.o b/PythonFiles/Scanner/src/main.o deleted file mode 100644 index 1b4f23c7..00000000 Binary files a/PythonFiles/Scanner/src/main.o and /dev/null differ diff --git a/PythonFiles/Scenes/AddUserScene.py b/PythonFiles/Scenes/AddUserScene.py new file mode 100644 index 00000000..17f5427f --- /dev/null +++ b/PythonFiles/Scenes/AddUserScene.py @@ -0,0 +1,217 @@ + +################################################################################# + +# importing necessary modules +import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.AddUserScene') + + +# Creates a class that is called by the GUIWindow. +# GUIWindow instantiates an object called add_user_scene. +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. + +class AddUserScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + super().__init__(master_frame, width=1300-213, height=800) + self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + # Creating the title for the window + lbl_title = ttk.Label( + self, + text="Add User", + font=('Arial', '24') + ) + lbl_title.pack(pady=(50,0)) + + # Creating entry box for new user's name + self.new_user_name = "" + self.user_entry = tk.Entry( + self, + textvariable= self.new_user_name, + font=('Arial', '15') + ) + self.user_entry.pack(pady=30) + + # Creating the title for the window + password_label = ttk.Label( + self, + text="Enter Admin Password", + font=('Arial', '20') + ) + password_label.pack(pady=(10,0)) + + # Creating entry box for new user's name + self.password = "" + self.user_password = tk.Entry( + self, + textvariable= self.password, + font=('Arial', '15'), + show = "*" + ) + self.user_password.pack(pady=30) + + # Creating the submit button + self.btn_submit = ttk.Button( + self, + text="Submit", + #padx = 50, + #pady = 10, + #relief=tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.pack() + + # Creating the cancel button + self.btn_submit = ttk.Button( + self, + text="Cancel", + #padx = 50, + #pady = 10, + #relief=tk.RAISED, + command= lambda: self.btn_cancel_action(parent) + ) + self.btn_submit.pack() + + # Forces frame to stay the size of the main_window + # rather than adjusting to the size of the widgets + self.pack_propagate(0) + + ################################################# + + # Creates the function for the submit button command + # @params "_parent" is also a parent like "parent", but it is a different "parent", + # passes in GUIWindow + def btn_submit_action(self, _parent): + + self.new_user_name = self.user_entry.get() + self.password = self.user_password.get() + + # Popup to confirm that a new user is added into the DB + cnfm_pop = ConfirmPopup(_parent, self.data_holder, self.new_user_name, self.password) + + + ################################################# + + def btn_cancel_action(self, _parent): + + _parent.set_frame_login_frame() + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + +################################################################################# + + +class ConfirmPopup(): + + ################################################# + + def __init__(self, parent, data_holder, new_user_name, password): + self.confirm_popup(data_holder, new_user_name, password) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder, new_user_name, password): + self.data_holder = data_holder + self.new_user_name = new_user_name + self.password = password + logger.info("Confirming that the user wants to add {}".format(self.new_user_name)) + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("New User Name") + self.popup.geometry("300x150+500+300") + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup) + frm_popup.pack() + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = " You are about to add {} as a user \n Are you sure? ".format(self.new_user_name), + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) + + # Creates retry and continue buttons + btn_retry = ttk.Button( + frm_popup, + width = 8, + height = 2, + text = "Cancel", + #relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.cancel_function() + ) + btn_retry.grid(column = 0, row = 1) + + btn_continue = ttk.Button( + frm_popup, + width = 8, + height = 2, + text = "Confirm", + #relief = tk.RAISED, + font = ('Arial', 12), + command = lambda: self.continue_function(self.parent) + ) + btn_continue.grid(column = 1, row = 1) + + + ################################################# + + # Called when the "cancel" button is selected + def cancel_function(self): + self.popup.destroy() + + ################################################# + + # Called to continue on in the testing procedure + def continue_function(self, _parent): + self.popup.destroy() + + # Adding a new user name to data_holder/DB + self.data_holder.add_new_user_name(self.new_user_name, self.password) + # Changes frame to scan_frame + _parent.set_frame_login_frame() + + + +################################################################################# diff --git a/PythonFiles/Scenes/AdminScanScene.py b/PythonFiles/Scenes/AdminScanScene.py new file mode 100644 index 00000000..4f40e046 --- /dev/null +++ b/PythonFiles/Scenes/AdminScanScene.py @@ -0,0 +1,505 @@ +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter.ttk as ttk +import tkinter as tk +import sys, time +from tkinter import * +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.AdminScanScene') + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class AdminScanScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + + self.data_holder = data_holder + + self.use_scanner = self.data_holder.get_use_scanner() + + self.is_current_scene = False + + self.EXIT_CODE = 0 + + self.master_frame = master_frame + + self.parent = parent + + super().__init__(self.master_frame, width=1300-213, height = 800) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent) + + self.create_style(parent) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + # can see more scanner documentation in the Visual Inspection GUI + def scan_QR_code(self, master_window): + + if self.use_scanner: + + self.ent_full.config(state = 'normal') + self.ent_full.delete(0,END) + self.master_window = master_window + self.hide_rescan_button() + sys.path.insert(1,'/home/hgcal/WagonTest/Scanner/python') + + from ..Scanner.python.get_barcodes import scan, listen, parse_xml + + manager = mp.Manager() + full_id = manager.list() + + self.ent_full.config(state = 'normal') + + self.scanner = scan() + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + while 1 > 0: + + try: + self.master_window.update() + except: + pass + if not len(full_id) == 0: + board = parse_xml(full_id[0]) + + self.listener.terminate() + self.scanner.terminate() + + self.ent_full.delete(0,END) + self.ent_full.insert(0, str(board)) + self.ent_full.config(state = 'disabled') + self.show_rescan_button() + break + + elif self.EXIT_CODE: + logger.info("Exit code received on AdminScanScene. Terminating processes.") + self.listener.terminate() + self.scanner.terminate() + logger.info("AdminScanScene processes terminated successfully.") + break + else: + time.sleep(.01) + + # Creates the GUI itself + def initialize_GUI(self, parent): + + # Create a photoimage object of the QR Code + + QR_image = Image.open("{}/Images/QRimage.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label = ttk.Label(self, image=QR_PhotoImage) + QR_label.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label.grid(column=1, row = 1, sticky='new') + + Scan_Board_Prompt_Frame = ttk.Frame(self, width = 1105, height = 650) + Scan_Board_Prompt_Frame.grid(column=0, row = 1, sticky='nsew') + + Button_Frame1 = ttk.Frame(self) + Button_Frame1.grid(column=1, row=0, sticky='ew') + + Button_Frame2 = ttk.Frame(self) + Button_Frame2.grid(column=1, row=2, sticky='ew') + + #resizing + Scan_Board_Prompt_Frame.grid_columnconfigure(0, weight=1) + Scan_Board_Prompt_Frame.grid_columnconfigure(1, weight=1) + QR_label.grid_columnconfigure(0, weight=1) + Button_Frame1.grid_columnconfigure(0, weight=1) + Button_Frame2.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(1, weight=1) + + # creates a Label Variable, different customization options + self.lbl_scan = ttk.Label( + master= Scan_Board_Prompt_Frame, + text = "Scan the QR Code on the Board", + font = ('Arial', 18) + ) + self.lbl_scan.grid(column=0, row=0, sticky='we') + + # Create a label to label the entry box + lbl_full = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Full ID: ", + font = ('Arial', 16) + ) + lbl_full.grid(column=0, row=2) + + # Entry for the full id to be displayed. Upon Scan, update and disable? + global ent_full + + # Creating intial value in entry box + self.user_text = tk.StringVar(self) + + # Creates an entry box + self.ent_full = tk.Entry( + Scan_Board_Prompt_Frame, + font = ('Arial', 16), + textvariable= self.user_text, + ) + self.ent_full.grid(column=0, row=3) + + + # Traces an input to show the submit button once text is inside the entry box + self.user_text.trace( + "w", + lambda name, + index, + mode, + sv=self.user_text: self.show_submit_button() + ) + + # Rescan button creation + self.btn_rescan = ttk.Button( + Scan_Board_Prompt_Frame, + text="Rescan", + #padx = 20, + #pady =10, + #relief = tk.RAISED, + command = lambda: self.scan_QR_code(self.master_frame) + ) + self.btn_rescan.grid(column=0, row=5, padx=10, pady=5) + + # Submit button creation + self.btn_submit = ttk.Button( + Scan_Board_Prompt_Frame, + text="Submit", + #padx = 20, + #pady = 10, + #relief = tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.grid(column=0, row=6, padx=10, pady=5) + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=0, row = 1) + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 1, row = 1, sticky= 'se') + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.grid(column=0, row=1, sticky='ne', padx=10, pady=10) + + # Creating the help button + + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.grid(column=0, row=0, sticky='ne', padx=10, pady=10) + + + def update_frame(self, parent, index): + self.ent_full.delete(0,END) + if self.data_holder.tester_type == 'Wagon': + self.index = list(self.data_holder.wagon_tester_info)[index] + self.lbl_scan['text'] = 'Scan the ' + self.index + if self.data_holder.tester_type == 'Engine': + self.index = list(self.data_holder.engine_tester_info)[index] + self.lbl_scan['text'] = 'Scan the ' + self.index + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + + # Function for the submit button + def btn_submit_action(self, _parent): + + self.EXIT_CODE = 1 + + if self.data_holder.tester_type == 'Wagon': + self.data_holder.wagon_tester_info[self.index] = self.ent_full.get() + if self.data_holder.tester_type == 'Engine': + self.data_holder.engine_tester_info[self.index] = self.ent_full.get() + + self.EXIT_CODE = 0 + + _parent.next_frame_admin_scan() + + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + # Function for the log out button + def btn_logout_action(self, _parent): + + self.EXIT_CODE = 1 + + if self.use_scanner: + self.listener.terminate() + self.scanner.terminate() + + # Send user back to login frame + _parent.set_frame_login_frame() + + self.EXIT_CODE = 0 + + ################################################# + + # Function to activate the submit button + def show_submit_button(self): + self.data_holder.decode_label(self.ent_full.get()) + self.btn_submit["state"] = "active" + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + + ################################################# + + # Function to disable to the submit button + def hide_submit_button(self): + self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to activate the rescan button + def show_rescan_button(self): + self.btn_rescan["state"] = "active" + + ################################################# + + # Function to disable to the rescan button + def hide_rescan_button(self): + self.btn_rescan["state"] = "disabled" + + ################################################# + + def kill_processes(self): + logger.info("Terminating scanner processes.") + try: + if self.use_scanner: + self.scanner.kill() + self.listener.terminate() + self.EXIT_CODE = 1 + except: + logger.info("Scanner processes could not be terminated.") + + +########################################################## + +class interposer_Popup(): + + ################################################# + + def __init__(self, parent, data_holder): + self.confirm_popup(data_holder) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder): + self.data_holder = data_holder + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Select Interposer Type") + self.popup.geometry("200x100+550+350") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Select Interposer Type", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2) + + # Creates retry and continue buttons + btn_east = ttk.Button( + frm_popup, + text = "East", + command = lambda: self.east_function(self.parent) + ) + btn_east.grid(column = 1, row = 1) + + btn_west = ttk.Button( + frm_popup, + text = "West", + command = lambda: self.west_function(self.parent) + ) + btn_west.grid(column = 0, row = 1) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + + + ################################################# + + def east_function(self, _parent): + self.popup.destroy() + self.data_holder.wagon_tester_info['interposer_type'] = 'East' + + ################################################# + + def west_function(self, _parent): + self.popup.destroy() + self.data_holder.wagon_tester_info['interposer_type'] = 'West' + + + +############################################################### + + +class finished_Popup(): + + ################################################# + + def __init__(self, parent, data_holder): + self.confirm_popup(data_holder) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder): + self.data_holder = data_holder + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Done") + self.popup.geometry("300x150+550+350") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Finished Scanning Tester Info", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2) + + # Creates retry and continue buttons + btn_ok = ttk.Button( + frm_popup, + text = "OK", + command = lambda: self.east_function(self.parent) + ) + btn_ok.grid(column = 1, row = 1) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + + + ################################################# + + def east_function(self, _parent): + self.popup.destroy() + + diff --git a/PythonFiles/Scenes/AdminScene.py b/PythonFiles/Scenes/AdminScene.py new file mode 100644 index 00000000..8bd533cc --- /dev/null +++ b/PythonFiles/Scenes/AdminScene.py @@ -0,0 +1,339 @@ + +################################################################################# + +# importing necessary modules +import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.AdminScene') + + +# Creates a class that is called by the GUIWindow. +# GUIWindow instantiates an object called add_user_scene. +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. + +class AdminScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + super().__init__(master_frame, width=1300-213, height=800) + self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + # Creating the title for the window + lbl_title = ttk.Label( + self, + text="Admin Tools", + font=('Arial', '24') + ) + lbl_title.pack(pady=(50,0)) + + # Creating the submit button + self.btn_submit = ttk.Button( + self, + text="Add User", + command= lambda: self.btn_add_user_action(parent) + ) + self.btn_submit.pack() + + # Creating the cancel button + self.btn_submit = ttk.Button( + self, + text="Specify Test Stand Info", + command= lambda: self.btn_teststand_action(parent) + ) + self.btn_submit.pack() + + self.btn_submit = ttk.Button( + self, + text="Add Tester Component Info", + command= lambda: self.btn_component_action(parent) + ) + self.btn_submit.pack() + + # Forces frame to stay the size of the main_window + # rather than adjusting to the size of the widgets + self.pack_propagate(0) + + ################################################# + + # Creates the function for the submit button command + # @params "_parent" is also a parent like "parent", but it is a different "parent", + # passes in GUIWindow + def btn_add_user_action(self, _parent): + _parent.set_frame_add_user_frame() + + ################################################# + + def btn_teststand_action(self, _parent): + popup1 = Popup1(_parent, self.data_holder) + + ################################################# + + def btn_component_action(self, _parent): + popup3 = Popup3(_parent, self.data_holder) + + ################################################# + +################################################################################# + + +class Popup1(): + + ################################################# + + def __init__(self, parent, data_holder): + self.confirm_popup(data_holder) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder): + self.data_holder = data_holder + logger.info("Teststand info is being specified.") + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Select Test Stand Type") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Select Test Stand Type", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2) + + # Creates retry and continue buttons + btn_wagon = ttk.Button( + frm_popup, + text = "Wagon Tester", + command = lambda: self.wagon_function(self.parent) + ) + btn_wagon.grid(column = 1, row = 1) + + btn_continue = ttk.Button( + frm_popup, + text = "Engine Tester", + command = lambda: self.engine_function(self.parent) + ) + btn_continue.grid(column = 0, row = 1) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + + + ################################################# + + def wagon_function(self, _parent): + self.popup.destroy() + self.data_holder.tester_type = 'Wagon' + popup2 = Popup2(_parent, self.data_holder) + + ################################################# + + def engine_function(self, _parent): + self.popup.destroy() + self.data_holder.tester_type = 'Engine' + + _parent.set_frame_admin_scan() + + +################################################################################# + +class Popup2(): + + ################################################# + + def __init__(self, parent, data_holder): + self.confirm_popup(data_holder) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder): + self.data_holder = data_holder + logger.info("Number of Wagon Wheels is being Specified.") + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Select Test Stand Type") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + bind_func = self.continue_function + frm_popup.bind_all("", lambda event: bind_func(self.parent)) + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Enter Number of Wagon Wheels", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2) + + self.text = "" + self.wagon_wheels = tk.Entry( + frm_popup, + textvariable= self.text, + font=('Arial', '15'), + ) + self.wagon_wheels.grid(column = 0, row = 1, columnspan = 2) + + # Creates retry and continue buttons + btn_retry = ttk.Button( + frm_popup, + text = "Cancel", + command = lambda: self.cancel_function() + ) + btn_retry.grid(column = 1, row = 2) + + btn_continue = ttk.Button( + frm_popup, + text = "Confirm", + command = lambda: self.continue_function(self.parent) + ) + btn_continue.grid(column = 0, row = 2) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + frm_popup.grid_rowconfigure(2, weight=1) + + + ################################################# + + # Called when the "cancel" button is selected + def cancel_function(self): + self.popup.destroy() + + ################################################# + + # Called to continue on in the testing procedure + def continue_function(self, _parent): + self.data_holder.wagon_tester_info['num_wagon_wheels'] = self.wagon_wheels.get() + + self.popup.destroy() + _parent.set_frame_admin_scan() + + + +################################################################################# + + +class Popup3(): + + ################################################# + + def __init__(self, parent, data_holder): + self.confirm_popup(data_holder) + self.parent = parent + + ################################################# + + # Function to make retry or continue window if the test fails + def confirm_popup(self, data_holder): + self.data_holder = data_holder + logger.info("Tester component info is being updated.") + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Select Test Stand Type") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Select Test Stand Type", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2) + + # Creates retry and continue buttons + btn_wagon = ttk.Button( + frm_popup, + text = "Wagon Tester", + command = lambda: self.wagon_function(self.parent) + ) + btn_wagon.grid(column = 1, row = 1) + + btn_continue = ttk.Button( + frm_popup, + text = "Engine Tester", + command = lambda: self.engine_function(self.parent) + ) + btn_continue.grid(column = 0, row = 1) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + + + ################################################# + + def wagon_function(self, _parent): + self.popup.destroy() + self.data_holder.tester_type = 'Wagon' + _parent.set_frame_tester_component_frame() + + ################################################# + + def engine_function(self, _parent): + self.popup.destroy() + self.data_holder.tester_type = 'Engine' + _parent.set_frame_tester_component_frame() + + +################################################################################# diff --git a/PythonFiles/Scenes/ComponentScanScene.py b/PythonFiles/Scenes/ComponentScanScene.py new file mode 100644 index 00000000..1cfb298d --- /dev/null +++ b/PythonFiles/Scenes/ComponentScanScene.py @@ -0,0 +1,378 @@ +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter as tk +import tkinter.ttk as ttk +import sys, time +from tkinter import * +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ComponentScanScene') + + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class TesterComponentScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + self.data_holder = data_holder + self.is_current_scene = False + + self.create_style(parent) + + self.EXIT_CODE = 0 + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/../awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + # can see more scanner documentation in the Visual Inspection GUI + def scan_QR_code(self, master_window): + + self.ent_full.config(state = 'normal') + self.ent_full.delete(0,END) + self.master_window = master_window + self.hide_rescan_button() + + #sys.path.insert(1,'/home/hgcal/WagonTest/Scanner/python') + + from ..Scanner.python.get_barcodes import scan, listen, parse_xml + + manager = mp.Manager() + full_id = manager.list() + + self.ent_full.config(state = 'normal') + + self.scanner = scan() + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + while 1 > 0: + + try: + self.master_window.update() + except: + pass + if not len(full_id) == 0: + self.label = parse_xml(full_id[0]) + + self.listener.terminate() + self.scanner.terminate() + + self.ent_full.delete(0,END) + self.ent_full.insert(0, str(self.label)) + self.ent_full.config(state = 'disabled') + self.show_rescan_button() + break + + elif self.EXIT_CODE: + logger.info("Exit code received on ComponentScanScene. Terminating processes.") + self.listener.terminate() + self.scanner.terminate() + logger.info("ComponentScanScene processes terminated successfully.") + break + else: + time.sleep(.01) + + # Creates the GUI itself + def initialize_GUI(self, parent, master_frame): + + self.master_frame = master_frame + self.parent = parent + + super().__init__(self.master_frame, width = 1105, height = 800) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + # Create a photoimage object of the QR Code + QR_image = Image.open("{}/Images/EngineExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label = ttk.Label(self, image=QR_PhotoImage) + QR_label.image = QR_PhotoImage + + # the .grid() adds it to the Frame + QR_label.grid(column=3, row = 0, sticky= 'ne', pady = (250,0)) + + Scan_Board_Prompt_Frame = ttk.Frame(self,) + Scan_Board_Prompt_Frame.grid(column=0, row = 0) + + # creates a Label Variable, different customization options + self.lbl_check = ttk.Label( + master = Scan_Board_Prompt_Frame, + text = 'Check In', + font = ('Arial', 40) + ) + self.lbl_check.pack(padx = 50, pady = 50) + + lbl_scan = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Scan the QR Code on the Board", + font = ('Arial', 24) + ) + lbl_scan.pack(padx = 50, pady = 25) + + # Create a label to label the entry box + lbl_full = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Full ID:", + font = ('Arial', 24) + ) + lbl_full.pack(padx = 20) + + # Entry for the full id to be displayed. Upon Scan, update and disable? + global ent_full + + # Creating intial value in entry box + user_text = tk.StringVar(self) + + # Creates an entry box + self.ent_full = tk.Entry( + Scan_Board_Prompt_Frame, + font = ('Arial', 16), + textvariable= user_text, + ) + self.ent_full.pack(padx = 50, pady = 25) + + Option_list = ['Yes', 'No'] + self.option_selected = tk.StringVar(self) + + lbl_select = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Does the component work?", + font = ('Arial', 24) + ) + lbl_select.pack(padx = 20) + + self.opt_user_dropdown = ttk.OptionMenu( + Scan_Board_Prompt_Frame, + self.option_selected, + Option_list[0], + *Option_list, + ) + self.opt_user_dropdown.pack(pady=20) + + # Create a label to label the comments box + lbl_com = ttk.Label( + Scan_Board_Prompt_Frame, + text = "Comments:", + font = ('Arial', 32), + ) + lbl_com.pack(padx = 20) + + com_text = '' + #place to enter comments + self.ent_com = tk.Text( + master = Scan_Board_Prompt_Frame, + font = ('Arial', 16), + height = 5, + width = 20 + ) + self.ent_com.pack(padx = 50) + + # Traces an input to show the submit button once text is inside the entry box + user_text.trace( + "w", + lambda name, + index, + mode, + sv=user_text: self.show_submit_button() + ) + + # Rescan button creation + self.btn_rescan = ttk.Button( + self, + text="Rescan", + #padx = 20, + #pady =10, + #relief = tk.RAISED, + command = lambda: self.scan_QR_code(self.master_window) + ) + self.btn_rescan.grid(row = 1, column = 3, sticky = 'ne') + + # Submit button creation + self.btn_submit = ttk.Button( + self, + text="Submit", + #padx = 20, + #pady = 10, + #relief = tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.grid(row = 1, column = 4, sticky = 'ne') + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=0, row = 1) + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 3, row = 0, sticky= 'se') + + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Done Updating Info", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.pack(anchor = 'se', padx = 10, pady = 20) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 's', padx = 10, pady = 20) + + + # Locks frame size to the master_frame size + self.grid_propagate(0) + + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + ################################################# + + # Function for the submit button + def btn_submit_action(self, _parent): + + self.EXIT_CODE = 1 + + self.data_holder.set_component_info(self.ent_full.get(), self.option_selected.get(), self.ent_com.get(1.0, 'end-1c')) + _parent.set_frame_tester_component_frame() + + self.EXIT_CODE = 0 + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + # Function for the log out button + def btn_logout_action(self, _parent): + + self.EXIT_CODE = 1 + self.listener.terminate() + self.scanner.terminate() + + + # Send user back to login frame + _parent.set_frame_login_frame() + + ################################################# + + # Function to activate the submit button + def show_submit_button(self): + self.data_holder.decode_label(self.ent_full.get()) + self.btn_submit["state"] = "active" + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to disable to the submit button + def hide_submit_button(self): + self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to activate the rescan button + def show_rescan_button(self): + self.btn_rescan["state"] = "active" + + ################################################# + + # Function to disable to the rescan button + def hide_rescan_button(self): + self.btn_rescan["state"] = "disabled" + + ################################################# + + def kill_processes(self): + logger.info("Terminating scanner processes.") + try: + self.scanner.kill() + self.listener.terminate() + self.EXIT_CODE = 1 + except: + logger.info("Processes could not be terminated.") diff --git a/PythonFiles/Scenes/LoginScene.py b/PythonFiles/Scenes/LoginScene.py index c2541658..0cbe5085 100644 --- a/PythonFiles/Scenes/LoginScene.py +++ b/PythonFiles/Scenes/LoginScene.py @@ -2,9 +2,16 @@ # importing necessary modules import tkinter as tk +import tkinter.ttk as ttk +import logging +import PythonFiles +import os +import sys ################################################################################# +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.LoginScene') + # Creates a class that is called by the GUIWindow. # GUIWindow instantiates an object called login_frame. @@ -12,30 +19,45 @@ # @param master_frame -> passes master_frame as the container for everything in the class. # @param data_holder -> passes data_holder into the class so the data_holder functions can # be accessed within the class. -class LoginScene(tk.Frame): +class LoginScene(ttk.Frame): ################################################# def __init__(self, parent, master_frame, data_holder): - super().__init__(master_frame, width=850, height=500) + super().__init__(master_frame, width=1300-225, height=800) self.data_holder = data_holder + self.create_style(parent) + self.update_frame(parent) + + self.parent = parent + + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') - # Creating a list of users for dropdown menu - # Eventually need to add a way for a database to have control over this list - User_List = [ - "Bob Johnson", - "Spencer Higgins", - "Amanda Holmes" - ] + def update_frame(self, parent): + + for widget in self.winfo_children(): + widget.destroy() + + + # getting list of users for dropdown menu + User_List = self.data_holder.get_all_users() # Creating the title for the window - lbl_title = tk.Label( + lbl_title = ttk.Label( self, text="Please Select Your Name", - font=('Arial', '24') ) + lbl_title.config(font = ('Arial', 52)) lbl_title.pack(pady=75) # Creating intial value in dropdown menu @@ -49,8 +71,8 @@ def __init__(self, parent, master_frame, data_holder): *User_List # Tells the dropdown menu to use every index in the User_List list ) self.opt_user_dropdown.pack(pady=15) - self.opt_user_dropdown.config(width = 20, font = ('Arial', 13)) - self.opt_user_dropdown['menu'].configure(font = ('Arial', 12)) + self.opt_user_dropdown.config(width = 20) + #self.opt_user_dropdown['menu'].configure(font = ('Arial', 12)) # Traces when the user selects an option in the dropdown menu # When an option is selected, it calls the show_submit_button function @@ -61,52 +83,80 @@ def __init__(self, parent, master_frame, data_holder): # Creating the submit button # It does not get enabled until the user selects an option menu option - self.btn_submit = tk.Button( + self.btn_submit = ttk.Button( self, text="Submit", - padx = 50, - pady = 10, - relief=tk.RAISED, + #padx = 50, + #pady = 10, + #relief=tk.RAISED, command= lambda: self.btn_submit_action(parent) ) - self.btn_submit.pack() - self.btn_submit.config(state = 'disabled') - + self.btn_submit.pack(pady = (25,0)) + self.btn_submit.config( state = 'disabled') # Creating the add user button - self.btn_add_user = tk.Button( + self.btn_admin = ttk.Button( self, - text="Add User", - padx = 20, - pady = 5, - relief=tk.RAISED, - command= lambda: self.btn_add_user_action(parent) + text="Admin Tools", + #padx = 20, + #pady = 5, + #relief=tk.RAISED, + command= lambda: self.btn_admin_action(parent) ) - self.btn_add_user.pack(pady=70) + self.btn_admin.pack(pady=40) + + if (self.data_holder.tester_type == 'Thermal'): + self.btn_admin.pack_forget() + + + # Creating the help button + self.btn_help = ttk.Button( + self, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + self.btn_help.pack(anchor = 's', padx = 10, pady = 20) + # Forces frame to stay the size of the main_window # rather than adjusting to the size of the widgets self.pack_propagate(0) + + ################################################# + + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# # Creates the function for the submit button command # @params "_parent" is also a parent like "parent", but it is a different "parent", # passes in GUIWindow def btn_submit_action(self, _parent): + if (self.user_selected.get() != ""): # Sets the user_ID in the data_holder to the selected user - self.data_holder.user_ID = self.user_selected.get() - # Changes frame to scan_frame - _parent.set_frame_scan_frame() - + self.data_holder.set_user_ID(self.user_selected.get()) + # Changes frame to scan_frame + _parent.set_frame_scan_frame() + else: + pass + + def get_submit_action(self): + return self.btn_submit_action - self.data_holder.print() + def get_parent(self): + return self.parent ################################################# - # To be given commands later, for now it is a dummy function - def btn_add_user_action(self, _parent): - pass + def btn_admin_action(self, _parent): + logger.info("User has opened the Admin tools") + pass_pop = PasswordPopup(_parent, self.data_holder) ################################################# @@ -116,5 +166,176 @@ def show_submit_button(self): ################################################# +class PasswordPopup(): + + ################################################# + + def __init__(self, parent, data_holder): + self.create_style(parent) + self.password_popup(data_holder) + self.parent = parent + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + ################################################# + + # Function to enter password for admin access + def password_popup(self, data_holder): + self.data_holder = data_holder + logger.info("Prompting the user for the admin password") + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Admin Access") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + bind_func = self.continue_function + frm_popup.bind_all("", lambda event: bind_func(self.parent)) + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Enter Admin Password", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2) + + self.password = "" + self.user_password = tk.Entry( + frm_popup, + textvariable= self.password, + font=('Arial', '15'), + show = "*" + ) + self.user_password.grid(column = 0, row = 1, columnspan = 2) -################################################################################# \ No newline at end of file + # Creates retry and continue buttons + btn_retry = ttk.Button( + frm_popup, + text = "Cancel", + command = lambda: self.cancel_function() + ) + btn_retry.grid(column = 1, row = 2) + + btn_continue = ttk.Button( + frm_popup, + text = "Confirm", + command = lambda: self.continue_function(self.parent) + ) + btn_continue.grid(column = 0, row = 2) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + frm_popup.grid_rowconfigure(2, weight=1) + + + ################################################# + + # Called when the "cancel" button is selected + def cancel_function(self): + self.popup.destroy() + + ################################################# + + # Called to continue on in the testing procedure + def continue_function(self, _parent): + self.data_holder.attempt_admin_access(self.user_password.get()) + if self.data_holder.admin == True: + _parent.set_frame_admin_frame() + else: + fail_pop = FailedPopup(_parent, self.data_holder) + + self.popup.destroy() + + + + +################################################################################# + +class FailedPopup(): + + ################################################# + + def __init__(self, parent, data_holder): + self.password_popup(data_holder) + self.parent = parent + + ################################################# + + # Function to enter password for admin access + def password_popup(self, data_holder): + self.data_holder = data_holder + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Admin Connection Failed") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + bind_func = self.cancel_function + frm_popup.bind_all("", lambda event: bind_func()) + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Incorrect Password!", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) + + # Creates retry and continue buttons + btn_retry = ttk.Button( + frm_popup, + text = "Retry", + command = lambda: self.retry_function(self.parent) + ) + btn_retry.grid(column = 0, row = 1) + + btn_ok = ttk.Button( + frm_popup, + text = "Cancel", + command = lambda: self.cancel_function() + ) + btn_ok.grid(column = 1, row = 1) + + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_columnconfigure(1, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + + + ################################################# + + # Called when the "cancel" button is selected + def cancel_function(self): + self.popup.destroy() + + def retry_function(self, _parent): + self.popup.destroy() + pass_pop = PasswordPopup(_parent, self.data_holder) + + + +################################################################################# diff --git a/PythonFiles/Scenes/PostScanScene.py b/PythonFiles/Scenes/PostScanScene.py new file mode 100644 index 00000000..b5651c7e --- /dev/null +++ b/PythonFiles/Scenes/PostScanScene.py @@ -0,0 +1,270 @@ +################################################################################# + +import PythonFiles +import json, logging +import tkinter as tk +import tkinter.ttk as ttk +from PIL import ImageTk as iTK +from PIL import Image +from matplotlib.pyplot import table +from pyparsing import col +import PythonFiles +import os +import datetime + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.PostScanScene') + +# Frame that shows up after board has been entered with info about the board +# @param parent -> References a GUIWindow object +# @param master_frame -> Tkinter object that the frame is going to be placed on +# @param data_holder -> DataHolder object that stores all relevant data + +class PostScanScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder): + + # Call to the super class's constructor + # Super class is the tk.Frame class + self.data_holder = data_holder + + self.master_frame = master_frame + + super().__init__(self.master_frame, width = 1300-213, height = 800) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + self.parent = parent + + self.create_frame(parent) + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def create_frame(self, parent): + self.create_style(parent) + + try: + for widget in self.winfo_children(): + widget.destroy() + except: + logger.warning("Widgets could not be found and/or destroyed (making room for new widgets on the PostScanScene).") + else: + logger.info("Widgets destroyed successfully (making room for new widgets on the PostScanScene).") + + + self.canvas = tk.Canvas(self) + self.frame = ttk.Frame(self.canvas) + self.scroller = ttk.Scrollbar(self, orient='vertical', command=self.canvas.yview) + self.canvas.configure(yscrollcommand=self.scroller.set) + + self.canvas.grid(row = 0, column = 0, sticky='nsew') + self.scroller.grid(row=0, column=1, sticky='nsw') + self.window = self.canvas.create_window((0,0), window=self.frame, anchor='nw', tags='self.frame') + + #resizing + #self.frame.pack(fill='both', expand=True) + + self.canvas.bind('', self.onCanvasConfigure) + self.frame.bind('', self.onFrameConfigure) + self.frame.bind('', self.onEnter) + self.frame.bind('', self.onLeave) + + self.onFrameConfigure(None) + + self.frame.grid_columnconfigure(1, weight = 1) + self.frame.grid_columnconfigure(2, weight = 1) + self.frame.grid_rowconfigure(0, weight = 1) + self.grid_columnconfigure(0, weight = 1) + self.grid_rowconfigure(0, weight = 1) + + if self.data_holder.data_dict['prev_results']: + + # Adds the title to the Summary Frame + self.title = ttk.Label( + self.frame, + text = "Board Scanned!", + font=('Arial',20) + ) + self.title.grid(row= 0, column= 1, pady = 20) + + # Adds Board Full ID to the SummaryFrame + self.id = ttk.Label( + self.frame, + text = str(self.data_holder.data_dict['current_full_ID']), + font=('Arial',20) + ) + self.id.grid(row= 1, column= 1, pady = 20) + + green_check = Image.open("{}/Images/GreenCheckMark.png".format(PythonFiles.__path__[0])) + green_check = green_check.resize((75, 75), Image.LANCZOS) + green_check = iTK.PhotoImage(green_check) + + redx = Image.open('{}/Images/RedX.png'.format(PythonFiles.__path__[0])) + redx = redx.resize((75, 75), Image.LANCZOS) + redx = iTK.PhotoImage(redx) + # adds previously run tests to the canvas with pass/fail info + try: + if self.data_holder.data_dict['test_names']: + res_dict = {} + for n in self.data_holder.data_dict['test_names']: + res_dict[n] = [] + for idx,el in enumerate(self.data_holder.data_dict['prev_results']): + res_dict[el[0]] = el[1] + + for idx,el in enumerate(res_dict.keys()): + self.lbl_res = ttk.Label( + self.frame, + text = str(el) + ': ', + font=('Arial',14) + ) + self.lbl_res.grid(row=idx+2, column=1) + if res_dict[el] == 'Passed': + self.lbl_img = ttk.Label( + self.frame, + image = green_check, + font=('Arial',14) + ) + self.lbl_img.image=green_check + self.lbl_img.grid(row=idx+2, column=2) + elif res_dict[el] == 'Failed': + self.lbl_img = ttk.Label( + self.frame, + image = redx, + font=('Arial',14) + ) + self.lbl_img.image=redx + self.lbl_img.grid(row=idx+2, column=2) + else: + self.lbl_res = ttk.Label( + self.frame, + text = 'This test has not been run.', + font=('Arial',14) + ) + self.lbl_res.grid(row=idx+2, column=2) + + else: + self.lbl_res = ttk.Label( + self.frame, + text = str(self.data_holder.data_dict['prev_results']), + font=('Arial',14) + ) + self.lbl_res.grid(row=2, column=1) + + except Exception as e: + logger.exception(e) + self.lbl_full = ttk.Label( + self, + text = 'Error, No Results', + font=('Arial', 14) + ) + self.lbl_full.grid(row = 2, column =1, pady = 10) + + # Creating the proceed button + proceed_button = ttk.Button( + self.frame, + #relief = tk.RAISED, + text = "Proceed", + command = lambda: self.btn_proceed_action(parent) + ) + proceed_button.grid(row=2, column=3, padx = 10, pady = 10) + + else: + self.lbl_1 = ttk.Label( + self, + text = "This board hasn't been checked in.", + font=('Arial', 14) + ) + self.lbl_1.grid(row = 2, column =1, pady = 10) + + self.lbl_2 = ttk.Label( + self, + text = "Please visit the check in and inspection station.", + font=('Arial', 14) + ) + self.lbl_2.grid(row = 2, column =1, pady = 10) + + + + #creating the next board buttom + next_board_button = ttk.Button( + self.frame, + #relief = tk.RAISED, + text = "Change Boards", + command = lambda: self.btn_NextBoard_action(parent) + ) + next_board_button.grid(row=3, column=3, padx = 10, pady = 10) + + + # Creating the logout button + btn_logout = ttk.Button( + self.frame, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.grid(row=4, column=3, padx = 10, pady = 20) + + + + + ################################################# + + def btn_proceed_action(self, _parent): + _parent.scan_frame_progress() + + def btn_NextBoard_action(self, parent): + parent.set_frame_scan_frame() + + def btn_logout_action(self, parent): + parent.set_frame_login_frame() + + def get_submit_action(self): + return self.btn_proceed_action + + def get_parent(self): + return self.parent + + + ################################################# + + def update_frame(self): + self.create_frame(self.parent) + + ################################################# + + def onFrameConfigure(self, event): + self.canvas.configure(scrollregion=self.canvas.bbox('all')) + + def onCanvasConfigure(self, event): + self.canvas.itemconfig(self.window, width=event.width) + + def onMouseWheel(self, event): + if event.num == 4: + self.canvas.yview_scroll(-1, 'units') + elif event.num == 5: + self.canvas.yview_scroll(1, 'units') + + def onEnter(self, event): + self.canvas.bind_all('', self.onMouseWheel) + self.canvas.bind_all('', self.onMouseWheel) + + def onLeave(self, event): + self.canvas.unbind_all('') + self.canvas.unbind_all('') + + + +################################################################################# diff --git a/PythonFiles/Scenes/ScanManyScene.py b/PythonFiles/Scenes/ScanManyScene.py new file mode 100644 index 00000000..a69938e3 --- /dev/null +++ b/PythonFiles/Scenes/ScanManyScene.py @@ -0,0 +1,419 @@ +################################################################################# + +# importing necessary modules +import multiprocessing as mp +import logging, time +import tkinter.ttk as ttk +import tkinter as tk +import sys, time +from tkinter import * +from turtle import back +from PIL import ImageTk as iTK +from PIL import Image +import PythonFiles +import os + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ScanManyScene') + +# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow +# instantiated as scan_frame by GUIWindow +# @param parent -> passes in GUIWindow as the parent. +# @param master_frame -> passes master_frame as the container for everything in the class. +# @param data_holder -> passes data_holder into the class so the data_holder functions can +# be accessed within the class. +class ScanManyScene(ttk.Frame): + + ################################################# + + # Runs upon creation + def __init__(self, parent, master_frame, data_holder): + + self.data_holder = data_holder + + self.use_scanner = self.data_holder.get_use_scanner() + + self.is_current_scene = False + + self.EXIT_CODE = 0 + + self.master_frame = master_frame + + self.parent = parent + + super().__init__(self.master_frame, width=1300-213, height = 800) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) + + self.create_style(parent) + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + # can see more scanner documentation in the Visual Inspection GUI + def scan_QR_code(self, master_window): + + if self.use_scanner: + + self.EXIT_CODE = 0 + + #self.ent_full.config(state = 'normal') + #self.ent_full.delete(0,END) + self.master_window = master_window + self.hide_rescan_button() + sys.path.insert(1,'/home/hgcal/WagonTest/Scanner/python') + + from ..Scanner.python.get_barcodes import scan, listen, parse_xml + + manager = mp.Manager() + full_id = manager.list() + + #self.ent_full.config(state = 'normal') + + self.scanner = scan() + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + current_idx = 0 + + logger.debug(f'Current_scan_idx = {current_idx}') + + while 1 > 0: + + try: + self.master_window.update() + except: + pass + if not len(full_id) == 0: + label = parse_xml(full_id[0]) + + self.listener.terminate() + self.scanner.terminate() + + #self.ent_full.delete(0,END) + self.ent_list.insert(tk.END, str(label)) + #self.ent_full.config(state = 'disabled') + self.show_rescan_button() + #break + + current_idx += 1 + + self.ent_list.grid(column=0, row=2) + logger.debug('Label: ' + str(label)) + manager = mp.Manager() + full_id = manager.list() + + self.scanner = scan() + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + elif self.EXIT_CODE == 1: + logger.info("Exit code received on the ScanManyScene. Terminating processes.") + self.listener.terminate() + self.scanner.terminate() + logger.info("ScanManyScene processes terminated successfully.") + break + else: + time.sleep(.01) + + # Creates the GUI itself + def initialize_GUI(self, parent, master_frame): + + QR_Frame = ttk.Frame(self) + QR_Frame.grid(sticky = 'ne', column = 1) + # Create a photoimage object of the QR Code + + QR_image = Image.open("{}/Images/WagonExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image) + QR_label = ttk.Label(QR_Frame, image=QR_PhotoImage) + QR_label.image = QR_PhotoImage + + QR_image2 = Image.open("{}/Images/EngineExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image2) + QR_label2 = ttk.Label(QR_Frame, image=QR_PhotoImage) + QR_label2.image = QR_PhotoImage + + + # the .grid() adds it to the Frame + QR_label.grid(column=1, row = 1, sticky='nw', pady = (25, 15)) + QR_label2.grid(column=1, row = 2, sticky='nw', pady = 15) + + + Scan_Board_Prompt_Frame = ttk.Frame(self, width = 1105, height = 650) + Scan_Board_Prompt_Frame.grid(column=0, row = 0, sticky='nsew') + + Button_Frame1 = ttk.Frame(self) + Button_Frame1.grid(column=1, row=0, sticky='ew') + + Button_Frame2 = ttk.Frame(self) + Button_Frame2.grid(column=1, row=2, sticky='ew') + + #resizing + Scan_Board_Prompt_Frame.grid_columnconfigure(0, weight=1) + Scan_Board_Prompt_Frame.grid_columnconfigure(1, weight=1) + QR_label.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(1, weight=1) + + # creates a Label Variable, different customization options + lbl_scan = ttk.Label( + master= Scan_Board_Prompt_Frame, + text = "Scan all other zippers in sleeve:", + font = ('Arial', 28) + ) + lbl_scan.grid(column=0, row=0, sticky='n', pady = 50) + + # Create a label to label the entry box + #lbl_full = ttk.Label( + # Scan_Board_Prompt_Frame, + # text = "Full ID: ", + # font = ('Arial', 24) + #) + #lbl_scan.grid(column=0, row=2) + + # Entry for the full id to be displayed. Upon Scan, update and disable? + #global ent_full + + # Creating intial value in entry box + self.user_text = tk.StringVar(self) + + # Creates an entry box + #self.ent_full = tk.Entry( + # Scan_Board_Prompt_Frame, + # font = ('Arial', 24), + # textvariable= self.user_text, + # ) + #self.ent_full.grid(column=0, row=3) + + self.ent_list = tk.Listbox( + Scan_Board_Prompt_Frame, + font = ('Arial', 16) + ) + self.ent_list.grid(column=0, row=2) + + + # Traces an input to show the submit button once text is inside the entry box + self.user_text.trace( + "w", + lambda name, + index, + mode, + sv=self.user_text: self.show_submit_button() + ) + + # Remove button creation + self.btn_rescan = ttk.Button( + Scan_Board_Prompt_Frame, + text="Remove", + #padx = 20, + #pady =10, + #relief = tk.RAISED, + command = lambda: self.remove_list_entry() + ) + self.btn_rescan.grid(column=0, row=5, padx=10, pady=(25,10)) + + # Submit button creation + self.btn_submit = ttk.Button( + Scan_Board_Prompt_Frame, + text="Submit", + #padx = 20, + #pady = 10, + #relief = tk.RAISED, + command= lambda: self.btn_submit_action(parent) + ) + self.btn_submit.grid(column=0, row=6, padx=10, pady=5) + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=0, row = 1) + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + + + # Creating frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 1, row = 1, sticky= 'se') + + # Creating the logout button + btn_logout = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Logout", + command = lambda: self.btn_logout_action(parent) + ) + btn_logout.grid(column=0, row=1, sticky='ne', padx=10, pady=10) + + # Creating the help button + + #btn_help = ttk.Button( + # frm_logout, + # #relief = tk.RAISED, + # text = "Help", + # command = lambda: self.help_action(parent) + #) + #btn_help.grid(column=0, row=0, sticky='ne', padx=10, pady=10) + + + + + ################################################# + + #def help_action(self, _parent): + # _parent.help_popup(self) + + def remove_list_entry(self): + + idxs = self.ent_list.curselection() + + self.ent_list.delete(*idxs) + + + ################################################# + + + # Function for the submit button + def btn_submit_action(self, _parent): + + self.EXIT_CODE = 1 + + if self.use_scanner: + self.listener.terminate() + self.scanner.terminate() + + #self.data_holder.set_full_ID(self.ent_full.get()) + #_parent.update_config() + #if self.data_holder.getGUIcfg().get_if_use_DB(): + # self.data_holder.check_if_new_board() + + entries = self.ent_list.get(0, tk.END) + + self.data_holder.setOtherZippers(entries) + self.data_holder.passOtherZippers() + + self.ent_list.delete(0, tk.END) + + _parent.create_test_frames(self.data_holder.data_dict['queue']) + _parent.set_frame_postscan() + + #self.EXIT_CODE = 0 + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent + + ################################################# + + # Function for the log out button + def btn_logout_action(self, _parent): + + self.EXIT_CODE = 1 + + if self.use_scanner: + self.listener.terminate() + self.scanner.terminate() + + # Send user back to login frame + _parent.set_frame_login_frame() + + #self.EXIT_CODE = 0 + + ################################################# + + # Function to activate the submit button + def show_submit_button(self): + self.data_holder.decode_label(self.ent_full.get()) + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + major = self.data_holder.label_info['Major Type'] + if major == 'LD-Engine' or major == 'HD-Engine' or major == 'LD-Wagon-West' or major == 'LD-Wagon-East' or major == 'HD-Wagon' or major == "Zipper Board": + self.btn_submit["state"] = "active" + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + + ################################################# + + # Function to disable to the submit button + def hide_submit_button(self): + self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + + ################################################# + + # Function to activate the rescan button + def show_rescan_button(self): + self.btn_rescan["state"] = "active" + + ################################################# + + # Function to disable to the rescan button + def hide_rescan_button(self): + self.btn_rescan["state"] = "disabled" + + ################################################# + + def kill_processes(self): + logger.info("Terminating scanner proceses.") + try: + if self.use_scanner: + self.scanner.kill() + self.listener.terminate() + self.EXIT_CODE = 1 + except: + logger.info("Processes could not be terminated.") diff --git a/PythonFiles/Scenes/ScanScene.py b/PythonFiles/Scenes/ScanScene.py index 73393c4e..de426b9f 100644 --- a/PythonFiles/Scenes/ScanScene.py +++ b/PythonFiles/Scenes/ScanScene.py @@ -2,20 +2,21 @@ # importing necessary modules import multiprocessing as mp -import threading, time +import logging, time +import tkinter.ttk as ttk import tkinter as tk import sys, time from tkinter import * from turtle import back from PIL import ImageTk as iTK from PIL import Image +import PythonFiles +import os ################################################################################# - -# Creating variable for testing QR Code entry -QRcode = "1090201033667425" +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ScanScene') # creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow @@ -24,236 +25,343 @@ # @param master_frame -> passes master_frame as the container for everything in the class. # @param data_holder -> passes data_holder into the class so the data_holder functions can # be accessed within the class. -class ScanScene(tk.Frame): +class ScanScene(ttk.Frame): ################################################# # Runs upon creation def __init__(self, parent, master_frame, data_holder): - self.data_holder = data_holder - self.is_current_scene = False - self.EXIT_CODE = 0 - # Runs the initilize_GUI function, which actually creates the frame - # params are the same as defined above - self.initialize_GUI(parent, master_frame) + self.data_holder = data_holder + self.use_scanner = self.data_holder.get_use_scanner() - # Creates a thread for the scanning of a barcode - # Needs to be updated to run the read_barcode function in the original GUI - def scan_QR_code(self, master_window): + self.is_current_scene = False - print("Begin to scan") - ent_snum.config(state = 'normal') - ent_snum.delete(0,END) - self.master_window = master_window - self.hide_rescan_button() - - sys.path.insert(1,'/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/Scanner/python') + self.EXIT_CODE = 0 - from get_barcodes import scan, listen, parse_xml + self.master_frame = master_frame - manager = mp.Manager() - serial = manager.list() + self.parent = parent - ent_snum.config(state = 'normal') - self.scanner = scan() - self.listener = mp.Process(target=listen, args=(serial, self.scanner)) + super().__init__(self.master_frame, width=1300-213, height = 800) - self.listener.start() - - while 1 > 0: + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) - try: - self.master_window.update() - except: - pass - if not len(serial) == 0: - self.data_holder.current_serial_ID = parse_xml(serial[0]) - self.listener.terminate() - self.scanner.terminate() - - ent_snum.delete(0,END) - ent_snum.insert(0, str(self.data_holder.current_serial_ID)) - ent_snum.config(state = 'disabled') - self.show_rescan_button() - break - - elif self.EXIT_CODE: - self.listener.terminate() - self.scanner.terminate() - break - else: - time.sleep(.01) + # Runs the initilize_GUI function, which actually creates the frame + # params are the same as defined above + self.initialize_GUI(parent, master_frame) - ''' - self.QR_thread = mp.Process(target=self.insert_QR_ID) + self.create_style(parent) + + def create_style(self, _parent): - self.QR_thread.start() + self.s = ttk.Style() - self.QR_thread.join() - ''' - # Updates the QR ID in the task - # Place holder function to insert the QRcode into the textbox - def insert_QR_ID(self): + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') - - # Clears the textbox of anything possibly in the box - ent_snum.delete(0, END) - - # Disables the rescan button until after the scanning is complete - self.hide_rescan_button() - - # Runs the hide_submit_button function and sets a default value to the QR_value - self.hide_submit_button() - #self.scanned_QR_value = 000 - - # Delay to simulate scanning a QRcode - '''for i in range(1): - time.sleep(1) - time.sleep(1) - print(i + 1) - time.sleep(0.5)''' - print("Finished Scan") - ent_snum.insert(0, self.data_holder.current_serial_ID) - #ent_snum.insert(0, QRcode) - ent_snum.config(state = 'disabled') - - # Restores access to the rescan button - self.show_rescan_button() + + # Creates a thread for the scanning of a barcode + # Needs to be updated to run the read_barcode function in the original GUI + # can see more scanner documentation in the Visual Inspection GUI + def scan_QR_code(self, master_window): - # Sets the scanned_QR_value to 0 when the function is not in use - '''if (not self.is_current_scene): - self.scanned_QR_value = 0 - else: - self.scanned_QR_value = QRcode - self.data_holder.current_serial_ID = self.scanned_QR_value - ''' - self.data_holder.print() - + if self.use_scanner: + + self.ent_full.config(state = 'normal') + self.ent_full.delete(0,END) + self.master_window = master_window + self.hide_rescan_button() + sys.path.insert(1,'/home/hgcal/WagonTest/Scanner/python') + + from ..Scanner.python.get_barcodes import scan, listen, parse_xml + + manager = mp.Manager() + full_id = manager.list() + + self.ent_full.config(state = 'normal') + + self.scanner = scan() + self.listener = mp.Process(target=listen, args=(full_id, self.scanner)) + + self.listener.start() + + while 1 > 0: + + try: + self.master_window.update() + except: + pass + if not len(full_id) == 0: + label = parse_xml(full_id[0]) + + self.listener.terminate() + self.scanner.terminate() + + self.ent_full.delete(0,END) + self.ent_full.insert(0, str(label)) + self.ent_full.config(state = 'disabled') + self.show_rescan_button() + break + + elif self.EXIT_CODE: + logger.info("Exit code received on the ScanScene. Terminating processes.") + self.listener.terminate() + self.scanner.terminate() + logger.info("ScanScene processes terminated successfully.") + break + else: + time.sleep(.01) # Creates the GUI itself def initialize_GUI(self, parent, master_frame): - - self.master_frame = master_frame - - super().__init__(self.master_frame, width = 850, height = 500) + + QR_Frame = ttk.Frame(self) + QR_Frame.grid(sticky = 'ne', column = 1) # Create a photoimage object of the QR Code - QR_image = Image.open("./PythonFiles/Images/QRimage.png") + + QR_image = Image.open("{}/Images/WagonExample.png".format(PythonFiles.__path__[0])) QR_PhotoImage = iTK.PhotoImage(QR_image) - QR_label = tk.Label(self, image=QR_PhotoImage) + QR_label = ttk.Label(QR_Frame, image=QR_PhotoImage) QR_label.image = QR_PhotoImage + QR_image2 = Image.open("{}/Images/EngineExample.png".format(PythonFiles.__path__[0])) + QR_PhotoImage = iTK.PhotoImage(QR_image2) + QR_label2 = ttk.Label(QR_Frame, image=QR_PhotoImage) + QR_label2.image = QR_PhotoImage + + # the .grid() adds it to the Frame - QR_label.grid(column=1, row = 0) + QR_label.grid(column=1, row = 1, sticky='nw', pady = (25, 15)) + QR_label2.grid(column=1, row = 2, sticky='nw', pady = 15) - Scan_Board_Prompt_Frame = Frame(self,) - Scan_Board_Prompt_Frame.grid(column=0, row = 0) + + Scan_Board_Prompt_Frame = ttk.Frame(self, width = 1105, height = 650) + Scan_Board_Prompt_Frame.grid(column=0, row = 0, sticky='nsew') + + Button_Frame1 = ttk.Frame(self) + Button_Frame1.grid(column=1, row=0, sticky='ew') + + Button_Frame2 = ttk.Frame(self) + Button_Frame2.grid(column=1, row=2, sticky='ew') + + #resizing + Scan_Board_Prompt_Frame.grid_columnconfigure(0, weight=1) + Scan_Board_Prompt_Frame.grid_columnconfigure(1, weight=1) + QR_label.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(1, weight=1) # creates a Label Variable, different customization options - lbl_scan = tk.Label( + lbl_scan = ttk.Label( master= Scan_Board_Prompt_Frame, text = "Scan the QR Code on the Board", - font = ('Arial', 18) + font = ('Arial', 28) ) - lbl_scan.pack(padx = 50, pady = 50) + lbl_scan.grid(column=0, row=0, sticky='n', pady = 50) # Create a label to label the entry box - lbl_snum = tk.Label( + lbl_full = ttk.Label( Scan_Board_Prompt_Frame, - text = "Serial Number:", - font = ('Arial', 16) + text = "Full ID: ", + font = ('Arial', 24) ) - lbl_snum.pack() + lbl_scan.grid(column=0, row=2) - # Entry for the serial number to be displayed. Upon Scan, update and disable? - global ent_snum + # Entry for the full id to be displayed. Upon Scan, update and disable? + global ent_full # Creating intial value in entry box - user_text = tk.StringVar(self) + self.user_text = tk.StringVar(self) # Creates an entry box - ent_snum = tk.Entry( + self.ent_full = tk.Entry( Scan_Board_Prompt_Frame, - font = ('Arial', 16), - textvariable= user_text, + font = ('Arial', 24), + textvariable= self.user_text, ) - ent_snum.pack(padx = 50) + self.ent_full.grid(column=0, row=3) + # Traces an input to show the submit button once text is inside the entry box - user_text.trace( + self.user_text.trace( "w", lambda name, index, mode, - sv=user_text: self.show_submit_button() + sv=self.user_text: self.show_submit_button() ) # Rescan button creation - self.btn_rescan = tk.Button( + self.btn_rescan = ttk.Button( Scan_Board_Prompt_Frame, text="Rescan", - padx = 50, - pady =10, - relief = tk.RAISED, - command = lambda: self.scan_QR_code(self.master_window) + #padx = 20, + #pady =10, + #relief = tk.RAISED, + command = lambda: self.scan_QR_code(self.master_frame) ) - self.btn_rescan.pack(padx=10, pady=30) + self.btn_rescan.grid(column=0, row=5, padx=10, pady=(25,10)) # Submit button creation - self.btn_submit = tk.Button( + self.btn_submit = ttk.Button( Scan_Board_Prompt_Frame, text="Submit", - padx = 50, - pady = 10, - relief = tk.RAISED, + #padx = 20, + #pady = 10, + #relief = tk.RAISED, command= lambda: self.btn_submit_action(parent) ) - self.btn_submit.pack(padx=10) + self.btn_submit.grid(column=0, row=6, padx=10, pady=5) + + #creates a frame for the label info + label_frame = ttk.Frame(self) + label_frame.grid(column=0, row = 1) + + self.label_major = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_major.pack(padx=50, pady=10) + + self.label_sub = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sub.pack(padx=50, pady=10) + + self.label_sn = ttk.Label( + label_frame, + text='', + font = ('Arial', 16), + ) + self.label_sn.pack(padx=50, pady=10) + # Creating frame for logout button - frm_logout = tk.Frame(self) + frm_logout = ttk.Frame(self) frm_logout.grid(column = 1, row = 1, sticky= 'se') # Creating the logout button - btn_logout = tk.Button( + btn_logout = ttk.Button( frm_logout, - relief = tk.RAISED, + #relief = tk.RAISED, text = "Logout", command = lambda: self.btn_logout_action(parent) ) - btn_logout.pack(anchor = 'se', padx = 230, pady = 180) - # Locks frame size to the master_frame size - self.grid_propagate(0) + btn_logout.grid(column=0, row=1, sticky='ne', padx=10, pady=10) + + # Creating the help button + + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.grid(column=0, row=0, sticky='ne', padx=10, pady=10) + + + ################################################# + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + # Function for the submit button def btn_submit_action(self, _parent): - _parent.go_to_next_test() + + self.EXIT_CODE = 1 + +# if self.use_scanner: +# self.listener.terminate() +# self.scanner.terminate() + + self.data_holder.set_full_ID(self.ent_full.get()) + _parent.update_config() + if self.data_holder.getGUIcfg().get_if_use_DB(): + self.data_holder.check_if_new_board() + + _parent.create_test_frames(self.data_holder.data_dict['queue']) + if 'Zipper' in self.data_holder.getGUIcfg().getGUIType(): + _parent.set_frame_scan_many_frame() + else: + _parent.set_frame_postscan() + + self.EXIT_CODE = 0 + + def get_submit_action(self): + return self.btn_submit_action + + def get_parent(self): + return self.parent ################################################# # Function for the log out button def btn_logout_action(self, _parent): + self.EXIT_CODE = 1 + + if self.use_scanner: + self.listener.terminate() + self.scanner.terminate() + # Send user back to login frame _parent.set_frame_login_frame() + self.EXIT_CODE = 0 + ################################################# # Function to activate the submit button def show_submit_button(self): - self.btn_submit["state"] = "active" + self.data_holder.decode_label(self.ent_full.get()) + try: + self.label_major['text'] = 'Major Type: ' + self.data_holder.label_info['Major Type'] + self.label_sub['text'] = 'Subtype: ' + self.data_holder.label_info['Subtype'] + self.label_sn['text'] = 'Serial Number: ' + self.data_holder.label_info['SN'] + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + major = self.data_holder.label_info['Major Type'] + if major in ('LD-Engine', 'HD-Engine', 'LD-Wagon-West', 'LD-Wagon-East', 'HD-Wagon', "Zipper Board", "Scintillator Cables"): + self.btn_submit["state"] = "active" + except TypeError: + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() + ################################################# # Function to disable to the submit button def hide_submit_button(self): self.btn_submit["state"] = "disabled" + self.label_major['text'] = '' + self.label_sub['text'] = '' + self.label_sn['text'] = '' + self.label_major.update() + self.label_sub.update() + self.label_sn.update() ################################################# @@ -270,9 +378,11 @@ def hide_rescan_button(self): ################################################# def kill_processes(self): + logger.info("Terminating scanner processes.") try: - self.scanner.kill() - self.listener.terminate() + if self.use_scanner: + self.scanner.kill() + self.listener.terminate() self.EXIT_CODE = 1 except: - print("Processes could not be terminated.") + logger.info("Processes could not be terminated.") diff --git a/PythonFiles/Scenes/SidebarScene.py b/PythonFiles/Scenes/SidebarScene.py index 6016c88f..da0dee1c 100644 --- a/PythonFiles/Scenes/SidebarScene.py +++ b/PythonFiles/Scenes/SidebarScene.py @@ -1,197 +1,360 @@ ################################################################################# import tkinter as tk +import tkinter.ttk as ttk +from tkinter import Canvas +from tkinter import Scrollbar from PIL import ImageTk as iTK from PIL import Image +import logging +import PythonFiles +import os +import platform +import requests +import time +from tkinter import messagebox + ################################################################################# +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.SidebarScene') -class SidebarScene(tk.Frame): +class SidebarScene(ttk.Frame): ################################################# def __init__(self, parent, sidebar_frame, data_holder): - super().__init__(sidebar_frame, width=213, height = 500, bg = '#808080', padx = 10, pady=10) + super().__init__(sidebar_frame, width=225, height=850) + + + self.mycanvas = tk.Canvas(self, width=225, height=850, bg="#33393b") + self.viewingFrame = tk.Frame(self.mycanvas, background="#33393b", width=225, height=850) + + self.create_style(parent) + + self.Green_Check_Image = Image.open("{}/Images/GreenCheckMark.png".format(PythonFiles.__path__[0])) + self.Green_Check_Image = self.Green_Check_Image.resize((50,50), Image.LANCZOS) + self.Green_Check_PhotoImage = iTK.PhotoImage(self.Green_Check_Image) + self.Red_X_Image = Image.open("{}/Images/RedX.png".format(PythonFiles.__path__[0])) + self.Red_X_Image = self.Red_X_Image.resize((50,50), Image.LANCZOS) + self.Red_X_PhotoImage = iTK.PhotoImage(self.Red_X_Image) + self.notrun_Image = Image.open("{}/Images/not_yet_run.png".format(PythonFiles.__path__[0])) + self.notrun_Image = self.notrun_Image.resize((50,50), Image.LANCZOS) + self.notrun_PhotoImage = iTK.PhotoImage(self.notrun_Image) + + + ############ + + self.scroller = ttk.Scrollbar(self, orient="vertical", command=self.mycanvas.yview) + self.mycanvas.configure(yscrollcommand=self.scroller.set) + + sidebar_frame.grid_columnconfigure(0, weight=1) + sidebar_frame.grid_rowconfigure(0, weight=1) + + #background="#808080" + + self.mycanvas.grid(row=0, column=0, sticky="ns") + self.scroller.grid(row=0, column=1, sticky="nsw") + + self.canvas_window = self.mycanvas.create_window((0, 0), window=self.viewingFrame, anchor='nw', tags="self.viewingFrame") + self.viewingFrame.pack(fill='y', expand=True, side='left') + + + """ + self.viewingFrame.bind("", self.onFrameConfigure) + self.mycanvas.bind("", self.onCanvasConfigure) + """ + + self.viewingFrame.bind('', self.onEnter) + self.viewingFrame.bind('', self.onLeave) + + #self.onFrameConfigure(None) self.data_holder = data_holder self.update_sidebar(parent) - ################################################# + '''Reset the scroll region to encompass the inner frame''' + self.mycanvas.configure(scrollregion=self.mycanvas.bbox("all")) #whenever the size of the frame changes, alter the scroll region respectively. + """ + def onCanvasConfigure(self, event): + '''Reset the canvas window to encompass inner frame when required''' + canvas_width = event.width + self.mycanvas.itemconfig(self, width = canvas_width) #whenever the size of the canvas changes alter the window region respectively. + """ + + + def create_style(self, _parent): - def update_sidebar(self, parent): + self.s = ttk.Style() - # List for creating check marks with for loop - self.list_of_pass_fail = [ - self.data_holder.test1_pass, - self.data_holder.test2_pass, - self.data_holder.test3_pass, - self.data_holder.test4_pass - ] + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') - # For loop to create checkmarks based on pass/fail - for index in range(len(self.list_of_pass_fail)): - if(self.list_of_pass_fail[index] == True): - # Create a photoimage object of the QR Code - Green_Check_Image = Image.open("./PythonFiles/Images/GreenCheckMark.png") - Green_Check_Image = Green_Check_Image.resize((50,50), Image.ANTIALIAS) - Green_Check_PhotoImage = iTK.PhotoImage(Green_Check_Image) - GreenCheck_Label = tk.Label(self, image=Green_Check_PhotoImage, width=50, height=50, bg = '#808080') - GreenCheck_Label.image = Green_Check_PhotoImage + self.s.theme_use('awdark') - GreenCheck_Label.grid(row=index + 2, column=1) + ################################################# - else: - # Create a photoimage object of the QR Code - Red_X_Image = Image.open("./PythonFiles/Images/RedX.png") - Red_X_Image = Red_X_Image.resize((50,50), Image.ANTIALIAS) - Red_X_PhotoImage = iTK.PhotoImage(Red_X_Image) - RedX_Label = tk.Label(self, image=Red_X_PhotoImage, width=50, height=50, bg = '#808080') - RedX_Label.image = Red_X_PhotoImage + def clean_up_btns(self): + for btn in self.all_btns: + btn.destroy() - RedX_Label.grid(row=index + 2, column=1) + def update_sidebar(self, _parent): + + logger.info("The sidebar has been updated.") # Variables for easy button editing btn_height = 3 btn_width = 18 - btn_font = ('Arial', 10) - btn_pady = 5 + btn_pady = 12 + btn_padx = 15 - self.btn_login = tk.Button( - self, - pady = btn_pady, + self.btn_login = ttk.Button( + self.viewingFrame, text = 'LOGIN PAGE', - height = btn_height, width = btn_width, - font = btn_font ) - self.btn_login.grid(column = 0, row = 0) + self.btn_login.grid(column = 0, row = 0, pady = btn_pady, padx = btn_padx) + - self.btn_scan = tk.Button( - self, - pady = btn_pady, + + self.btn_scan = ttk.Button( + self.viewingFrame, text = 'SCAN PAGE', - height = btn_height, width = btn_width, - font = btn_font ) - self.btn_scan.grid(column = 0, row = 1) + self.btn_scan.grid(column = 0, row = 2, pady = btn_pady, padx = btn_padx) - self.btn_test1 = tk.Button( - self, - pady = btn_pady, - text = 'GEN. RESIST. TEST', - height = btn_height, - width = btn_width, - font = btn_font, - command = lambda: self.btn_test1_action(parent) - ) - self.btn_test1.grid(column = 0, row = 2) + test_names = self.data_holder.getTestNames() + physical_names = self.data_holder.getPhysicalNames() - if self.data_holder.test1_pass == True: - self.btn_test1.config(state = 'disabled') + self.test_btns = [] - - self.btn_test2 = tk.Button( - self, - pady = btn_pady, - text = 'ID RESISTOR TEST', - height = btn_height, + # Offset = number of buttons before the test buttons begin + original_offset = 3 + + # How much offset from the physical board tests + + + for i in range(self.data_holder.getNumPhysicalTest()): + self.test_btns.append(ttk.Button( + self.viewingFrame, + text = '{}'.format(physical_names[i]), + width = btn_width, + command = lambda i=i: self.btn_test_action(_parent, i) + )) + self.test_btns[i].grid(column = 0, row = 3 + i, pady = btn_pady, padx = btn_padx) #i + original_offset) + + + if self.data_holder.data_dict['physical{}_pass'.format(i)] == True: + self.test_btns[i].config(state = 'disabled') + + + + # + ## For the digital buttons + # + digital_offset = 0 + + for i in range(self.data_holder.getNumTest()): + + self.test_btns.append(ttk.Button( + self.viewingFrame, + text = '{}'.format(test_names[i]), + width = btn_width, + command = lambda i=i: self.btn_test_action(_parent, i ) + )) + self.test_btns[i].grid(column = 0, row = 3 + i, pady = btn_pady, padx = btn_pady) #original_offset + i) + + if self.data_holder.data_dict['test{}_pass'.format(i)] == True: + self.test_btns[i].config(state = 'disabled') + + digital_offset = digital_offset + 1 + + self.btn_summary = ttk.Button( + self.viewingFrame, + #pady = btn_pady, + text = 'TEST SUMMARY', + #height = btn_height, width = btn_width, - font = btn_font, - command = lambda: self.btn_test2_action(parent) + #font = btn_font, + command = lambda: self.btn_summary_action(_parent) ) - self.btn_test2.grid(column = 0, row = 3) + self.btn_summary.grid(column = 0, row = 4 + self.data_holder.getNumTest(), pady = btn_pady) - if self.data_holder.test2_pass == True: - self.btn_test2.config(state = 'disabled') - - self.btn_test3 = tk.Button( - self, - pady = btn_pady, - text = 'I2C COMM. TEST', - height = btn_height, + self.restart_server_btn = ttk.Button( + self.viewingFrame, + #pady = btn_pady, + text = 'Restart Server', + #height = btn_height, width = btn_width, - font = btn_font, - command = lambda: self.btn_test3_action(parent) + #font = ('Kozuka Gothic Pr6N L', 8), + command = lambda: self.restart_server(_parent) ) - self.btn_test3.grid(column = 0, row = 4) - - if self.data_holder.test3_pass == True: - self.btn_test3.config(state = 'disabled') - - self.btn_test4 = tk.Button( - self, - pady = btn_pady, - text = 'BIT RATE TEST', - height = btn_height, + self.restart_server_btn.grid(column = 0, row = 5 + self.data_holder.getNumTest(), pady = btn_pady) + + self.reload_firmware_btn = ttk.Button( + self.viewingFrame, + #pady = btn_pady, + text = 'Reload Firmware', + #height = btn_height, width = btn_width, - font = btn_font, - command = lambda: self.btn_test4_action(parent) + #font = ('Kozuka Gothic Pr6N L', 8), + command = lambda: self.reload_firmware(_parent) ) - self.btn_test4.grid(column = 0, row = 5) - if self.data_holder.test4_pass == True: - self.btn_test4.config(state = 'disabled') - - self.btn_summary = tk.Button( - self, - pady = btn_pady, - text = 'TEST SUMMARY', - height = btn_height, + self.reload_firmware_btn.grid(column = 0, row = 6 + self.data_holder.getNumTest(), pady = (btn_pady)) + + self.reset_power_btn = ttk.Button( + self.viewingFrame, + #pady = btn_pady, + text = 'Reset Power', + #height = btn_height, width = btn_width, - font = btn_font, - command = lambda: self.btn_summary_action(parent) + #font = ('Kozuka Gothic Pr6N L', 8), + command = lambda: self.reset_power(_parent) ) - self.btn_summary.grid(column = 0, row = 6) + self.reset_power_btn.grid(column = 0, row = 7 + self.data_holder.getNumTest(), pady = (btn_pady, 235)) + + if (self.data_holder.tester_type == 'Thermal'): + self.btn_scan.grid_forget() + self.scroller.grid_forget() + self.restart_server_btn.grid_forget() + self.reset_power_btn.grid_forget() + self.reload_firmware_btn.grid_forget() + + self.all_btns = [*self.test_btns, self.btn_summary, self.restart_server_btn, self.reload_firmware_btn, self.reset_power_btn] + + + # List for creating check marks with for loop + self.list_of_completion = self.data_holder.data_lists['test_completion'] + self.list_of_pass_fail = self.data_holder.data_lists['test_results'] + + + + # For loop to create checkmarks based on pass/fail + for index in range(len(self.list_of_pass_fail)): + if self.list_of_completion[index] == True: + if(self.list_of_pass_fail[index] == True): + # Create a photoimage object of the QR Code + GreenCheck_Label = tk.Label(self.viewingFrame, image=self.Green_Check_PhotoImage, width=50, height=50, bg = '#33393b') + GreenCheck_Label.image = self.Green_Check_PhotoImage + GreenCheck_Label.grid(row=index + original_offset , column=1, padx = btn_padx) + + else: + # Create a photoimage object of the QR Code + RedX_Label = tk.Label(self.viewingFrame, image=self.Red_X_PhotoImage, width=50, height=50, bg = '#33393b') + RedX_Label.image = self.Red_X_PhotoImage + RedX_Label.grid(row=index + original_offset , column=1, padx = btn_padx) + else: + notrun_Label = tk.Label(self.viewingFrame, image=self.notrun_PhotoImage, width=50, height=50, bg = '#33393b') + notrun_Label.image = self.notrun_PhotoImage + notrun_Label.grid(row=index + original_offset , column=1, padx = btn_padx) + + self.physical_pass_fail = self.data_holder.data_lists['physical_results'] + + # For loop to create checkmarks based on pass/fail + for index in range(len(self.physical_pass_fail)): + if(self.physical_pass_fail[index] == True): + # Create a photoimage object of the QR Code + GreenCheck_Label = tk.Label(self.viewingFrame, image=self.Green_Check_PhotoImage, width=50, height=50, bg = '#33393b') + GreenCheck_Label.image = self.Green_Check_PhotoImage + + GreenCheck_Label.grid(row=index + original_offset, column=1, padx = btn_padx) + + else: + # Create a photoimage object of the QR Code + RedX_Label = tk.Label(self.viewingFrame, image=self.Red_X_PhotoImage, width=50, height=50, bg = '#33393b') + RedX_Label.image = self.Red_X_PhotoImage - self.grid_propagate(0) + RedX_Label.grid(row=index + original_offset, column=1, padx = btn_padx) + ################################################# - def btn_test1_action(self, _parent): - _parent.set_frame_test1() - def btn_test2_action(self, _parent): - _parent.set_frame_test2() + def onMouseWheel(self, event): # cross platform scroll wheel event + if event.num == 4: + self.mycanvas.yview_scroll( -1, "units" ) + elif event.num == 5: + self.mycanvas.yview_scroll( 1, "units" ) + + def onEnter(self, event): # bind wheel events when the cursor enters the control + self.mycanvas.bind_all("", self.onMouseWheel) + self.mycanvas.bind_all("", self.onMouseWheel) + + def onLeave(self, event): # unbind wheel events when the cursorl leaves the control + self.mycanvas.unbind_all("") + self.mycanvas.unbind_all("") - def btn_test3_action(self, _parent): - _parent.set_frame_test3() - def btn_test4_action(self, _parent): - _parent.set_frame_test4() - def btn_summary_action(self, _parent): - _parent.set_frame_test_summary() + ################################################# + + def report_bug(self, _parent): + _parent.report_bug(self) + + def restart_server(self, _parent): + handler = _parent.gui_cfg.getTestHandler() + r = requests.get('http://{}:8899/stop/server'.format(handler['remoteip'])) + logger.info('Stopping server') + logger.debug(r.text) + time.sleep(1) + r = requests.get('http://{}:8899/start/server'.format(handler['remoteip'])) + logger.info('Starting server') + logger.debug(r.text) + + def reload_firmware(self, _parent): + handler = _parent.gui_cfg.getTestHandler() + r = requests.get('http://{}:8899/start/reloadfw'.format(handler['remoteip'])) + logger.info('Reloading firmware') + logger.debug(r.text) + + def reset_power(self, _parent): + handler = _parent.gui_cfg.getTestHandler() + r = requests.get('http://{}:8899/start/cycle_kconn_pwr'.format(handler['remoteip'])) + logger.info('Resetting power') + logger.debug(r.text) + + def btn_test_action(self, _parent, test_idx): + _parent.set_frame_test(test_idx) + + + + def btn_summary_action(self, _parent): + if (self.data_holder.tester_type != 'Thermal'): + _parent.set_frame_test_summary() + else: + response = messagebox.askyesno( + title="Go to Test Results?", + message="Are you sure you want to go the the thermal test summary? Make sure a test has already been run and completed!" + ) + if response: + _parent.set_frame_thermal_final_results() ################################################# def disable_all_btns(self): - self.btn_login.config(state = 'disabled') - self.btn_scan.config(state = 'disabled') - self.btn_test1.config(state = 'disabled') - self.btn_test2.config(state = 'disabled') - self.btn_test3.config(state = 'disabled') - self.btn_test4.config(state = 'disabled') - self.btn_summary.config(state = 'disabled') + for btn in self.all_btns: + btn.config(state = 'disabled') + #self.btn_login.config(state = 'disabled') + #self.btn_scan.config(state = 'disabled') + #for btn in self.test_btns: + # btn.config(state = 'disabled') + #self.btn_summary.config(state = 'disabled') ################################################# def disable_all_but_log_scan(self): - self.btn_test1.config(state = 'disabled') - self.btn_test2.config(state = 'disabled') - self.btn_test3.config(state = 'disabled') - self.btn_test4.config(state = 'disabled') + for btn in self.test_btns: + btn.config(state = 'disabled') self.btn_summary.config(state = 'disabled') ################################################# def disable_all_btns_but_scan(self): self.btn_login.config(state = 'disabled') - self.btn_test1.config(state = 'disabled') - self.btn_test2.config(state = 'disabled') - self.btn_test3.config(state = 'disabled') - self.btn_test4.config(state = 'disabled') + for btn in self.test_btns: + btn.config(state = 'disabled') self.btn_summary.config(state = 'disabled') ################################################# @@ -199,10 +362,8 @@ def disable_all_btns_but_scan(self): def disable_all_btns_but_login(self): self.btn_login.config(state = 'normal') self.btn_scan.config(state = 'disabled') - self.btn_test1.config(state = 'disabled') - self.btn_test2.config(state = 'disabled') - self.btn_test3.config(state = 'disabled') - self.btn_test4.config(state = 'disabled') + for btn in self.test_btns: + btn.config(state = 'disabled') self.btn_summary.config(state = 'disabled') ################################################# @@ -224,4 +385,4 @@ def disable_scan_button(self): ################################################# -################################################################################# \ No newline at end of file +################################################################################# diff --git a/PythonFiles/Scenes/SplashScene.py b/PythonFiles/Scenes/SplashScene.py index adc2c8df..633bc90d 100644 --- a/PythonFiles/Scenes/SplashScene.py +++ b/PythonFiles/Scenes/SplashScene.py @@ -1,14 +1,19 @@ -################################################################################# +################################################################################ # Importing necessary modules import tkinter as tk +import tkinter.ttk as ttk from PIL import ImageTk as iTK from PIL import Image +import logging +import PythonFiles +import os ################################################################################# +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.SplashScene') -class SplashScene(tk.Frame): +class SplashScene(ttk.Frame): ################################################# @@ -16,32 +21,43 @@ def __init__(self, parent, master_frame): self.initialize_GUI(parent, master_frame) ################################################# - + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def initialize_GUI(self, parent, master_frame): - super().__init__(master_frame, width = 850, height = 500) + super().__init__(master_frame, width=1300-213, height = 800) + + self.create_style(parent) # Creating Bethel Logo - img_bethel_logo = Image.open("./PythonFiles/Images/Bethel_Logo.png") - img_bethel_logo = img_bethel_logo.resize((250,100), Image.ANTIALIAS) + img_bethel_logo = Image.open("{}/Images/Bethel_Logo.png".format(PythonFiles.__path__[0])) + img_bethel_logo = img_bethel_logo.resize((250,100), Image.LANCZOS) phimg_bethel_logo = iTK.PhotoImage(img_bethel_logo) - lbl_bethel_logo = tk.Label(self, image=phimg_bethel_logo, width=250, height=100) + lbl_bethel_logo = ttk.Label(self, image=phimg_bethel_logo, width=250) lbl_bethel_logo.image = phimg_bethel_logo lbl_bethel_logo.grid(row=0, column= 0, padx = 50, pady = 100) # Creating UMN Logo - img_umn_logo = Image.open("./PythonFiles/Images/UMN_Logo.png") - img_umn_logo = img_umn_logo.resize((250,100), Image.ANTIALIAS) + img_umn_logo = Image.open('{}/Images/UMN_Logo.png'.format(PythonFiles.__path__[0])) + img_umn_logo = img_umn_logo.resize((250,100), Image.LANCZOS) phimg_umn_logo = iTK.PhotoImage(img_umn_logo) - lbl_umn_logo = tk.Label(self, image=phimg_umn_logo, width=250, height=100) + lbl_umn_logo = ttk.Label(self, image=phimg_umn_logo, width=250) lbl_umn_logo.image = phimg_umn_logo lbl_umn_logo.grid(row = 0 , column = 2, padx = 50, pady = 100) # Creating label for names - lbl_names = tk.Label( + lbl_names = ttk.Label( self, - text = ' Created by:\n \n Bryan Crosman, \n Andrew Kirzeder, \n & \n Garrett Schindler', + text = ' Created by:\n \n Bryan Crossman, \n Andrew Kirzeder, \n Garrett Schindler, \n & Rand Bovard', font = ('Arial', 15) ) lbl_names.grid(row = 1, column = 1) @@ -51,4 +67,4 @@ def initialize_GUI(self, parent, master_frame): ################################################# -################################################################################# \ No newline at end of file +################################################################################# diff --git a/PythonFiles/Scenes/TestInProgressScene.py b/PythonFiles/Scenes/TestInProgressScene.py index 8be34ccb..cb1a5f77 100644 --- a/PythonFiles/Scenes/TestInProgressScene.py +++ b/PythonFiles/Scenes/TestInProgressScene.py @@ -1,39 +1,50 @@ -################################################################################# +################################################################################ # Imports all the necessary modules import tkinter as tk -from tkinter import ttk +import tkinter.ttk as ttk +from tkinter import messagebox from xml.dom.expatbuilder import parseFragmentString import time - - +import logging +import PythonFiles +import os ################################################################################# - +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.TestInProgressScene') # Creating the frame itself -class TestInProgressScene(tk.Frame): - def __init__(self, parent, master_frame, data_holder, queue): +class TestInProgressScene(ttk.Frame): + def __init__(self, parent, master_frame, data_holder, queue, _conn): - super().__init__(master_frame, width = 850, height = 500) - + super().__init__(master_frame, width=1300-213, height = 800) + self.create_style(parent) self.queue = queue self.data_holder = data_holder self.is_current_scene = False self.initialize_scene(parent, master_frame) + self.conn = _conn + + def create_style(self, _parent): + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') ################################################## # A function for the stop button - def btn_stop_action(self, _parent): - - _parent.go_to_next_test() + #def btn_stop_action(self, _parent): + # self.window_closed = True + # _parent.go_to_next_test() - - # Destroys the console window - self.console_destroy() + # + # # Destroys the console window + # self.console_destroy() ################################################# @@ -52,8 +63,8 @@ def go_to_previous_frame(self, _parent, previous_frame): # Used to initialize the frame that is on the main window # next_frame is used to progress to the next scene and is passed in from GUIWindow def initialize_scene(self, parent, master_frame): - - scrollbar = tk.Scrollbar(self) + + scrollbar = ttk.Scrollbar(self) scrollbar.pack(side = "right", fill = 'y') @@ -64,7 +75,7 @@ def initialize_scene(self, parent, master_frame): bg = 'black', fg = 'white', height= 15, - width= 400, + width= 75, font = ('Arial', 15), yscrollcommand = scrollbar.set ) @@ -75,20 +86,23 @@ def initialize_scene(self, parent, master_frame): # Creating the main title in the frame - lbl_title = tk.Label(self, + lbl_title = ttk.Label(self, text = "Test in progress. Please wait.", - font = ('Arial', 20) + font = ('Arial', 32) ) lbl_title.pack(padx = 0, pady = 50) # Create a progress bar that does not track progress but adds motion to the window - self.prgbar_progress = ttk.Progressbar( + self.progressbar = ttk.Progressbar( self, orient = 'horizontal', mode = 'indeterminate', length = 350) - self.prgbar_progress.pack(padx = 50) - self.prgbar_progress.start() - + self.progressbar.pack(padx = 50) + self.stop_txt = ttk.Label(self, + text='Waiting for test to finish...', + font=('Arial', 15) + ) + self.stop_txt.pack_forget() # A Button To Stop the Progress Bar and Progress Forward (Temporary until we link to actual progress) btn_stop = ttk.Button( self, @@ -105,15 +119,19 @@ def initialize_scene(self, parent, master_frame): # A function for the stop button def btn_stop_action(self, _parent): + #_parent.return_to_current_test() + self.progressbar.stop() + self.stop_txt.pack(padx=0, pady=50) + _parent.stop_tests() + #self.queue.put('Stop') - _parent.go_to_next_test() - - + def remove_stop_txt(self): + self.stop_txt.pack_forget() # Goes to the next scene after the progress scene is complete def go_to_next_frame(self, _parent): _parent.go_to_next_test() - + self.window_closed = True # Used to bring the user back to the test that just failed @@ -121,57 +139,182 @@ def go_to_previous_frame(self, _parent, previous_frame): self.previous_frame = previous_frame _parent.set_frame(previous_frame) - + ################################################# - # Dummy Script Function - def run_test_gen_resist(self): - print ("General Resistance Test Run") + def begin_update(self, master_window, queue, parent): - ################################################# + logger.info("TestInProgressScene: Started console update loop.") + + # How long before the queue is being checked (if empty) + # units of seconds + refresh_break = 0.01 + + # Time spent in the waiting phase; in units of refresh_break + # Time waiting (sec) = counter * refresh_break + counter = 0 + + self.progressbar.start(10) + + self.window_closed = False + + # Maximum timeout in seconds + Timeout_after = 10 + MAX_TIMEOUT = Timeout_after / 2.5 + try: + logger.info("TestInProgressScene: Beginning the while loop") + + information_received = False + while 1>0: + master_window.update() + if not queue.empty(): + information_received = True + text = queue.get() + ent_console.insert(tk.END, text.strip('\r\n')) + # need this twice since the first one is stripped from the original text + ent_console.insert(tk.END, "\n") + ent_console.insert(tk.END, "\n") + ent_console.see('end') + + if "Done." in text: + logger.info("Stopping Progress Bar.") + self.progressbar.stop() + + if "Exit." in text: + self.progressbar.stop() + time.sleep(1) + parent.test_error_popup("Unable to run test") + logger.error("Unable to run test.") + break + + if "Results received successfully." in text: + + message = self.conn.recv() + self.data_holder.update_from_json_string(message) + + logger.info("JSON Received.") + logger.debug(message) +# FinishedTestPopup(parent, self.data_holder, queue) +# +# if "Closing Test Window." in text: + logger.info("TestInProgressScene: ending loop") + try: + master_window.update() + except Exception as e: + logger.warning('Unable to update master_window') + logger.exception(e) + + time.sleep(0.02) + break + + if self.window_closed == True: + self.progressbar.destroy() + break + + #else: + # + # print("TestInProgressScene: The queue is empty, going to sleep for {} seconds".format(refresh_break)) + + # # Sleep before looking for more information + # time.sleep(refresh_break) + + # # Increment the counter of time spent sleeping + # counter = counter + 1 + + # # If beyond the MAX_TIMEOUT range -> raise an exception + # if (counter > MAX_TIMEOUT/refresh_break) and not information_received: + # print("\n\nTestInProcessScene: Raising an exception now\n") + # logger.info("TestInProgressScene: Raising Exception -> Timeout Reached - 10 seconds") + # raise ValueError("Process timed out after 10 seconds") + # time.sleep(1) + # break + except ValueError as e: + + logger.exception(e) + + # Throw a message box that shows the error message + # Logs the message + time_sec = counter*refresh_break + logger.error('Timeout Error', "Exception received -> Process timed out after 10 seconds") + + messagebox.showwarning('Timeout Error', "TestInProgressScene: Process timed out after 10 seconds") + logger.info("Trying to go back to the login frame.") + parent.set_frame_login_frame() + return False + + #except Exception as e: + + # print("\n\nException: ", e, "\n\n") + + return True - # Dummy Script Function - def run_test_id_resistor(self): - print ("ID Resistor Test Run") +######################################################### + +class FinishedTestPopup(): + ################################################# - # Dummy Script Function - def run_test_i2c_comm(self): - print("I2C Comm. Test Run") + def __init__(self, parent, data_holder, queue): + self.create_style(parent) + self.finished_popup(data_holder) + self.parent = parent + self.queue = queue + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') ################################################# - # Dummy Script Function - def run_test_bit_rate(self): - print("Bit Rate Test Run") + def finished_popup(self, data_holder): + self.data_holder = data_holder + logger.info("Test Finished") + # Creates a popup to ask whether or not to retry the test + self.popup = tk.Toplevel() + self.popup.title("Test Completed") + self.popup.geometry("300x200+500+300") + self.popup.pack_propagate(1) + self.popup.grid_columnconfigure(0, weight=1) # Make the master frame resizable + self.popup.grid_rowconfigure(0, weight=1) + self.popup.grab_set() + + # Creates frame in the new window + frm_popup = ttk.Frame(self.popup, width=300, height=200) + frm_popup.grid(row=0, column=0, sticky='nsew') + + bind_func = self.continue_function + frm_popup.bind_all("", lambda event: bind_func(self.parent)) + + # Creates label in the frame + lbl_popup = ttk.Label( + frm_popup, + text = "Test has finished running.", + font = ('Arial', 13) + ) + lbl_popup.grid(column = 0, row = 0) + + btn_continue = ttk.Button( + frm_popup, + text = "Continue", + command = lambda: self.continue_function(self.parent) + ) + btn_continue.grid(column = 0, row = 1) + frm_popup.grid_columnconfigure(0, weight=1) + frm_popup.grid_rowconfigure(0, weight=1) + frm_popup.grid_rowconfigure(1, weight=1) + + ################################################# - def begin_update(self, master_window, queue): - print("started update loop") - # try: - while 1>0: - # try: - master_window.update() - if not queue.empty(): - print("Waiting for queue objects...") - text = queue.get() - print(text) - ent_console.insert(tk.END, text) - ent_console.insert(tk.END, "\n") - ent_console.see('end') - - if text == "JSON Received.": - print("JSON Received.") - master_window.update() - time.sleep(1) - break - - else: - time.sleep(.01) - - def close_prgbar(self): - print("Closing the progressbar.") - self.prgbar_progress.stop() - self.prgbar_progress.destroy() - print("Progressbar succesfully closed.") + # Called to continue on in the testing procedure + def continue_function(self, _parent): + self.popup.destroy() + self.queue.put('Closing Test Window.') + + diff --git a/PythonFiles/Scenes/TestScene.py b/PythonFiles/Scenes/TestScene.py index 74777b6d..94d747bd 100644 --- a/PythonFiles/Scenes/TestScene.py +++ b/PythonFiles/Scenes/TestScene.py @@ -2,163 +2,237 @@ # Importing Necessary Modules import tkinter as tk +import tkinter.ttk as ttk +from tkinter import messagebox import tkinter.font as font +import logging +import PythonFiles +import os # Importing Necessary Files from PythonFiles.utils.REQClient import REQClient ################################################################################# +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.TestScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) # Creating class for the window -class TestScene(tk.Frame): +class TestScene(ttk.Frame): ################################################# - def __init__(self, parent, master_frame, data_holder, test_name, queue): - super().__init__(master_frame, width=850, height=500) + def __init__(self, parent, master_frame, data_holder, test_name, test_description_short, test_description_long, queue, conn_trigger, test_idx): + super().__init__(master_frame, width=1300-213, height = 800) self.queue = queue + self.conn_trigger = conn_trigger self.test_name = test_name + self.test_description_short = test_description_short + self.test_description_long = test_description_long self.data_holder = data_holder + self.test_idx = test_idx + self.parent = parent self.update_frame(parent) ################################################# - def update_frame(self, parent): + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + self.s.theme_use('awdark') + + def update_frame(self, parent): # Creates a font to be more easily referenced later in the code font_scene = ('Arial', 15) - + + self.create_style(parent) # Create a centralized window for information - frm_window = tk.Frame(self, width = 850, height = 500) - frm_window.grid(column=1, row=1, padx = 235, pady = 109) + frm_window = ttk.Frame(self, width=870, height = 480) + frm_window.grid(column=0, row=0, sticky='nsew') + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + frm_window.columnconfigure(0, weight=1) + frm_window.rowconfigure(0, weight=1) # Create a label for the tester's name - lbl_tester = tk.Label( + lbl_tester = ttk.Label( frm_window, text = "Tester: ", - font = font_scene + font = ('Arial', '24') ) - lbl_tester.pack(side = 'top') - + lbl_tester.pack(side = 'top', pady = 10) + # Create an entry for the tester's name ent_tester = tk.Entry( frm_window, - font = font_scene ) - ent_tester.insert(0, self.data_holder.user_ID) - ent_tester.pack(side = 'top') + ent_tester.insert(0, self.data_holder.data_dict['user_ID']) + ent_tester.pack(side = 'top', pady = 10) ent_tester.config(state = "disabled") - # Create a label for the serial number box - lbl_snum = tk.Label( + # Create a label for the full id box + lbl_full = ttk.Label( frm_window, - text = "Serial Number: ", - font = font_scene + text = "Full ID: ", + font = ('Arial', '24') ) - lbl_snum.pack(side = 'top') + lbl_full.pack(side = 'top', pady = 10) - # Create a entry for the serial number box - ent_snum = tk.Entry( + # Create a entry for the full id box + ent_full = tk.Entry( frm_window, - font = font_scene + #font = font_scene ) - ent_snum.insert(0, self.data_holder.current_serial_ID) - ent_snum.pack(side = 'top') - ent_snum.config(state = "disabled") + ent_full.insert(0, self.data_holder.data_dict['current_full_ID']) + ent_full.pack(side = 'top', pady = 10) + ent_full.config(state = "disabled") # Create a label for the test about to be run - lbl_test = tk.Label( + lbl_test = ttk.Label( frm_window, text = "Current Test: ", - font = font_scene + font = ('Arial', '24') ) - lbl_test.pack(side = 'top') - + lbl_test.pack(side = 'top', pady = 10) # Create a entry for the test type self.ent_test = tk.Entry( frm_window, - font = font_scene ) - self.ent_test.pack(side = 'top') + self.ent_test.pack(side = 'top', pady = 10) self.ent_test.insert(0, self.test_name) self.ent_test.config(state = "disabled") # Create a label for confirming test - lbl_confirm = tk.Label( + lbl_confirm = ttk.Label( frm_window, text = "Are you ready to begin the test?", - font = font_scene + font = ('Arial', '24') + ) + lbl_confirm.pack(side = 'top', pady = 15) + + self.lbl_desc_short = ttk.Label( + frm_window, + text = self.test_description_short, + wraplength = 500, + justify="center", + font = ('Arial', '24') ) - lbl_confirm.pack(side = 'top') - # Create a button for confirming test - btn_confirm = tk.Button( - frm_window, - text = "Confirm", - relief = tk.RAISED, - command = lambda:self.btn_confirm_action(parent) + self.lbl_desc_short.pack(side = 'top', pady = 15) + + self.lbl_desc = ttk.Label( + frm_window, + text = self.test_description_long, + wraplength = 800, + justify="center", + font = ('Arial', '24') ) - btn_confirm.pack(side = 'top') - btn_confirm['font'] = font.Font(family = 'Arial', size = 13) + self.lbl_desc.pack(side = 'top', pady = 15) # Create frame for logout button - frm_logout = tk.Frame(self) - frm_logout.grid(column = 2, row = 2, sticky = 'ne') + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 2, row = 1, padx = 5, sticky = 'ew') + frm_logout.columnconfigure(0, weight=1) # Create a logout button - btn_logout = tk.Button( + btn_logout = ttk.Button( frm_logout, text = "Logout", - relief = tk.RAISED, + #relief = tk.RAISED, command = lambda: self.btn_logout_action(parent)) - btn_logout.pack(anchor = 'se') + btn_logout.pack(anchor = 'center', pady = 5) + + # Create a button for confirming test + btn_confirm = ttk.Button( + frm_logout, + text = "Confirm", + #relief = tk.RAISED, + command = lambda:self.btn_confirm_action(parent) + ) + btn_confirm.pack(anchor = 'center', pady = 5) + + #if (self.test_idx == 0): + + # Create a button for confirming test + run_all_btn = ttk.Button( + frm_logout, + text = "Run All Tests", + command = lambda:self.run_all_action(parent), + ) + run_all_btn.pack(anchor = 'center', pady = 5) - # Create a frame for the back button - frm_back = tk.Frame(self) - frm_back.grid(column = 2, row = 0, sticky = 'ne') # Create a rescan button - btn_rescan = tk.Button( - frm_back, + btn_rescan = ttk.Button( + frm_logout, text = "Change Boards", - relief = tk.RAISED, + #relief = tk.RAISED, command = lambda: self.btn_rescan_action(parent)) - btn_rescan.pack(anchor = 'ne') - - + btn_rescan.pack(anchor = 'center', pady = 5) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'center', pady = 5) self.grid_propagate(0) + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + def run_all_action(self, _parent): + + _parent.run_all_tests() + ################################################# # Rescan button takes the user back to scanning in a new board def btn_rescan_action(self, _parent): - _parent.set_frame_scan_frame() + _parent.reset_board() ################################################# # Confirm button action takes the user to the test in progress scene def btn_confirm_action(self, _parent): - # # # # # # # # # # # # # # # # # # # # # # # # # # # - # ++ GOAL CODE ++ # - # def confirm(): # - # set_frame_TIPS() # - # Runs_Test() # Might include multithread # - # Get_Results() # - # Update_Dataholder() # - # Go_To_Next_Test() # - # # # # # # # # # # # # # # # # # # # # # # # # # # # - # _parent.set_frame_test_in_progress(self.queue) - pass + self.gui_cfg = self.data_holder.getGUIcfg() + cur_name = self.gui_cfg.getTests()[self.test_idx]['name'] + + #try: + test_client = REQClient(self.gui_cfg, cur_name.strip().replace(" ", ""), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID'], self.conn_trigger) + #test_client = REQClient(self.gui_cfg, 'test{}'.format(self.test_idx), self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID'], self.conn_trigger) + #except Exception as e: + # messagebox.showerror('Exception', e) + + _parent.set_frame_test_in_progress(self.queue) + + def get_submit_action(self): + return self.btn_confirm_action + def get_parent(self): + return self.parent + ################################################# # functionality for the logout button def btn_logout_action(self, _parent): + logger.info("Successfully logged out from the TestScene.") _parent.set_frame_login_frame() ################################################# @@ -168,32 +242,28 @@ def btn_logout_action(self, _parent): class Test1Scene(TestScene): + + logger.info("Test1Scene: Frame has successfully been created.") + # Override to add specific functionality def btn_confirm_action(self, _parent): - self.data_holder.test1_completed = True - self.data_holder.test1_pass = True - self.data_holder.print() super().btn_confirm_action(_parent) - test_1_client = REQClient('test1', self.data_holder.current_serial_ID, self.data_holder.user_ID) + test_1_client = REQClient('test1', self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID']) _parent.set_frame_test_in_progress(self.queue) - #TODO EDIT THIS WITH ACTUAL TEST DATA - ################################################################################# class Test2Scene(TestScene): + + logger.info("Test2Scene: Frame has successfully been created.") + # Override to add specific functionality def btn_confirm_action(self, _parent): - self.data_holder.test2_completed = True - self.data_holder.test2_pass = False - self.data_holder.print() super().btn_confirm_action(_parent) - test_2_client = REQClient('test2', self.data_holder.current_serial_ID, self.data_holder.user_ID) + test_2_client = REQClient('test2', self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID']) _parent.set_frame_test_in_progress(self.queue) - #TODO EDIT THIS WITH ACTUAL TEST DATA - @@ -201,31 +271,30 @@ def btn_confirm_action(self, _parent): class Test3Scene(TestScene): + + logger.info("Test3Scene: Frame has successfully been created.") + # Override to add specific functionality def btn_confirm_action(self, _parent): - self.data_holder.test3_completed = True - self.data_holder.test3_pass = True - self.data_holder.print() super().btn_confirm_action(_parent) - test_3_client = REQClient('test3', self.data_holder.current_serial_ID, self.data_holder.user_ID) + test_3_client = REQClient('test3', self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID']) _parent.set_frame_test_in_progress(self.queue) - #TODO EDIT THIS WITH ACTUAL TEST DATA ################################################################################# class Test4Scene(TestScene): + + logger.info("Test4Scene: Frame has successfully been created.") + # Override to add specific functionality def btn_confirm_action(self, _parent): - self.data_holder.test4_completed = True - self.data_holder.test4_pass = True - self.data_holder.print() super().btn_confirm_action(_parent) - test_4_client = REQClient('test4', self.data_holder.current_serial_ID, self.data_holder.user_ID) + test_4_client = REQClient('test4', self.data_holder.data_dict['current_full_ID'], self.data_holder.data_dict['user_ID']) _parent.set_frame_test_in_progress(self.queue) - #TODO EDIT THIS WITH ACTUAL TEST DATA ################################################################################# + diff --git a/PythonFiles/Scenes/TestSummaryScene.py b/PythonFiles/Scenes/TestSummaryScene.py index 2aff9311..2f4087a5 100644 --- a/PythonFiles/Scenes/TestSummaryScene.py +++ b/PythonFiles/Scenes/TestSummaryScene.py @@ -1,343 +1,412 @@ ################################################################################# -import json +import PythonFiles +import json, logging import tkinter as tk +import tkinter.ttk as ttk from PIL import ImageTk as iTK from PIL import Image from matplotlib.pyplot import table from pyparsing import col +import PythonFiles +import os +from pathlib import Path ################################################################################# - +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.TestSummaryScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) # Frame that shows all of the final test results # @param parent -> References a GUIWindow object # @param master_frame -> Tkinter object that the frame is going to be placed on # @param data_holder -> DataHolder object that stores all relevant data -class TestSummaryScene(tk.Frame): +class TestSummaryScene(ttk.Frame): ################################################# def __init__(self, parent, master_frame, data_holder): self.parent = parent - + self.create_style(parent) # Call to the super class's constructor # Super class is the tk.Frame class - super().__init__(master_frame, width=850, height=500) + super().__init__(master_frame, width=1300-213, height=800) + + master_frame.grid_rowconfigure(0, weight=1) + master_frame.grid_columnconfigure(0, weight=1) + + + self.id_text = tk.StringVar() self.data_holder = data_holder - # Setting weights of columns so the column 4 is half the size of columns 0-3 - self.columnconfigure(0, weight = 2) - self.columnconfigure(1, weight = 2) - self.columnconfigure(2, weight = 2) - self.columnconfigure(3, weight = 2) - self.columnconfigure(4, weight = 1) # Instantiates an updated table with the current data - self.create_updated_table(parent) + #self.create_updated_table(parent) + + self.parent = parent # Adds the title to the TestSummary Frame - self.title = tk.Label( + self.title = ttk.Label( self, - fg='#0d0d0d', + #fg='#0d0d0d', text = "Testing Finished!", - font=('Arial',18,'bold') + font=('Arial',28,'bold') ) - self.title.grid(row= 0, column= 1, pady = 20) + self.title.grid(row= 0, column= 1, pady = 20, sticky='ew') + + self.id_text.set("Full ID: " + str(self.data_holder.data_dict['current_full_ID'])) + + # Adds Board Full ID to the TestSummaryFrame + self.lbl_id = ttk.Label( + self, + textvariable = self.id_text, + font=('Arial', 20) + ) + self.lbl_id.grid(column = 2, row = 0, pady = 20, padx = 5, sticky='ew') + # Fits the frame to set size rather than interior widgets self.grid_propagate(0) + ################################################# + def create_style(self, _parent): + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') # Creates the table with the updated information from the data_holder # @param parent -> References the GUIWindow object that creates the class def create_updated_table(self, parent): - + + logger.info("Summary table is being updated.") - self.list_of_tests = ["General Resistance Test", "ID Resistor Test", "I2C Comm. Test", "Bit Rate Test"] - self.list_of_table_labels = ["Test Name", "Test Status", "Pass/Fail"] - self.list_of_completed_tests = [self.data_holder.test1_completed, self.data_holder.test2_completed, self.data_holder.test3_completed, self.data_holder.test4_completed] - self.list_of_pass_fail = [self.data_holder.test1_pass, self.data_holder.test2_pass, self.data_holder.test3_pass, self.data_holder.test4_pass] + self.list_of_tests =self.data_holder.getPhysicalNames() + self.data_holder.getTestNames() + self.list_of_table_labels = ["Test Name", "Test Status", "Actions"] + self.list_of_completed_tests = self.data_holder.data_lists['physical_completion'] + self.data_holder.data_lists['test_completion'] + self.list_of_pass_fail = self.data_holder.data_lists['physical_results'] + self.data_holder.data_lists['test_results'] + self.test_results = self.data_holder.get_test_results() - # Adds Board Serial Number to the TestSummaryFrame - self.lbl_snum = tk.Label( - self, - text = "Serial Number: " + str(self.data_holder.current_serial_ID), - font=('Arial', 14) - ) - self.lbl_snum.grid(column = 2, row = 0, pady = 20) + + #Checks for duplicate test names, which cause problems with saving the json files + prev_names = set() + + for name in self.list_of_tests: + if name in prev_names: + logger.warning(f'Warning, Duplicate test name found: {name}') + else: + prev_names.add(name) + + + self.id_text.set("Full ID: " + str(self.data_holder.data_dict['current_full_ID'])) # Adds Tester Name to the TestSummary Frame - self.lbl_tester = tk.Label( + self.lbl_tester = ttk.Label( self, - text = "Tester: " + self.data_holder.user_ID, - font=('Arial', 14) + text = "Tester: " + self.data_holder.data_dict['user_ID'], + font=('Arial', 24) ) - self.lbl_tester.grid(column = 0, row = 0, pady = 20) - - - # Creates the "table" as a frame object - self.frm_table = tk.Frame(self) - self.frm_table.grid(row = 1, column= 0, columnspan = 4, rowspan = 4) - - # Setting weights of columns so the column 4 is half the size of columns 0-3 - self.frm_table.columnconfigure(0, weight = 2) - self.frm_table.columnconfigure(1, weight = 2) - self.frm_table.columnconfigure(2, weight = 2) - self.frm_table.columnconfigure(3, weight = 1) - self.frm_table.columnconfigure(4, weight = 1) - + self.lbl_tester.grid(column = 3, row = 0, pady = 20, padx = 5, sticky='ew') + + self.columnconfigure(0, weight = 1) + self.columnconfigure(1, weight = 1) + self.columnconfigure(2, weight = 1) + self.columnconfigure(3, weight = 1) + + + + ########## + + self.mycanvas = tk.Canvas(self) + self.viewingFrame = ttk.Frame(self.mycanvas) + self.scroller = ttk.Scrollbar(self, orient="vertical", command=self.mycanvas.yview) + self.mycanvas.configure(yscrollcommand=self.scroller.set) + + self.mycanvas.grid(row = 3, column = 0, columnspan = 4, sticky="nsew") + self.scroller.grid(row = 3, column = 4, sticky = 'nsw') + + + self.canvas_window = self.mycanvas.create_window((0,0), window=self.viewingFrame, anchor='nw', tags="self.viewingFrame") + #self.viewingFrame.pack(fill='both', expand=True) + + + self.viewingFrame.columnconfigure(0, weight = 1) + self.viewingFrame.columnconfigure(1, weight = 1) + self.viewingFrame.columnconfigure(2, weight = 1) + self.viewingFrame.columnconfigure(3, weight = 1) + self.rowconfigure(3, weight = 1) + + self.mycanvas.bind('', self.onCanvasConfigure) + self.viewingFrame.bind('', self.onFrameConfigure) + self.viewingFrame.bind('', self.onEnter) + self.viewingFrame.bind('', self.onLeave) + + # Adds the labels to the top of the table for index in range(len(self.list_of_table_labels)): - _label = tk.Label( - self.frm_table, + _label = ttk.Label( + self.viewingFrame, text = self.list_of_table_labels[index], - relief = 'ridge', + #relief = 'ridge', width=25, - height=1, - font=('Arial', 11, "bold") + #height=1, + font=('Arial', 24, "bold") ) - _label.grid(row= 0, column=index) - - - # Adds the test names to the first column - for index in range(len(self.list_of_tests)): - _label= tk.Label( - self.frm_table, - text = self.list_of_tests[index], - relief = 'ridge', + _label.grid(row= 3, column=index, sticky='nsew', padx = 15, pady = 15) + + Green_Check_Image = Image.open("{}/Images/GreenCheckMark.png".format(PythonFiles.__path__[0])) + Green_Check_Image = Green_Check_Image.resize((50,50), Image.LANCZOS) + Green_Check_PhotoImage = iTK.PhotoImage(Green_Check_Image) + Red_X_Image = Image.open("{}/Images/RedX.png".format(PythonFiles.__path__[0])) + Red_X_Image = Red_X_Image.resize((50,50), Image.LANCZOS) + Red_X_PhotoImage = iTK.PhotoImage(Red_X_Image) + notrun_Image = Image.open("{}/Images/not_yet_run.png".format(PythonFiles.__path__[0])) + notrun_Image = notrun_Image.resize((50,50), Image.LANCZOS) + notrun_PhotoImage = iTK.PhotoImage(notrun_Image) + + for index,key in enumerate(self.list_of_tests): + _label= ttk.Label( + self.viewingFrame, + text = key, + #relief = 'ridge', width=25, - height=5, - font=('Arial', 11) + #height=3, + font=('Arial', 16) ) - _label.grid(row=index + 1, column=0) - - - - # Create Labels that tell whether or not a test was completed - for index in range(len(self.list_of_completed_tests)): - - # Instantiates a Label - _label = tk.Label( - self.frm_table, - relief = 'ridge', - width=25, - height=5, - font=('Arial',11) - ) - - # if the test is completed, set the label to "Complete" - if (self.list_of_completed_tests[index]): - _label.config( - text = "COMPLETED" - ) - # else, set the label to "Unfinished" - else: - _label.config( - text = "UNFINISHED" - ) - - # Puts the completed/unfinished label into the table - _label.grid(row=index + 1, column=1) - - - # Adds the Image as to whether the test was completed or not - for index in range(len(self.list_of_pass_fail)): - if(self.list_of_pass_fail[index]): - # Create a photoimage object of the QR Code - Green_Check_Image = Image.open("./PythonFiles/Images/GreenCheckMark.png") - Green_Check_Image = Green_Check_Image.resize((75,75), Image.ANTIALIAS) - Green_Check_PhotoImage = iTK.PhotoImage(Green_Check_Image) - GreenCheck_Label = tk.Label(self.frm_table, image=Green_Check_PhotoImage, width=75, height=75) + _label.grid(row=index + 5, column=0, sticky='nsew', padx = 15, pady = 15) + if self.test_results[key] == 'Passed': + GreenCheck_Label = ttk.Label(self.viewingFrame, image=Green_Check_PhotoImage, width=75) GreenCheck_Label.image = Green_Check_PhotoImage - GreenCheck_Label.grid(row=index + 1, column=2) + GreenCheck_Label.grid(row=index + 5, column=1, pady = 15) - else: - # Create a photoimage object of the QR Code - Red_X_Image = Image.open("./PythonFiles/Images/RedX.png") - Red_X_Image = Red_X_Image.resize((75,75), Image.ANTIALIAS) - Red_X_PhotoImage = iTK.PhotoImage(Red_X_Image) - RedX_Label = tk.Label(self.frm_table, image=Red_X_PhotoImage, width=75, height=75) + elif self.test_results[key] == 'Failed': + RedX_Label = ttk.Label(self.viewingFrame, image=Red_X_PhotoImage, width=75) RedX_Label.image = Red_X_PhotoImage - RedX_Label.grid(row=index + 1, column=2) + RedX_Label.grid(row=index + 5, column=1, pady = 15) + else: + notrun_Label = ttk.Label(self.viewingFrame, image=notrun_PhotoImage, width=75) + notrun_Label.image = notrun_PhotoImage + notrun_Label.grid(row=index + 5, column=1, pady = 15) + + - self.create_retest_more_info_btns(parent) - - self.grid_propagate(0) + ## Adds the test names to the first column + #for index in range(len(self.list_of_tests)): + # _label= ttk.Label( + # self.viewingFrame, + # text = self.list_of_tests[index], + # #relief = 'ridge', + # width=25, + # #height=3, + # font=('Arial', 16) + # ) + # _label.grid(row=index + 5, column=0, sticky='nsew', padx = 15, pady = 15) + # + + + ## Create Labels that tell whether or not a test was completed + #for index in range(len(self.list_of_completed_tests)): + # + # # Instantiates a Label + # _label = ttk.Label( + # self.viewingFrame, + # width=25, + # font=('Arial',16) + # ) + + # # if the test is completed, set the label to "Complete" + # if (self.list_of_completed_tests[index]): + # _label.config( + # text = "COMPLETED", + # justify = "center" + # ) + # # else, set the label to "Unfinished" + # else: + # _label.config( + # text = "UNFINISHED", + # justify = "center" + # ) + + # # Puts the completed/unfinished label into the table + # _label.grid(row=index + 5, column=1, sticky="ew", padx = 10, pady = 15) + + + ## Adds the Image as to whether the test was completed or not + #for index in range(len(self.list_of_pass_fail)): + # if(self.list_of_pass_fail[index]): + # # Create a photoimage object of the QR Code + # Green_Check_Image = Image.open("{}/Images/GreenCheckMark.png".format(PythonFiles.__path__[0])) + # Green_Check_Image = Green_Check_Image.resize((50,50), Image.LANCZOS) + # Green_Check_PhotoImage = iTK.PhotoImage(Green_Check_Image) + # GreenCheck_Label = ttk.Label(self.viewingFrame, image=Green_Check_PhotoImage, width=75) + # GreenCheck_Label.image = Green_Check_PhotoImage + + # GreenCheck_Label.grid(row=index + 5, column=2, pady = 15) + + # else: + # # Create a photoimage object of the QR Code + # Red_X_Image = Image.open("{}/Images/RedX.png".format(PythonFiles.__path__[0])) + # Red_X_Image = Red_X_Image.resize((50,50), Image.LANCZOS) + # Red_X_PhotoImage = iTK.PhotoImage(Red_X_Image) + # RedX_Label = ttk.Label(self.viewingFrame, image=Red_X_PhotoImage, width=75) + # RedX_Label.image = Red_X_PhotoImage + + # RedX_Label.grid(row=index + 5, column=2, pady = 15) - ################################################# - # Creates all of the retest button - def create_retest_more_info_btns(self, parent): + self.create_retest_more_info_btns(parent) - - row1 = tk.Frame(self.frm_table) - row1.grid(column = 3, row = 1) - - btn_retest1 = tk.Button( - row1, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest1_action(parent) - ) - btn_retest1.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info1 = tk.Button( - row1, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info1_action(parent) - ) - btn_more_info1.grid(column=0, row = 0) + ################################################# + ################################################# + def onFrameConfigure(self, event): + self.mycanvas.configure(scrollregion=self.mycanvas.bbox('all')) + def onCanvasConfigure(self, event): + self.mycanvas.itemconfig(self.canvas_window, width=event.width) - row2 = tk.Frame(self.frm_table) - row2.grid(column = 3, row = 2) - - btn_retest2 = tk.Button( - row2, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest2_action(parent) - ) - btn_retest2.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info2 = tk.Button( - row2, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info2_action(parent) - ) - btn_more_info2.grid(column=0, row = 0) + def onMouseWheel(self, event): # cross platform scroll wheel event + if event.num == 4: + self.mycanvas.yview_scroll( -1, "units" ) + elif event.num == 5: + self.mycanvas.yview_scroll( 1, "units" ) + def onEnter(self, event): # bind wheel events when the cursor enters the control + self.mycanvas.bind_all("", self.onMouseWheel) + self.mycanvas.bind_all("", self.onMouseWheel) + def onLeave(self, event): # unbind wheel events when the cursorl leaves the control + self.mycanvas.unbind_all("") + self.mycanvas.unbind_all("") - row3 = tk.Frame(self.frm_table) - row3.grid(column = 3, row = 3) - - btn_retest3 = tk.Button( - row3, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest3_action(parent) - ) - btn_retest3.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info3 = tk.Button( - row3, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info3_action(parent) - ) - btn_more_info3.grid(column=0, row = 0) - - - + ################################################# - - row4 = tk.Frame(self.frm_table) - row4.grid(column = 3, row = 4) - - btn_retest4 = tk.Button( - row4, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest4_action(parent) - ) - btn_retest4.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info4 = tk.Button( - row4, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info4_action(parent) - ) - btn_more_info4.grid(column=0, row = 0) + ################################################# + # Creates all of the retest button + def create_retest_more_info_btns(self, parent): + rows = [] + retests = [] + more_infos = [] + + for i in range(len(self.list_of_tests)): + rows.append(ttk.Frame(self.viewingFrame)) + rows[i].grid(column = 2, row = i + 5) + + retests.append(ttk.Button( + rows[i], + text = "RETEST", + #padx= 5, + #pady=3, + command = lambda i=i: self.btn_retest_action(parent, i) + )) + retests[i].grid(column = 0, row = i , pady = 15) + + more_infos.append(ttk.Button( + rows[i], + text = "MORE INFO", + #padx= 5, + #pady=3, + command = lambda i=i: self.btn_more_info_action(parent, i) + )) + more_infos[i].grid(column=1, row = i , pady = 15) + + rows[i].columnconfigure(0, weight=1) + rows[i].columnconfigure(1, weight=1) - btn_next_test = tk.Button( - self.frm_table, - text = "NEXT TEST", - font = ('Arial', 15), + btn_next_test = ttk.Button( + self.viewingFrame, + text = "NEXT BOARD", + #font = ('Arial', 15), command = lambda: self.btn_next_test_action(parent) ) - btn_next_test.grid(column = 3, row = 5) - + btn_next_test.grid(column = 3, row = self.data_holder.getNumTest() + 5, sticky='se', pady=50, padx = 50) + + ################################################# # A function to be called within GUIWindow to create the console output # when the frame is being brought to the top - def create_JSON_popup(self, JSON_String): - - # Creating a popup window for the JSON Details - self.JSON_popup = tk.Toplevel() - self.JSON_popup.geometry("500x300+750+100") - self.JSON_popup.title("JSON Details") - # self.JSON_popup.wm_attributes('-toolwindow', 'True') - - - - # Creating a Frame For Console Output - frm_JSON = tk.Frame(self.JSON_popup, width = 500, height = 300, bg = 'green') - frm_JSON.pack_propagate(0) - frm_JSON.pack() + def create_JSON_popup(self, JSON_String, test): + try: + # Creating a popup window for the JSON Details + self.JSON_popup = tk.Toplevel() + self.JSON_popup.geometry("500x300+750+100") + self.JSON_popup.title("JSON Details") + # self.JSON_popup.wm_attributes('-toolwindow', 'True') + + self.JSON_popup.grab_set() + self.JSON_popup.attributes('-topmost', 'true') + self.JSON_popup.grid_rowconfigure(0, weight=1) + self.JSON_popup.grid_columnconfigure(0, weight=1) + self.JSON_popup.grid_propagate() + + # Creating a Frame For Console Output + frm_JSON = ttk.Frame(self.JSON_popup, width = 500, height = 300) + frm_JSON.pack_propagate(0) + frm_JSON.grid(row=0, column=0, sticky='nsew') + + # Placing an entry box in the frm_console + self.JSON_entry_box = tk.Text( + frm_JSON, + bg = '#6e5e5d', + fg = 'white', + font = ('Arial', 14) + ) + self.JSON_entry_box.pack(anchor = 'center', fill=tk.BOTH, expand=1) - # Placing an entry box in the frm_console - self.JSON_entry_box = tk.Text( - frm_JSON, - bg = '#6e5e5d', - fg = 'white', - font = ('Arial', 14) - ) - self.JSON_entry_box.pack(anchor = 'center', fill=tk.BOTH, expand=1) + logger.debug(JSON_String) + current_JSON_file = open(JSON_String) + current_JSON_data = json.load(current_JSON_file) - current_JSON_file = open(JSON_String) - current_JSON_data = json.load(current_JSON_file) + temp = "" + temp = json.dumps(current_JSON_data, indent=2) + #for key, value in current_JSON_data.items(): + # temp = temp + "{} : {}".format(key, value) + "\n" - temp = "" - for key, value in current_JSON_data.items(): - temp = temp + "{} : {}".format(key, value) + "\n" + self.JSON_entry_box.delete(1.0,"end") + self.JSON_entry_box.insert(1.0, temp) + + current_JSON_file.close() + except Exception as e: + logger.exception(e) + logger.warning("More Info popup has failed to be created.") - self.JSON_entry_box.delete(1.0,"end") - self.JSON_entry_box.insert(1.0, temp) + ################################################# # All of the different methods for what the retest buttons should do + def btn_retest_action(self, _parent, test_idx): + _parent.set_frame_test(test_idx) + def btn_retest1_action(self, _parent): _parent.set_frame(_parent.test1_frame) - + def btn_retest2_action(self, _parent): _parent.set_frame(_parent.test2_frame) - + def btn_retest3_action(self, _parent): _parent.set_frame(_parent.test3_frame) @@ -346,24 +415,24 @@ def btn_retest4_action(self, _parent): ################################################# - def btn_more_info1_action(self, _parent): - self.create_JSON_popup("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/DummyJSONTest.JSON") - - def btn_more_info2_action(self, _parent): - self.create_JSON_popup("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/DummyJSONTest.JSON") - - def btn_more_info3_action(self, _parent): - self.create_JSON_popup("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/DummyJSONTest.JSON") - - def btn_more_info4_action(self, _parent): - self.create_JSON_popup("/home/hgcal/WagonTest/WagonTestGUI/PythonFiles/JSONFiles/DummyJSONTest.JSON") + def btn_more_info_action(self, _parent, test_idx): + names = self.data_holder.getTestNames() + logger.info('Opening JSON file for %s...' % names[test_idx]) + self.create_JSON_popup("{}/JSONFiles/Current_{}_JSON.json".format(Path.home(), names[test_idx].replace(" ", "").replace("/", "")), names[test_idx]) ################################################# # Next test button action def btn_next_test_action(self, _parent): - self.data_holder.reset_data_holder() - _parent.set_frame(_parent.scan_frame) + self.data_holder.data_holder_new_test() + self.lbl_id.destroy() + _parent.reset_board() + + def get_submit_action(self): + return self.btn_next_test_action + + def get_parent(self): + return self.parent ################################################# diff --git a/PythonFiles/Scenes/ThermalTestBeginScene.py b/PythonFiles/Scenes/ThermalTestBeginScene.py new file mode 100644 index 00000000..a94ca20e --- /dev/null +++ b/PythonFiles/Scenes/ThermalTestBeginScene.py @@ -0,0 +1,217 @@ +################################################################################# + +# Importing Necessary Modules +import tkinter as tk +import tkinter.ttk as ttk +from tkinter import messagebox +import tkinter.font as font +import logging +logging.getLogger('PIL').setLevel(logging.WARNING) +# import PythonFiles +import os +import time +# Importing Necessary Files +from PythonFiles.utils.ThermalREQClient import ThermalREQClient + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ThermalTestBeginScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + +# Creating class for the window +class ThermalTestBeginScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder, queue, conn_trigger): + super().__init__(master_frame, width=1300-213, height = 800) + self.queue = queue + self.conn_trigger = conn_trigger + self.data_holder = data_holder + self.parent = parent + + self.update_frame(parent) + self.naming_scheme = [ + "SFP0", "SFP1", "SFP2", "SFP3", + "A1", "A2", "A3", "A4", + "B1", "B2", "B3", "B4", + "C1", "C2", "C3", "C4", + "D1", "D2", "D3", "D4" + ] + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + logger.debug("ParentTestClass: A ThermalTestBeginScene frame has been updated.") + # Creates a font to be more easily referenced later in the code + font_scene = ('Arial', 15) + self.create_style(parent) + # Create a centralized window for information + frm_window = ttk.Frame(self, width=870, height = 480) + frm_window.grid(column=0, row=0, sticky='nsew') + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + frm_window.columnconfigure(0, weight=1) + frm_window.rowconfigure(0, weight=1) + + # Create a label for the tester's name + lbl_title = ttk.Label( + frm_window, + text = "Begin Thermal Test", + font = ('Arial', '28') + ) + lbl_title.pack(side = 'top', pady = (10, 50)) + + + lbl_section1 = ttk.Label( + frm_window, + text = f"1. Seal the chamber door \n 2. Start the ENGIN CYCL profile on the thermal cycler", + font = ('Arial', '24'), + anchor="w", + justify="left" + ) + lbl_section1.pack(side = 'top', pady = (15, 175)) + + lbl_section2_title = ttk.Label( + frm_window, + text = "Then, verify the following:", + font = ('Arial', '24'), + anchor="w", + justify="center" + ) + lbl_section2_title.pack(side = 'top', pady = (15, 10)) + + lbl_section2 = ttk.Label( + frm_window, + text = '1."ENGIN CYCL Running" is displayed on the thermal chamber screen \n 2. The rotameter indicator is at the top of the gauge \n 3. Ensure the Master Switch is turned on',font = ('Arial', '24'), + anchor="w", + justify="left" + ) + lbl_section2.pack(side = 'top', pady = (15, 200)) + + + lbl_section3 = ttk.Label( + frm_window, + text = "If everything is ready, click below to start the full test", + font = ('Arial', '24') + ) + lbl_section3.pack(side = 'top', pady = 15) + + + # Create a logout button + btn_start_all = ttk.Button( + frm_window, + text = "Start Full Test", + #relief = tk.RAISED, + command = lambda: self.btn_start_full_test_action(parent)) + btn_start_all.pack(anchor = 'center', pady = 5) + + # Create frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 2, row = 2, padx = 10, pady = 10, sticky = 'se') + frm_logout.columnconfigure(0, weight=1) + + # Create a logout button + btn_logout = ttk.Button( + frm_logout, + text = "Logout", + #relief = tk.RAISED, + command = lambda: self.btn_logout_action(parent)) + btn_logout.pack(anchor = 'center', pady = 5) + + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'center', pady = 5) + + + self.grid_propagate(0) + + + ################################################# + + def help_action(self, _parent): + _parent.help_popup(self) + + + ################################################# + + # Confirm button action takes the user to the test in progress scene + def btn_start_full_test_action(self, _parent): + self.gui_cfg = self.data_holder.getGUIcfg() + checkbox_states = self.data_holder.data_dict.get("checkbox_states",[]) + ready_channels = [] + for i in range(len(checkbox_states)): + if checkbox_states[i] != 'excluded': + ready_channels.append(True) + else: + ready_channels.append(False) + + logger.info("Sending request to begin testing...") + sending_REQ = ThermalREQClient( + self.gui_cfg, + 'startCycle', + ready_channels, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + #except Exception as e: + # messagebox.showerror('Exception', e) + self.begin_update(self.parent.master_window, self.parent.queue, self.parent) + + _parent.set_frame_thermal_test_in_progress() + + def get_parent(self): + return self.parent + + ################################################# + + # functionality for the logout button + def btn_logout_action(self, _parent): + result = messagebox.askyesno("Confirm Logout", "Are you sure you want to logout?") + if result: + _parent.set_frame_login_frame() + + + ################################################# + + def begin_update(self, master_window, queue, parent): + + received_data = False + json_received = None + while not received_data: + if not queue.empty(): + signal=queue.get() + + if "Results received successfully." in signal: + # self.data_holder.update_from_json_string(message) + message='FOO' + message=self.conn_trigger.recv() + logger.info("ThermalTestInProgressScene: JSON Received.") + logger.info(message) + json_received=message + received_data = True + + time.sleep(0.01) + + if json_received: + logger.info("startCycle has been received.") + else: + logger.warning("No json received after allotted time.") + diff --git a/PythonFiles/Scenes/ThermalTestConfigScene.py b/PythonFiles/Scenes/ThermalTestConfigScene.py new file mode 100644 index 00000000..b63cb6ee --- /dev/null +++ b/PythonFiles/Scenes/ThermalTestConfigScene.py @@ -0,0 +1,367 @@ +################################################################################# + +# Importing Necessary Modules +import tkinter as tk +import tkinter.ttk as ttk +from tkinter import messagebox +import tkinter.font as font +import logging +logging.getLogger('PIL').setLevel(logging.WARNING) +# import PythonFiles +import os + +# Importing Necessary Server Files +from PythonFiles.utils.ThermalREQClient import ThermalREQClient + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ThermalTestConfigScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + +# Creating class for the window +class ThermalTestConfigScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder, queue, conn_trigger): + super().__init__(master_frame, width=1300-213, height = 800) + self.queue = queue + self.conn_trigger = conn_trigger + self.data_holder = data_holder + self.parent = parent + + # Create a list of boolean values for the checkboxes + # TODO Verify this is correctly set up + self.checkbox_values = [ + False, False, False, False, False, + False, False, False, False, False, + False, False, False, False, False, + False, False, False, False, False + ] + self.bool_checkbox_values = [ + False, False, False, False, False, + False, False, False, False, False, + False, False, False, False, False, + False, False, False, False, False + ] + self.naming_scheme = [ + "SFP0", "SFP1", "SFP2", "SFP3", + "A1", "A2", "A3", "A4", + "B1", "B2", "B3", "B4", + "C1", "C2", "C3", "C4", + "D1", "D2", "D3", "D4" + ] + + self.current_engine_selection = None + + self.update_frame(parent) + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + logger.debug("ParentTestClass: A ThermalTestConfigScene frame has been updated.") + # Creates a font to be more easily referenced later in the code + font_scene = ('Arial', 15) + + self.create_style(parent) + # Create a centralized window for information + frm_window = ttk.Frame(self, width=870, height = 480) + frm_window.grid(column=0, row=0, sticky='nsew') + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + frm_window.columnconfigure(0, weight=1) + frm_window.rowconfigure(0, weight=1) + + # Create a label for the tester's name + lbl_title = ttk.Label( + frm_window, + text = "Test Configuration", + font = ('Arial', '28') + ) + lbl_title.pack(side = 'top', pady = 10) + + + + # Create a frame to contain the label, dropdown, and confirm button in a single row + frm_engine_selection = ttk.Frame(frm_window) + frm_engine_selection.pack(anchor='center', pady=10) + + # Create a label for the engine type + lbl_full = ttk.Label( + frm_engine_selection, + text="Engine Type: ", + font=('Arial', '20') + ) + lbl_full.pack(side='left', padx=5) + + # Dynamically update dropdown menu + engine_types = ["LD", "HD_Half", "HD_Full"] + self.engine_type_selected = tk.StringVar(self) + self.engine_type_selected.set("") + + # Creating the dropdown menu itself + self.engine_dropdown = tk.OptionMenu( + frm_engine_selection, + self.engine_type_selected, + *engine_types + ) + self.engine_dropdown.pack(side='left', padx=5) + self.engine_dropdown.config(width=20) + + # Traces when the user selects an option in the dropdown menu + self.engine_type_selected.trace_add( + 'write', + lambda *args: self.dropdown_engine_selected() + ) + + + + # Create a label for confirming test + lbl_active = ttk.Label( + frm_window, + text = "Select active sites:", + font = ('Arial', '24') + ) + lbl_active.pack(side = 'top', pady = 15) + + + + + # Create a frame to hold the checkboxes + checkbox_frame = ttk.Frame(frm_window) + checkbox_frame.pack(pady=10) + + # Loop to create 20 checkboxes (5 columns and 4 rows) + for i in range(20): + if i < 4: + col = 0 + row = i + else: + col = ((i - 4) // 4) + 1 + row = (i - 4) % 4 + + # Create the checkbox and label for each + chk_var = tk.BooleanVar() + chk_var.set(self.checkbox_values[i]) + + checkbox = ttk.Checkbutton( + checkbox_frame, + text=f"{self.naming_scheme[i]}", # Display the number next to the checkbox (1-indexed) + variable=chk_var, + command=lambda idx=i: self.checkbox_selected(idx) # Pass index to function + ) + checkbox.grid(row=row, column=col, padx=10, pady=5, sticky="w") + + # Store the checkbox variable if you need to access the values later + self.checkbox_values[i] = chk_var + + + # Create a frame for the select/deselect buttons + frm_select = ttk.Frame(frm_window) + # Create "Select All" button + btn_select_all = ttk.Button( + frm_select, + text="Select All", + command=lambda: self.btn_select_all_action(parent) + ) + btn_select_all.pack(side='left', padx=10) + + # Create "Deselect All" button + btn_deselect_all = ttk.Button( + frm_select, + text="Deselect All", + command=lambda: self.btn_deselect_all_action(parent) + ) + btn_deselect_all.pack(side='left', padx=5) + + frm_select.pack(anchor='center', pady=10) + + + + # Create a label for bottom text + lbl_begin_text = ttk.Label( + frm_window, + text = "Once all engines are properly connected, click the button below to begin the setup check:", + font = ('Arial', '20') + ) + lbl_begin_text.pack(side = 'top', pady = 25) + + + # Create a logout button + btn_setup_check = ttk.Button( + frm_window, + text = "Run Setup Check", + #relief = tk.RAISED, + command = lambda: self.btn_setup_check_action(parent)) + btn_setup_check.pack(anchor = 'center', pady = 5) + + + + + + + # Create frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 2, row = 2, padx=10, pady=10, sticky = 'se') + frm_logout.columnconfigure(0, weight=1) + + # Create a logout button + btn_logout = ttk.Button( + frm_logout, + text = "Logout", + #relief = tk.RAISED, + command = lambda: self.btn_logout_action(parent)) + btn_logout.pack(anchor = 'center', pady = 5) + + + #if (self.test_idx == 0): + + # # Create a button for confirming test + # run_all_btn = ttk.Button( + # frm_logout, + # text = "Run All Tests", + # command = lambda:self.run_all_action(parent), + # ) + # run_all_btn.pack(anchor = 'center', pady = 5) + + + # # Create a rescan button + # btn_rescan = ttk.Button( + # frm_logout, + # text = "Change Boards", + # #relief = tk.RAISED, + # command = lambda: self.btn_rescan_action(parent)) + # btn_rescan.pack(anchor = 'center', pady = 5) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'center', pady = 5) + + + self.grid_propagate(0) + + + ################################################# + + + + def help_action(self, _parent): + _parent.help_popup(self) + + def checkbox_selected(self, idx): + self.gui_cfg = self.data_holder.getGUIcfg() + + self.bool_checkbox_values = [] + + for chk_var in self.checkbox_values: + value = chk_var.get() + # print(f"Value: {value} (Type: {type(value)})") # Debugging output + self.bool_checkbox_values.append(value) # Ensure proper boolean conversion + + # print("simple_checkbox_values:", simple_checkbox_values) + self.data_holder.data_dict["checkbox_selection"] = self.bool_checkbox_values + + + + def btn_setup_check_action(self, _parent): + + Checkboxes = all(not value for value in self.bool_checkbox_values) + + if self.current_engine_selection == None or Checkboxes == True: + response = messagebox.showwarning( + title="Missing selection!", + message="You need to select an Engine Type and at least one channel!" + ) + else: + + logger.info("Sending request to do setup check...") + sending_REQ = ThermalREQClient( + self.gui_cfg, + ('fullIDs', self.current_engine_selection), + self.bool_checkbox_values, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + + _parent.set_frame_thermal_setup_results() + # TODO Complete data logging from current scene + + + def dropdown_engine_selected(self): + self.current_engine_selection = self.engine_type_selected.get() + self.data_holder.data_dict["engine_type"] = self.engine_type_selected.get() + + def btn_select_all_action(self, _parent): + + self.select_all_checkbox() + self.checkbox_selected(0) + + pass + + def btn_deselect_all_action(self, _parent): + + self.deselect_all_checkbox() + self.checkbox_selected(0) + + pass + + # def btn_confirm_engine_action(self, _parent): + + + # pass + + + def run_all_action(self, _parent): + + _parent.run_all_tests() + + # Since BooleanVar is linked to the checkboxes, updating it will instantly reflect on the GUI. + def select_all_checkbox(self): + # Set all checkbox values to True + for i in range(len(self.checkbox_values)): + self.checkbox_values[i].set(True) # Update BooleanVar + + # Since BooleanVar is linked to the checkboxes, updating it will instantly reflect on the GUI. + def deselect_all_checkbox(self): + for i in range(len(self.checkbox_values)): + self.checkbox_values[i].set(False) # Update BooleanVar + + + + ################################################# + + def get_submit_action(self): + return self.btn_confirm_action + + def get_parent(self): + return self.parent + + ################################################# + + # functionality for the logout button + def btn_logout_action(self, _parent): + result = messagebox.askyesno("Confirm Logout", "Are you sure you want to logout?") + if result: + _parent.set_frame_login_frame() + + ################################################# + + + + diff --git a/PythonFiles/Scenes/ThermalTestFinalResultsScene.py b/PythonFiles/Scenes/ThermalTestFinalResultsScene.py new file mode 100644 index 00000000..99ce5a89 --- /dev/null +++ b/PythonFiles/Scenes/ThermalTestFinalResultsScene.py @@ -0,0 +1,453 @@ +################################################################################# + +# Importing Necessary Modules +import tkinter as tk +import tkinter.ttk as ttk +from tkinter import messagebox +import tkinter.font as font +import logging +logging.getLogger('PIL').setLevel(logging.WARNING) +# import PythonFiles +import os +import time +import json +from PythonFiles.utils.ConsoleRedirector import ConsoleRedirector +import sys +import requests + +from PythonFiles.utils.ThermalREQClient import ThermalREQClient +# Importing Necessary Files +# from PythonFiles.utils.REQClient import REQClient + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ThermalTestFinalResultsScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + +# Define state options +STATES = { + "pass": ("✔", "green"), + "fail": ("✖", "red"), + "retest": ("⚠", "orange"), + "excluded": ("__", "black"), + "waiting": ("...", "lightgray") +} + +# Creating class for the window +class ThermalTestFinalResultsScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder, queue, conn_trigger): + super().__init__(master_frame, width=1300-213, height = 800) + self.queue = queue + self.conn_trigger = conn_trigger + self.data_holder = data_holder + self.parent = parent + + self.naming_scheme = [ + "SFP0", "SFP1", "SFP2", "SFP3", + "A1", "A2", "A3", "A4", + "B1", "B2", "B3", "B4", + "C1", "C2", "C3", "C4", + "D1", "D2", "D3", "D4" + ] + self.checkbox_states = ['waiting']*20 + + self.update_frame(parent) + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + logger.debug("ParentTestClass: A ThermalTestFinalResultsScene frame has been updated.") + # Creates a font to be more easily referenced later in the code + font_scene = ('Arial', 15) + + self.create_style(parent) + # Create a centralized window for information + frm_window = ttk.Frame(self, width=1000, height = 480) + frm_window.grid(column=0, row=0, sticky='nsew') + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + frm_window.columnconfigure(0, weight=1) + frm_window.rowconfigure(0, weight=1) + + # Create a label for the tester's name + lbl_title = ttk.Label( + frm_window, + text = "Thermal Test Results", + font = ('Arial', '28') + ) + lbl_title.pack(side = 'top', pady = 10) + + container_frame = ttk.Frame(frm_window) + container_frame.pack(pady=10, padx=450, fill='x') + + + # Create a frame to hold the checkboxes + checkbox_frame = ttk.Frame(container_frame) + checkbox_frame.pack(side='left', padx=(0, 50)) + + # Initialize states + # TODO Update these to fill dynamically + + # TODO Find where to pull this information from + self.checkbox_labels = [] + self.checkbox_vars = [] + + key_frame = ttk.Frame(container_frame, padding=10, width=400) + key_frame.pack(side='left', fill='y') + key_frame.pack_propagate(False) + + # Key descriptions + key_descriptions = { + "pass": "Pass", + "fail": "Fail", + "retest": "Needs Retesting", + "excluded": "Excluded from Test", + "waiting": "Waiting" + } + + self.naming_scheme = [ + "SFP0", "SFP1", "SFP2", "SFP3", + "A1", "A2", "A3", "A4", + "B1", "B2", "B3", "B4", + "C1", "C2", "C3", "C4", + "D1", "D2", "D3", "D4" + ] + + # Loop to create 20 visual checkboxes + for i in range(20): + col = i // 4 + row = i % 4 + + # Get the initial state from checkbox_states + initial_state = self.checkbox_states[i] + initial_text = STATES[initial_state][0] + initial_color = STATES[initial_state][1] + + # Create a label as a clickable checkbox (icon) + state_label = ttk.Label( + checkbox_frame, + text=initial_text, + foreground=initial_color, + font=("Arial", 18), + padding=2 + ) + state_label.grid(row=row, column=col * 3, padx=1, pady=2, sticky="w") + + text_label = ttk.Label( + checkbox_frame, + text=f"{self.naming_scheme[i]}", + font=("Arial", 14) + ) + text_label.grid(row=row, column=col * 3 + 1, padx=1, pady=6, sticky="w") + + spacer = ttk.Label(checkbox_frame, text=" ", width=10) + spacer.grid(row=row, column=col * 3 + 2) + + # TODO Remove/update board status button binds on Final Results Scene + # Bind click event to toggle state + # state_label.bind("", lambda e, lbl=state_label, idx=i: self.toggle_state(lbl, idx)) + + # Store label reference + self.checkbox_labels.append(state_label) + + # Add labels to the key + for i, (state, description) in enumerate(key_descriptions.items()): + ttk.Label( + key_frame, + text=f"{STATES[state][0]} {description}", + foreground=STATES[state][1], + font=("Arial", 14), + wraplength=400 + ).grid(row=i, column=0, padx=5, pady=3, sticky="w") + + + + # # Create a label for bottom text + # lbl_begin_text = ttk.Label( + # frm_window, + # text = "Place passed engines in blue bin\nFailed engines in red bin\nEngines needing retests in gray bin", + # font = ('Arial', '14') + # ) + # lbl_begin_text.pack(side = 'top', pady = (75, 15)) + + + + # Create a frame to hold the bottom text labels + lbl_frame = ttk.Frame(frm_window) + lbl_frame.pack(side="top", pady=(75, 15)) + + # Create labels for each line with colored symbols + pass_row = ttk.Frame(lbl_frame) + pass_row.pack(side="top", pady=2, padx=5) + lbl_pass = ttk.Label( + pass_row, + text=f"{STATES['pass'][0]} ", + font=("Arial", 14), + foreground=STATES["pass"][1] + ) + lbl_pass.pack(side="left", anchor="w", padx=5) + + lbl_pass_text = ttk.Label( + pass_row, + text="Place passed engines in blue bin", + font=("Arial", 14), + foreground="white" + ) + lbl_pass_text.pack(side="left", anchor="w", padx=5) + + fail_row = ttk.Frame(lbl_frame) + fail_row.pack(side="top", pady=2, padx=5) + lbl_fail = ttk.Label( + fail_row, + text=f"{STATES['fail'][0]} ", + font=("Arial", 14), + foreground=STATES["fail"][1] + ) + lbl_fail.pack(side="left", anchor="w", padx=5) + + lbl_fail_text = ttk.Label( + fail_row, + text="Failed engines in red bin", + font=("Arial", 14), + foreground="white" + ) + lbl_fail_text.pack(side="left", anchor="w", padx=5) + + retest_row = ttk.Frame(lbl_frame) + retest_row.pack(side="top", pady=2, padx=5) + lbl_retest = ttk.Label( + retest_row, + text=f"{STATES['retest'][0]} ", + font=("Arial", 14), + foreground=STATES["retest"][1] + ) + lbl_retest.pack(side="left", anchor="w", padx=5) + + lbl_retest_text = ttk.Label( + retest_row, + text="Engines needing retests in gray bin", + font=("Arial", 14), + foreground="white" + ) + lbl_retest_text.pack(side="left", anchor="w", padx=5) + + + + # Create a logout button + btn_finish = ttk.Button( + frm_window, + text = "Finish", + #relief = tk.RAISED, + command = lambda: self.btn_finish_action(parent)) + btn_finish.pack(anchor = 'center', pady = (25, 10)) + + + + + + + + # Create frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 2, row = 2, padx = 10, pady = 10, sticky = 'se') + frm_logout.columnconfigure(0, weight=1) + + # Create a logout button + btn_logout = ttk.Button( + frm_logout, + text = "Logout", + #relief = tk.RAISED, + command = lambda: self.btn_logout_action(parent)) + btn_logout.pack(anchor = 'center', pady = 5) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'center', pady = 5) + + + self.grid_propagate(0) + + + ################################################# + + # Function to toggle states on click + def toggle_state(self, label, index): + current_state = self.checkbox_states[index] + state_keys = list(STATES.keys()) + new_state = state_keys[(state_keys.index(current_state) + 1) % len(STATES)] + + # Update state and label + self.checkbox_states[index] = new_state + label.config(text=STATES[new_state][0], foreground=STATES[new_state][1]) + + + + def help_action(self, _parent): + _parent.help_popup(self) + + + def btn_finish_action(self, _parent): + + self.gui_cfg = self.data_holder.getGUIcfg() + confirm = messagebox.askyesno( + title="Confirm Finish", + message="Stop the current cycle before exiting?" + ) + + checkbox_states = self.data_holder.data_dict.get("checkbox_states",[]) + ready_channels = [] + for i in range(len(checkbox_states)): + if checkbox_states[i] != 'excluded': + ready_channels.append(True) + else: + ready_channels.append(False) + + if confirm: + + logger.info("Sending request to finish testing...") + sending_REQ = ThermalREQClient( + self.gui_cfg, + 'killCycle', + ready_channels, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + + + + logger.info("Successfully Finished Thermal Testing.") + _parent.set_frame_login_frame() + + + ################################################# + + + def get_submit_action(self): + return self.btn_confirm_action + + def get_parent(self): + return self.parent + + ################################################# + + # functionality for the logout button + def btn_logout_action(self, _parent): + result = messagebox.askyesno("Confirm Logout", "Are you sure you want to logout?") + if result: + _parent.set_frame_login_frame() + + def btn_stop_early_action(self, _parent): + self.gui_cfg = self.data_holder.getGUIcfg() + confirm = messagebox.askyesno( + title="Confirm Stop", + message="Make sure a thermal test is actually running!" + ) + + checkbox_states = self.data_holder.data_dict.get("checkbox_states",[]) + ready_channels = [] + for i in range(len(checkbox_states)): + if checkbox_states[i] != 'excluded': + ready_channels.append(True) + else: + ready_channels.append(False) + + if confirm: + logger.info("User stopped thermal testing early!") + self.cancel_timer() + + sending_REQ = ThermalREQClient( + self.gui_cfg, + 'killCycle', + ready_channels, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + + ################################################# + + + def send_REQ(self, _parent): + + checkbox_states = self.data_holder.data_dict.get("checkbox_states",[]) + ready_channels = [] + for i in range(len(checkbox_states)): + if checkbox_states[i] != 'excluded': + ready_channels.append(True) + else: + ready_channels.append(False) + + sending_REQ = ThermalREQClient( + self.data_holder.getGUIcfg(), + 'analyzeCycle', + ready_channels, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + + self.begin_update(self.parent.master_window, self.parent.queue, self.parent) + + def begin_update(self, master_window, queue, parent): + + received_data = False + json_received = None + while not received_data: + if not queue.empty(): + signal=queue.get() + + if "Results received successfully." in signal: + message = "FOO" + message = self.conn_trigger.recv() + logger.info("ThermalTestFinalResultsScene: JSON Received.") + logger.info(message) + + if 'completed' not in message: + received_data = True + json_received = message + + time.sleep(0.01) + + if json_received: + self.format_json_received_to_json(json_received) + else: + logger.warning("ThermalTestSetupResultsScene: No json received after allotted time.") + return False + + + def format_json_received_to_json(self, imported_json_string): + json_string = imported_json_string.replace("'", '"') + json_string = json_string.replace('True', 'true') + json_string = json_string.replace('False', 'false') + json_dict = json.loads(json_string) + + self.apply_results(json_dict) + + self.update_frame(self.parent) + + def apply_results(self, json_dict): + for i, name in enumerate(self.naming_scheme): + if name in json_dict: + state = json_dict[name].get('passing_state') + self.checkbox_states[i] = state + else: + self.checkbox_states[i] = 'excluded' + diff --git a/PythonFiles/Scenes/ThermalTestInProgressScene.py b/PythonFiles/Scenes/ThermalTestInProgressScene.py new file mode 100644 index 00000000..02b83ab7 --- /dev/null +++ b/PythonFiles/Scenes/ThermalTestInProgressScene.py @@ -0,0 +1,304 @@ +################################################################################# + +# Importing Necessary Modules +import tkinter as tk +import tkinter.ttk as ttk +from tkinter import messagebox +import tkinter.font as font +import logging +from PythonFiles.utils.ConsoleRedirector import ConsoleRedirector +logging.getLogger('PIL').setLevel(logging.WARNING) +# import PythonFiles +import os +import sys +import time +# Importing Necessary Files +from PythonFiles.utils.ThermalREQClient import ThermalREQClient + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ThermalTestInProgressScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + + +# Creating class for the window +class ThermalTestInProgressScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder, queue, conn_trigger): + super().__init__(master_frame, width=1300-213, height = 800) + + self.console_text = None + self.original_stdout = sys.stdout # Store the default stdout + + self.queue = queue + self.conn_trigger = conn_trigger + self.data_holder = data_holder + self.parent = parent + + self.update_frame(parent) + + # Restore to default (in constructor) + # sys.stdout = self.original_stdout + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + def update_frame(self, parent): + logger.debug("ParentTestClass: A ThermalTestInProgressScene frame has been updated.") + # Creates a font to be more easily referenced later in the code + font_scene = ('Arial', 15) + + self.create_style(parent) + # Create a centralized window for information + frm_window = ttk.Frame(self, width=870, height = 480) + frm_window.grid(column=0, row=0, sticky='nsew') + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + frm_window.columnconfigure(0, weight=1) + frm_window.rowconfigure(0, weight=1) + + # Create a label for the tester's name + lbl_title = ttk.Label( + frm_window, + text = "Thermal Test in Progress", + font = ('Arial', '28') + ) + lbl_title.pack(side = 'top', pady = 10) + + + # # Create the rectangle canvas + # canvas = tk.Canvas(frm_window, width=700, height=200) + # canvas.pack() + # canvas.create_rectangle(0, 0, 700, 200, fill="lightgray", outline="black") + + # Create console display inside the window + self.create_console_window(frm_window) + logger.info("Successfully created console for output on GUI.") + + + # Create a label for bottom text + lbl_wait_text = ttk.Label( + frm_window, + text = "Please wait, tests in progress...", + font = ('Arial', '14') + ) + lbl_wait_text.pack(side = 'top', pady = 15) + + + #------------------------------ + + # Create the countdown timer frame + self.frm_timer = ttk.Frame(frm_window, padding=10) + self.frm_timer.pack(side='top', pady=20) + + # Create the label for approximate time remaining + lbl_approx_time = ttk.Label(self.frm_timer, text="Approximate time remaining:", font=("Arial", 15)) + lbl_approx_time.grid(row=0, column=0, padx=5, pady=5, sticky="w") + + + # Create a label to display the countdown timer + self.timer_label = ttk.Label(self.frm_timer, text="02:00:00", font=("Arial", 24), foreground="red") + self.timer_label.grid(row=1, column=0, columnspan=3, pady=10) + + # Clear and start the countdown from 4 hours (14400 seconds) + self.cancel_timer() + self.remaining_time = 14400 + + + + + + + # Create a logout button + btn_stop_early = ttk.Button( + frm_window, + text = "Stop Test Early", + #relief = tk.RAISED, + command = lambda: self.btn_stop_early_action(parent)) + btn_stop_early.pack(anchor = 'center', pady = 5) + + + # Create a button to go to test results + self.btn_next = ttk.Button( + frm_window, + text = "Thermal Test Results", + state="disabled", + #relief = tk.RAISED, + command = lambda: self.btn_next_action(parent)) + self.btn_next.pack(anchor = 'center', pady = 5) + + + # Create frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 2, row = 2, padx = 10, pady = 10, sticky = 'se') + frm_logout.columnconfigure(0, weight=1) + + + #if (self.test_idx == 0): + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'center', pady = 5) + + + self.grid_propagate(0) + + + + ################################################# + def create_console_window(self, frm_window): + # Create a frame to hold the console output and scrollbar + console_frame = tk.Frame(frm_window) + + # Create a Text widget for displaying console output + self.console_text = tk.Text(console_frame, width=85, height=30, wrap="word", state="disabled", bg="black", fg="white") + self.console_text.pack(side="left", fill="both", expand=True) + + # Create a Scrollbar and attach it to the Text widget + scrollbar = tk.Scrollbar(console_frame, command=self.console_text.yview) + scrollbar.pack(side="right", fill="y") + self.console_text.config(yscrollcommand=scrollbar.set) + + console_frame.pack() + + # Redirect sys.stdout to the Text widget + # sys.stdout = ConsoleRedirector(self.console_text) + + + # Timer functionality + def update_timer(self): + # Updates the countdown timer every second. + logger.debug(f"Countdown time remaining: {self.remaining_time}") + if self.remaining_time == 14390: exit() + if self.remaining_time > 0: + self.remaining_time -= 1 + hours, remainder = divmod(self.remaining_time, 3600) + minutes, seconds = divmod(remainder, 60) + self.timer_label.config(text=f"{hours:02}:{minutes:02}:{seconds:02}") + + # Schedule the next update + self._timer_id = self.after(1000, self.update_timer) + else: + self.timer_label.config(text="00:00:00", foreground="red") + self.btn_next.config(state="normal") #Results Button can only be pressed when timer is done + + def set_timer(self, hours, minutes): + # Manually sets the countdown timer based on function parameters. + # Convert input to total seconds + self.remaining_time = (hours * 3600) + (minutes * 60) + + # Update timer display immediately + self.update_timer() + + + def cancel_timer(self): + # Cancel any scheduled timer updates + if hasattr(self, "_timer_id"): + self.after_cancel(self._timer_id) + + # Reset the remaining time + self.remaining_time = 0 + + # Update the timer display immediately + self.timer_label.config(text="00:00:00", foreground="red") + + + + def help_action(self, _parent): + _parent.help_popup(self) + + + def btn_stop_early_action(self, _parent): + self.gui_cfg = self.data_holder.getGUIcfg() + confirm = messagebox.askyesno( + title="Confirm Early Stop", + message="Are you sure you want to stop the test now?\nThermal testing is still in progress!" + ) + + checkbox_states = self.data_holder.data_dict.get("checkbox_states",[]) + ready_channels = [] + for i in range(len(checkbox_states)): + if checkbox_states[i] != 'excluded': + ready_channels.append(True) + else: + ready_channels.append(False) + + if confirm: + logger.info("User stopped thermal testing early!") + self.cancel_timer() + + logger.info("Sending request to stop thermal testing early...") + sending_REQ = ThermalREQClient( + self.gui_cfg, + 'killCycle', + ready_channels, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + + _parent.set_frame_thermal_final_results() + + pass + + + # Send to the next scene (thermal_final_results) + def btn_next_action(self, _parent): + self.gui_cfg = self.data_holder.getGUIcfg() + + response = messagebox.askokcancel( + title="Confirm Test Finish", + message="Make sure the green light on top of the Cycler is on before proceeding to results." + ) + + checkbox_states = self.data_holder.data_dict.get("checkbox_states",[]) + ready_channels = [] + for i in range(len(checkbox_states)): + if checkbox_states[i] != 'excluded': + ready_channels.append(True) + else: + ready_channels.append(False) + + if response: + self.cancel_timer() + # sys.stdout = self.original_stdout + + #sending_REQ = ThermalREQClient( + # self.gui_cfg, + # 'analyzeCycle', + # ready_channels, + # self.data_holder.data_dict['current_full_ID'], + # self.data_holder.data_dict['user_ID'], + # self.conn_trigger + # ) + _parent.set_frame_thermal_final_results() + + + ################################################# + + def get_submit_action(self): + return self.btn_confirm_action + + def get_parent(self): + return self.parent + + + ################################################# + + diff --git a/PythonFiles/Scenes/ThermalTestSetupResultsScene.py b/PythonFiles/Scenes/ThermalTestSetupResultsScene.py new file mode 100644 index 00000000..5f549a5a --- /dev/null +++ b/PythonFiles/Scenes/ThermalTestSetupResultsScene.py @@ -0,0 +1,515 @@ +################################################################################# + +# Importing Necessary Modules +import tkinter as tk +import time +import tkinter.ttk as ttk +from tkinter import messagebox +import tkinter.font as font +import logging +logging.getLogger('PIL').setLevel(logging.WARNING) +# import PythonFiles +import os +import sys +import json +from PythonFiles.utils.ConsoleRedirector import ConsoleRedirector + +import requests + +# Importing Necessary Server Files +from PythonFiles.utils.ThermalREQClient import ThermalREQClient + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.Scenes.ThermalTestSetupResultsScene') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'a', format=FORMAT, level=logging.DEBUG) + +# Define state options +STATES = { + "ready": ("✔", "green"), + "failure": ("✖", "red"), + "warning": ("⚠", "orange"), + "excluded": ("__", "black"), + "waiting": ("...", "lightgray"), + "failed3": ("✖⚠", "maroon"), + "passed": ("✔⚠", "steelblue") +} + +# Creating class for the window +class ThermalTestSetupResultsScene(ttk.Frame): + + ################################################# + + def __init__(self, parent, master_frame, data_holder, queue, conn_trigger): + super().__init__(master_frame, width=1300-213, height = 800) + + self.naming_scheme = [ + "SFP0", "SFP1", "SFP2", "SFP3", + "A1", "A2", "A3", "A4", + "B1", "B2", "B3", "B4", + "C1", "C2", "C3", "C4", + "D1", "D2", "D3", "D4" + ] + + # Initialize the states + self.checkbox_states = ["waiting"]*20 + + self.console_text = None + self.original_stdout = sys.stdout # Store the default stdout + + self.queue = queue + self.conn_trigger = conn_trigger + self.data_holder = data_holder + self.parent = parent + self.is_initial_check = True + self.failures = [0]*20 + + self.update_frame(parent) + # sys.stdout = self.original_stdout + + + ################################################# + + def create_style(self, _parent): + + self.s = ttk.Style() + + self.s.tk.call('lappend', 'auto_path', '{}/awthemes-10.4.0'.format(_parent.main_path)) + self.s.tk.call('package', 'require', 'awdark') + + self.s.theme_use('awdark') + + + def update_frame(self, parent): + logger.debug("ParentTestClass: A ThermalTestSetupResultsScene frame has been updated.") + # Creates a font to be more easily referenced later in the code + font_scene = ('Arial', 15) + + self.create_style(parent) + # Create a centralized window for information + frm_window = ttk.Frame(self, width=1000, height = 480) + frm_window.grid(column=0, row=0, sticky='nsew') + self.columnconfigure(0, weight=1) + self.rowconfigure(0, weight=0) + frm_window.columnconfigure(0, weight=1) + frm_window.rowconfigure(0, weight=1) + + # Create a label for the tester's name + lbl_title = ttk.Label( + frm_window, + text = "Setup Check Results", + font = ('Arial', '28') + ) + lbl_title.pack(side = 'top', pady = 10) + + + # # Create a canvas for the rectangle + # canvas = tk.Canvas(frm_window, width=700, height=200) + # canvas.pack() + + # # Draw the rectangle + # canvas.create_rectangle(0, 0, 700, 200, fill="lightgray", outline="black") + container_frame = ttk.Frame(frm_window) + container_frame.pack(pady=10, padx=320, fill='x') + + # Create a frame to hold the checkboxes + checkbox_frame = ttk.Frame(container_frame) + checkbox_frame.pack(side='left', padx=(0, 50)) + + + self.adjustment_var = [ + False, False, False, False, False, + False, False, False, False, False, + False, False, False, False, False, + False, False, False, False, False + ] + + # Key descriptions + key_descriptions = { + "ready": "Ready", + "failure": "Connection Failure", + "warning": "Not Ready for Thermal Testing", + "excluded": "Excluded from Test", + "waiting": "Waiting", + "failed3": "Failed Thermal Testing 3 Times", + "passed": "Already Passed Thermal Testing" + } + + + key_frame = ttk.Frame(container_frame, padding=10, width=400) + key_frame.pack(side='left', fill='y') + key_frame.pack_propagate(False) + + self.checkbox_labels = [] + self.checkbox_vars = [] + + # Loop to create 20 visual checkboxes (5 columns, 4 rows) + for i in range(20): + col = i // 4 # Determine column (0-4) + row = i % 4 # Determine row (0-3) + + # Get the initial state from checkbox_states + initial_state = self.checkbox_states[i] + initial_text = STATES[initial_state][0] + initial_color = STATES[initial_state][1] + + # Create a label as a clickable checkbox (icon) + state_label = ttk.Label( + checkbox_frame, + text=initial_text, + foreground=initial_color, + font=("Arial", 18), + padding=2 + ) + state_label.grid(row=row, column=col * 3, padx=5, pady=2, sticky="w") + + text_label = ttk.Label( + checkbox_frame, + text=f"{self.naming_scheme[i]}:{self.failures[i]}", + font=("Arial", 18) + ) + text_label.grid(row=row, column=col * 3 + 1, padx=1, pady=6, sticky="w") + + spacer = ttk.Label(checkbox_frame, text=" ", width=12) + spacer.grid(row=row, column = col * 3 + 2) + # Bind click event to toggle state + state_label.bind("", lambda e, lbl=state_label, idx=i: self.toggle_state(lbl, idx)) + + # Store label reference + self.checkbox_labels.append(state_label) + + # Add labels to the key + + + for i, (state, description) in enumerate(key_descriptions.items()): + ttk.Label( + key_frame, + text=f"{STATES[state][0]} - {description}", + foreground=STATES[state][1], + font=("Arial", 14), + wraplength=400 + ).grid(row=i, column=0, padx=5, pady=3, sticky="w") + + + + # Create a label for bottom text + lbl_begin_text = ttk.Label( + frm_window, + text = "Make any adjustments using the channel selectors below. You may: Add new boards, or replace/remove/recheck existing boards", + font = ('Arial', '16') + ) + lbl_begin_text.pack(side = 'top', pady = (15,5)) + + + + # Create 20 checkboxes in a single row + adjustment_row_frame = ttk.Frame(frm_window) + + for i in range(20): + row = i % 4 + col = i // 4 + adj_var = tk.BooleanVar() + adj_var.set(self.adjustment_var[i]) + self.adjustment_var[i] = adj_var + + adj_checkbox = ttk.Checkbutton( + adjustment_row_frame, + text=f"{self.naming_scheme[i]}", + variable=adj_var, + command= lambda idx=i: self.adj_checkbox_action(idx) + ) + adj_checkbox.grid(row=row, column=col, padx=20, pady=5, sticky="w") + adjustment_row_frame.pack(pady=10) + + + + # Create a logout button + btn_recheck = ttk.Button( + frm_window, + text = "Recheck Selected Sites", + #relief = tk.RAISED, + command = lambda: self.btn_recheck_selected_action(parent)) + btn_recheck.pack(anchor = 'center', pady = 5) + + btn_remove = ttk.Button( + frm_window, + text="Remove Selected Sites", + command = lambda: self.btn_remove_selected_action(parent)) + btn_remove.pack(anchor = 'center', pady = 5) + + # Create a label for bottom text + lbl_proceed_text = ttk.Label( + frm_window, + text = "If everything is ready, proceed to the full test", + font = ('Arial', '14') + ) + lbl_proceed_text.pack(side = 'top', pady = 15) + + + # Create a logout button + btn_proceed = ttk.Button( + frm_window, + text = "Proceed", + #relief = tk.RAISED, + command = lambda: self.btn_proceed_action(parent)) + btn_proceed.pack(anchor = 'center', pady = 5) + + # Create frame for logout button + frm_logout = ttk.Frame(self) + frm_logout.grid(column = 2, row = 2, padx = 10, pady=10, sticky = 'se') + frm_logout.columnconfigure(0, weight=1) + + # Create a logout button + btn_logout = ttk.Button( + frm_logout, + text = "Logout", + #relief = tk.RAISED, + command = lambda: self.btn_logout_action(parent)) + btn_logout.pack(anchor = 'center', pady = 5) + + # Creating the help button + btn_help = ttk.Button( + frm_logout, + #relief = tk.RAISED, + text = "Help", + command = lambda: self.help_action(parent) + ) + btn_help.pack(anchor = 'center', pady = 5) + + + self.grid_propagate(0) + + + ################################################# + + # Function to toggle states on click + def toggle_state(self, label, index): + current_state = self.checkbox_states[index] + state_keys = list(STATES.keys()) + new_state = state_keys[(state_keys.index(current_state) + 1) % len(STATES)] + + # Update state and label + self.checkbox_states[index] = new_state + label.config(text=STATES[new_state][0], foreground=STATES[new_state][1]) + + self.get_setup_check_results() + + # For completely resetting the checkbox states from a list + def set_checkbox_states(self, checkbox_list): + for index, state in checkbox_list: + self.checkbox_states[index] = "{}".format(state) + + # Formatting the frontend label + label = self.checkbox_labels[index] + label.config(text=STATES[state][0], foreground=STATES[state][1]) + + def get_setup_check_results(self): + # TODO return to the dataholder for storage + return self.checkbox_states + + def adj_checkbox_action(self, idx): + self.get_adj_checkbox_action(idx) + + def get_adj_checkbox_action(self, idx): + simple_adj = [] + + for adj_val in self.adjustment_var: + val = adj_val.get() + simple_adj.append(val) + + # TODO Return to the dataholder + return simple_adj + + + + def help_action(self, _parent): + _parent.help_popup(self) + + + def btn_proceed_action(self, _parent): + # sys.stdout = self.original_stdout + self.is_initial_check = True + self.checkbox_states = ['waiting']*20 + + _parent.set_frame_thermal_begin() + pass + + + def btn_recheck_selected_action(self, _parent): + + self.is_initial_check = False + gui_cfg = self.data_holder.getGUIcfg() + + bool_checkbox_values = [] + for chk_var in self.adjustment_var: + value = chk_var.get() + bool_checkbox_values.append(value) # Ensure proper boolean conversion + + sending_REQ = ThermalREQClient( + gui_cfg, + 'fullIDs', + bool_checkbox_values, + self.data_holder.data_dict['current_full_ID'], + self.data_holder.data_dict['user_ID'], + self.conn_trigger + ) + + self.begin_update(self.parent.master_window, self.parent.queue, self.parent) + # TODO Complete + # _parent.btn_recheck_selected_action(self) + pass + + + def btn_remove_selected_action(self, _parent): #This fuction will remove any user selected channels from the checkboxes + + for i in range(20): + if self.adjustment_var[i].get(): + state = 'excluded' + logger.debug(f"Updating {self.naming_scheme[i]} to state '{state}'") + + self.checkbox_states[i] = state + self.checkbox_labels[i].config( + text=STATES[state][0], + foreground=STATES[state][1] + ) + #This data_dict will be called in the next scene to tell the server which channels to thermal test + self.data_holder.data_dict["checkbox_states"] = self.checkbox_states + + self.update_frame(self.parent) + + + ################################################# + + def get_submit_action(self): + return self.btn_confirm_action + + def get_parent(self): + return self.parent + + ################################################# + + # functionality for the logout button + def btn_logout_action(self, _parent): + result = messagebox.askyesno("Confirm Logout", "Are you sure you want to logout?") + if result: + self.is_initial_check = True + self.checkbox_states = ['waiting']*20 + _parent.set_frame_login_frame() + + ################################################# + + #This is the initial result display update being fed from the ThermalConfig scene + def apply_initial_check_results(self, state_list): + + for i in range(min(len(state_list), len(self.checkbox_states))): + if state_list[i][1] > 2: + state = 'failed3' + else: + state = state_list[i][0] + + self.checkbox_states[i] = state + self.checkbox_labels[i].config( + text=STATES[state][0], + foreground=STATES[state][1] + ) + + + #This data_dict will be called in the next scene to tell the server which channels to thermal test + self.data_holder.data_dict["checkbox_states"] = self.checkbox_states + + + for i in range(20): + if state_list[i][1] == -1: + self.failures[i] = ' ' + else: + self.failures[i] = state_list[i][1] + + + + #This will be called for each recheck in the ThermalSetupResults scene. It only updates the channels selected for rechecking + def apply_recheck_results(self, state_list): + selected_indices = [i for i, var in enumerate(self.adjustment_var) if var.get()] + + for i in range(min(len(state_list), len(self.checkbox_states))): + if self.adjustment_var[i].get(): + if state_list[i][1] > 2: + state = 'failed3' + else: + state = state_list[i][0] + logger.debug(f'Updating channel {self.naming_scheme[i]} to state: {state}') + self.checkbox_states[i] = state + self.checkbox_labels[i].config( + text=STATES[state][0], + foreground=STATES[state][1] + ) + #This data_dict will be called in the next scene to tell the server which channels to thermal test + self.data_holder.data_dict["checkbox_states"] = self.checkbox_states + + for i in range(20): + if state_list[i][1] == -1: + self.failures[i] = ' ' + else: + self.failures[i] = state_list[i][1] + + def begin_update(self, master_window, queue, parent): + #Create loading popup while waiting for json to be sent from server + self.loading_popup = tk.Toplevel(self) + self.loading_popup.title("Loading...") + self.loading_popup.geometry("500x300") + self.loading_popup.transient(self) + self.loading_popup.grab_set() + self.loading_popup.configure(bg='#2e2e2e') + + label = ttk.Label(self.loading_popup, text="Checking\nselected\nchannels...", font=('Arial', 40),foreground='white',background='#2e2e2e') + label.pack(pady=30) + + self.after(100, lambda: self.wait_for_server_response(master_window, queue, parent)) + + + def wait_for_server_response(self, master_window, queue, parent): + + received_data = False + json_received = None + while not received_data: + if not queue.empty(): + signal=queue.get() + + if "Results received successfully." in signal: + message = "FOO" + message = self.conn_trigger.recv() + logger.info("ThermalTestSetupResultsScene: JSON Received.") + logger.info(message) + if 'completed' not in message: + json_received = message + received_data = True + + else: + self.after(50, lambda: self.wait_for_server_response(master_window, queue, parent)) + return + + if hasattr(self, 'loading_popup') and self.loading_popup.winfo_exists(): + self.loading_popup.destroy() + + + if json_received: + self.format_json_received_to_json(json_received) + else: + logger.warning("No json received after allotted time.") + return False + + def format_json_received_to_json(self, imported_json_string): + json_string = imported_json_string.replace("'", '"') + json_string = json_string.replace('True', 'true') + json_string = json_string.replace('False', 'false') + json_dict = json.loads(json_string) + + + if self.is_initial_check == True: + self.apply_initial_check_results(json_dict) + else: + self.apply_recheck_results(json_dict) + + self.update_frame(self.parent) + +######################################################### diff --git a/PythonFiles/Scenes/WaitingScene.py b/PythonFiles/Scenes/WaitingScene.py new file mode 100644 index 00000000..41f076b4 --- /dev/null +++ b/PythonFiles/Scenes/WaitingScene.py @@ -0,0 +1,17 @@ +import tkinter as tk +from tkinter import ttk + +class WaitingScene(ttk.Frame): + def __init__(self, parent, master_frame): + super().__init__(master_frame, width=1300-213, height=800) + self.parent = parent + self.grid_propagate(0) + + self.label = ttk.Label( + self, + text="Please wait...\nChecking selected Channels.", + font=('Arial', 28), + anchor='center', + justify='center' + ) + self.label.place(relx=0.5, rely=0.5, anchor="center") diff --git a/PythonFiles/Scenes/__init__.py b/PythonFiles/Scenes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/TestFailedPopup.py b/PythonFiles/TestFailedPopup.py index d9191437..3aef572a 100644 --- a/PythonFiles/TestFailedPopup.py +++ b/PythonFiles/TestFailedPopup.py @@ -1,22 +1,30 @@ ################################################################################# import tkinter as tk +import logging +import PythonFiles +import os ################################################################################# +logger = logging.getLogger('HGCALTestGUI.PythonFiles.TestFailedPopup') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig(filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), filemode = 'w', format=FORMAT, level=logging.DEBUG) + class TestFailedPopup(): ################################################# - def __init__(self, parent, previous_frame): - self.test_failed_popup(parent, previous_frame) + def __init__(self, parent, previous_frame, data_holder): + self.test_failed_popup(parent, previous_frame, data_holder) ################################################# # Function to make retry or continue window if the test fails - def test_failed_popup(self, parent, previous_frame): - + def test_failed_popup(self, parent, previous_frame, data_holder): + self.data_holder = data_holder + logger.info("TestFailedPopup: A test has been failed. Prompting user for 'retry' or 'continue'.") # Creates a popup to ask whether or not to retry the test self.popup = tk.Toplevel() self.popup.title("Test Failed") @@ -63,8 +71,6 @@ def test_failed_popup(self, parent, previous_frame): # Called when the no button is pressed to destroy popup and return you to the main window def retry_function(self, parent, previous_frame): self.popup.destroy() - - #TODO This needs to be overhauled parent.set_frame(previous_frame) ################################################# @@ -76,4 +82,4 @@ def continue_function(self): ################################################# -################################################################################# \ No newline at end of file +################################################################################# diff --git a/PythonFiles/__init__.py b/PythonFiles/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/update_config.py b/PythonFiles/update_config.py new file mode 100644 index 00000000..ed479a3e --- /dev/null +++ b/PythonFiles/update_config.py @@ -0,0 +1,44 @@ +from PythonFiles.GUIConfig import GUIConfig +import yaml +from pathlib import Path +import os + + +ENV_NAME = "GUI_CONFIG_PATH" +DEFAULT_PATH = "/etc/HGCALTestGUI" + +LD_4MOD_NAMES = ["WE40A1", "WE40A2", "WE31A1", "WE31A3"] + + +def loadConfig(cfg_type): + p = Path(os.environ.get(ENV_NAME, DEFAULT_PATH)) + if not p.exists(): + p = Path(__file__).parent.parent / "Configs" + with open(p / f"{cfg_type}_cfg.yaml", "r") as f: + ret = import_yaml(f) + return ret + + +def update_config(full_id): + if full_id[3:9] in LD_4MOD_NAMES: + cfg_type = "LD_4Mod_Wagon" + elif full_id[3:5] == "WW" or full_id[3:5] == "WE": + cfg_type = "LD_Wagon" + elif full_id[3:5] == "WH": + cfg_type = "HD_Wagon" + elif full_id[3:9] in ("ZPLMEZ", "ZPLMZ2"): + cfg_type = "Mezz" + elif full_id[3:5] == "ZP": + cfg_type = "Zipper" + elif full_id[3:5] == "EL": + cfg_type = "LD_Engine" + elif full_id[3:5] == "EH": + cfg_type = "HD_Engine" + else: + cfg_type = "LD_Wagon" + board_cfg = loadConfig(cfg_type) + return GUIConfig(board_cfg) + + +def import_yaml(filename): + return yaml.safe_load(filename) diff --git a/PythonFiles/utils/BitRateTest.py b/PythonFiles/utils/BitRateTest.py deleted file mode 100644 index 74b6c10b..00000000 --- a/PythonFiles/utils/BitRateTest.py +++ /dev/null @@ -1,30 +0,0 @@ -# Importing necessary modules -import time, json - - -class BitRateTest(): - - def __init__(self, conn): - time.sleep(1) - for i in range(7): - conn.send("Run:"+ str(i)) - time.sleep(0.25) - - - - - -################################################################################# - - # Include this in every test file - # Or in some sense - - conn.send("Done.") - time.sleep(0.25) - - # Test code to ensure json/text sending is working correctly - current_JSON_file = open("./PythonFiles/JSONFiles/testingJSON.JSON") - current_JSON_data = json.load(current_JSON_file) - json_string = json.dumps(current_JSON_data) - print(json_string) - conn.send(json_string) \ No newline at end of file diff --git a/PythonFiles/utils/ConsoleRedirector.py b/PythonFiles/utils/ConsoleRedirector.py new file mode 100644 index 00000000..430cd7d9 --- /dev/null +++ b/PythonFiles/utils/ConsoleRedirector.py @@ -0,0 +1,15 @@ +import tkinter as tk +import sys + +class ConsoleRedirector: + def __init__(self, text_widget): + self.text_widget = text_widget + + def write(self, message): + self.text_widget.config(state="normal") # Allow editing + self.text_widget.insert("end", message) # Insert new text + self.text_widget.see("end") # Auto-scroll to the bottom + self.text_widget.config(state="disabled") # Prevent user edits + + def flush(self): + pass # Required for compatibility with sys.stdout \ No newline at end of file diff --git a/PythonFiles/utils/GenResTest.py b/PythonFiles/utils/GenResTest.py deleted file mode 100644 index f3e1561b..00000000 --- a/PythonFiles/utils/GenResTest.py +++ /dev/null @@ -1,30 +0,0 @@ -# Importing necessary modules -import time, json - - -class GenResTest(): - - def __init__(self, conn): - time.sleep(2) - for i in range(17): - conn.send("Run:"+ str(i)) - time.sleep(0.25) - - - - - -################################################################################# - - # Include this in every test file - # Or in some sense - - conn.send("Done.") - time.sleep(0.25) - - # Test code to ensure json/text sending is working correctly - current_JSON_file = open("./PythonFiles/JSONFiles/testingJSON.JSON") - current_JSON_data = json.load(current_JSON_file) - json_string = json.dumps(current_JSON_data) - print(json_string) - conn.send(json_string) \ No newline at end of file diff --git a/PythonFiles/utils/I2CConnTest.py b/PythonFiles/utils/I2CConnTest.py deleted file mode 100644 index d8386343..00000000 --- a/PythonFiles/utils/I2CConnTest.py +++ /dev/null @@ -1,30 +0,0 @@ -# Importing necessary modules -import time, json - - -class I2CConnTest(): - - def __init__(self, conn): - time.sleep(1) - for i in range(13): - conn.send("Run:"+ str(i)) - time.sleep(0.25) - - - - - -################################################################################# - - # Include this in every test file - # Or in some sense - - conn.send("Done.") - time.sleep(0.25) - - # Test code to ensure json/text sending is working correctly - current_JSON_file = open("./PythonFiles/JSONFiles/testingJSON.JSON") - current_JSON_data = json.load(current_JSON_file) - json_string = json.dumps(current_JSON_data) - print(json_string) - conn.send(json_string) \ No newline at end of file diff --git a/PythonFiles/utils/IDResTest.py b/PythonFiles/utils/IDResTest.py deleted file mode 100644 index 5f39ed4e..00000000 --- a/PythonFiles/utils/IDResTest.py +++ /dev/null @@ -1,30 +0,0 @@ -# Importing necessary modules -import time, json - - -class IDResTest(): - - def __init__(self, conn): - time.sleep(1) - for i in range(10): - conn.send("Run:"+ str(i)) - time.sleep(0.25) - - - - - -################################################################################# - - # Include this in every test file - # Or in some sense - - conn.send("Done.") - time.sleep(0.25) - - # Test code to ensure json/text sending is working correctly - current_JSON_file = open("./PythonFiles/JSONFiles/testingJSON.JSON") - current_JSON_data = json.load(current_JSON_file) - json_string = json.dumps(current_JSON_data) - print(json_string) - conn.send(json_string) \ No newline at end of file diff --git a/PythonFiles/utils/Images/Bethel_Logo.png b/PythonFiles/utils/Images/Bethel_Logo.png new file mode 100644 index 00000000..4d6bac22 Binary files /dev/null and b/PythonFiles/utils/Images/Bethel_Logo.png differ diff --git a/PythonFiles/utils/Images/GreenCheckMark.png b/PythonFiles/utils/Images/GreenCheckMark.png new file mode 100644 index 00000000..ba7aaeb9 Binary files /dev/null and b/PythonFiles/utils/Images/GreenCheckMark.png differ diff --git a/PythonFiles/utils/Images/QRimage.png b/PythonFiles/utils/Images/QRimage.png new file mode 100644 index 00000000..bf953464 Binary files /dev/null and b/PythonFiles/utils/Images/QRimage.png differ diff --git a/PythonFiles/utils/Images/RedX.png b/PythonFiles/utils/Images/RedX.png new file mode 100644 index 00000000..3e7a2410 Binary files /dev/null and b/PythonFiles/utils/Images/RedX.png differ diff --git a/PythonFiles/utils/Images/UMN_Logo.png b/PythonFiles/utils/Images/UMN_Logo.png new file mode 100644 index 00000000..028e5b32 Binary files /dev/null and b/PythonFiles/utils/Images/UMN_Logo.png differ diff --git a/PythonFiles/utils/LocalHandler.py b/PythonFiles/utils/LocalHandler.py new file mode 100644 index 00000000..e4f49240 --- /dev/null +++ b/PythonFiles/utils/LocalHandler.py @@ -0,0 +1,92 @@ +import multiprocessing as mp +import zmq, sys, os, signal, logging, json + +sys.path.append("{}".format(os.getcwd())) +sys.path.append("{}/Tests".format(os.getcwd())) + +# Class needed for bridging the gap between request, running test +# and sending results back + +# This takes the place of the REPserver and PUBServer which are +# used when tests are run via ZMQ + +# Note that this process needs to start on instantiation of the GUI +# to avoid any overlapping event monitors. So, conn_trigger +# is used to trigger a new test via REQClient + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.utils.LocalHandler') + +class LocalHandler: + + def __init__(self, gui_cfg, conn_trigger, q): + + conn_test, conn_pub = mp.Pipe() + + # Listen for test request + while True: + logger.info("LocalHandler: New PUB proc") + request = json.loads(conn_trigger.recv()) + process_PUB = mp.Process(target = self.task_local, args=(conn_pub,q)) + process_PUB.start() + + if request is not None: + + desired_test = request["desired_test"] + test_info = {"full_id": request["full_id"], "tester": request["tester"]} + + logger.info("LocalHandler: New test proc") + self.process_test = mp.Process(target = self.task_test, args=(conn_test, gui_cfg, desired_test, test_info)) + self.process_test.start() + + # Hold until test finish + logger.info("LocalHandler: Joining test proc") + self.process_test.join() + + logger.info("LocalHandler: Terminate PUB proc") + process_PUB.terminate() + + + try: + conn_pub.close() + conn_test.close() + + except Exception as e: + logger.error("LocalHandler: PUB and test pipe could not be closed: {}".format(e)) + + try: + process_PUB.terminate() + except Exception as e: + logger.error("LocalHandler: PUB and test process could not be terminated: {}".format(e)) + + def task_local(self, conn, q): + # listens for incoming data and attaches the correct topic before sending it on to SUBClient + #try: + while 1 > 0: + prints = conn.recv() + if prints == "Done.": + prints = 'print ; ' + str(prints) + q.put(prints) + + json = queue.get() + json = 'JSON ; ' + str(json) + q.put(str(json)) + else: + prints = 'print ; ' + str(prints) + q.put(prints) + + logger.info("LocalHandler: Loop has been broken.") + #except: + # logging.critical("Local server has crashed.") + + + + def task_test(self, conn_test, gui_cfg, desired_test, test_info): + + # Dynamically import test class + test_meta = gui_cfg["Test"][desired_test] + # Need to strip .py from test script for import + mod = __import__(test_meta["TestScript"][:-3], fromlist=[test_meta["TestClass"]]) + test_class = getattr(mod, test_meta["TestClass"]) + + test_class(conn_test, board_sn=test_info["full_id"], tester=test_info["tester"]) + diff --git a/PythonFiles/utils/PUBServer.py b/PythonFiles/utils/PUBServer.py deleted file mode 100644 index 7846a834..00000000 --- a/PythonFiles/utils/PUBServer.py +++ /dev/null @@ -1,39 +0,0 @@ -# Importing necessary modules -import zmq, signal - -# Creates the class for the PUBServer -class PUBServer(): - - def __init__(self, conn): - # Making the conn variable accessible in later code - self.conn = conn - print("Publish Server starting up...") - # Used to allow CTRL+C keyboard interrupt - signal.signal(signal.SIGINT, signal.SIG_DFL) - # Creates zmq.Context object - cxt = zmq.Context() - # Sets socket type to PUBLISH - pub_socket = cxt.socket(zmq.PUB) - # Server side .bind - pub_socket.bind("tcp://*:5556") - - # Creates a while loop that is searching for the "print" messages on the pipe - # Sends them immediately and once it receieves "Done." It prepares to receive and send - # the json files of results. - while 1 > 0: - prints = conn.recv() - if prints == "Done.": - prints = "print ; " + prints - pub_socket.send_string(prints) - json = conn.recv() - json = "JSON ; " + json - pub_socket.send_string(json) - # Breaks the loop once it sends the JSON so the server will shut down - break - else: - prints = "print ; " + prints - pub_socket.send_string(prints) - - # Closes the server once the loop is broken so that there is no hang-up in the code - print("PUBServer Closing") - pub_socket.close() diff --git a/PythonFiles/utils/REPServer.py b/PythonFiles/utils/REPServer.py deleted file mode 100644 index 2497ed16..00000000 --- a/PythonFiles/utils/REPServer.py +++ /dev/null @@ -1,127 +0,0 @@ -##################################################################### -# # -# This code creates a server to be housed locally on the testing # -# station (at the moment this would be the raspberry pi) # -# # -##################################################################### - -# importing necessary modules -from asyncore import write -import time, zmq, json -import multiprocessing as mp -from tkinter import NONE -# Should contain imports for the test scripts -from GenResTest import GenResTest -from PUBServer import PUBServer -from IDResTest import IDResTest -from I2CConnTest import I2CConnTest -from BitRateTest import BitRateTest - -# Makes the REPServer a class -class REPServer(): - - def __init__(self): - self.output = None - - # Creates a context class which contains the method to create a socket. - cxt = zmq.Context() - socket = cxt.socket(zmq.REP) - - # Server-side for talking to a network point. "Bind" ## socket.connect() is only used for CLIENTS - socket.bind("tcp://*:5555") - - print ("Reply Server has started.") - time.sleep(1) - - - try: - # Sets up an infinite loop so the server is always on until a keyboard interrupt occurs - while 1>0: - # Wait for next request from client - # string = socket.recv_string().lower() - print("Waiting for request...") - message = socket.recv_string().lower() - print("Received request: %s " % message) - - - # Testing to see what the request sent to the server was. The only requests we - # care about are test1, test2, test3, & test4. Anything else will send back - # "invalid request" to the client. - if message == "test1": - # Immediately sends a response to the GUI, begins the test and PUBServer, then resets the message variable. - socket.send(b"Request receieved for Test 1. Starting test.") - self.begin_processes(message) - message = '' - - elif message == "test2": - # Immediately sends a response to the GUI, begins the test and PUBServer, then resets the message variable. - socket.send(b"Request receieved for Test 2. Starting test.") - self.begin_processes(message) - message = '' - - elif message == "test3": - # Immediately sends a response to the GUI, begins the test and PUBServer, then resets the message variable. - socket.send(b"Request receieved for Test 3. Starting test.") - self.begin_processes(message) - message = '' - - elif message == "test4": - # Immediately sends a response to the GUI, begins the test and PUBServer, then resets the message variable. - socket.send(b"Request receieved for Test 4. Starting test.") - self.begin_processes(message) - message = '' - - else: - # Contingency response for debugging - socket.send(b"Invalid request. Request must be a test.") - - # Keyboard interrupt with ZMQ has a bug when on BOTH Windows AND Python at the same time. - # This code should allow for CTRL + C interrupt for the server on any non-windows system. - except KeyboardInterrupt: - print("Closing the server...") - socket.close() - cxt.term() - - # The target function for processs_test being created in begin_process - def task_test(self, conn, desired_test): - # Tests for what test is being requested and then starts the corresponding test. - # If it is not one of the tests, this passes (maybe change that.) - # Every test tasks "conn" which is for piping "print" statements - # from the tests to the publish server - if desired_test == 'test1': - test1 = GenResTest(conn) - elif desired_test == 'test2': - test2 = IDResTest(conn) - elif desired_test == 'test3': - test3 = I2CConnTest(conn) - elif desired_test == 'test4': - test4 = BitRateTest(conn) - else: - pass - - # The target function for process_PUBServer being created in begin_process - def task_PUBServer(self, conn): - pub_server = PUBServer(conn) - - # Starts up the test and PUBServer as separate processes - def begin_processes(self, desired_test): - print("Starting processes") - conn_test, conn_PUBServer = mp.Pipe() - process_test = mp.Process(target = self.task_test, args=(conn_test, desired_test,)) - process_PUBServer = mp.Process(target = self.task_PUBServer, args=(conn_PUBServer,)) - - process_test.start() - process_PUBServer.start() - - # Prevents the code from continuing here until both processes have ended. - process_test.join() - process_PUBServer.join() - - print("Processes have ended.") - -# Having an odd bug where it trys to instantiate the server twice, this prevents anything weird from happening -try: - # Instantiates the server - REP_Server = REPServer() -except: - print("REPServer already instantiated") diff --git a/PythonFiles/utils/REQClient.py b/PythonFiles/utils/REQClient.py index b3544d9a..6c41ab8e 100644 --- a/PythonFiles/utils/REQClient.py +++ b/PythonFiles/utils/REQClient.py @@ -1,48 +1,167 @@ ##################################################################### # # # This is the code for a client to send a request to a server to # -# run specific test scripts. # +# run specific test scripts. You can additionally specify running # +# tests locally or via ssh. # # # ##################################################################### ################################################################################# # Importing necessary modules -import zmq +import zmq, logging +import PythonFiles +import multiprocessing as mp +import os +import json +import time + +from PythonFiles.utils.LocalHandler import LocalHandler ################################################################################# +logger = logging.getLogger('HGCALTestGUI.PythonFiles.utils.REQClient') + # Making the Client Server a class class REQClient(): - ################################################# + ################################################ # Ensures nothing happens on instantiantion - def __init__(self, desired_test, serial, tester): + def __init__(self, gui_cfg, desired_test, full_id, tester, conn_trigger): + with open("{}/utils/server_ip.txt".format(PythonFiles.__path__[0]),"r") as openfile: + grabbed_ip = openfile.read()[:-1] self.message = "" - self.serial = serial - self.tester = tester + + test_handler_name = gui_cfg.getTestHandler()["name"] + + # Run the ZMQ server on test stand and make requests via ZMQ client + if test_handler_name == "ZMQ": + + self.ZMQClient(gui_cfg, desired_test, full_id, tester) + + # Run tests only on the current computer + elif test_handler_name == "Local": + + self.LocalClient(conn_trigger, desired_test, full_id, tester) + + # Run tests on another machine via SHH (key required) + elif test_handler_name == "SSH": + + self.SSHClient(conn_trigger, desired_test, full_id, tester) + + + # Handling tests run on the local machine + def LocalClient(self, conn_trigger, desired_test, full_id, tester): + + desired_test = int(desired_test[4:]) + + trigger_dict = {"desired_test": desired_test, "full_id": full_id, "tester": tester} + trigger_message = json.dumps(trigger_dict) + + conn_trigger.send(trigger_message) + + # Handling tests run via SSH + def SSHClient(self, conn_trigger, desired_test, full_id, tester): + + desired_test = int(desired_test[4:]) + + trigger_dict = {"desired_test": desired_test, "full_id": full_id, "tester": tester} + trigger_message = json.dumps(trigger_dict) + + conn_trigger.send(trigger_message) + + def ZMQClient(self, gui_cfg, desired_test, serial, tester): sending_msg = desired_test + ";" + serial + ";" + tester # Establishing variable for use self.desired_test = desired_test # Necessary for zmqClient context = zmq.Context() - # Creates a socket to talk to the server - # print("Connecting to the testing server...") - socket = context.socket(zmq.REQ) - socket.connect("tcp://192.168.23.23:5555") + try: + remote_ip = gui_cfg.getTestHandler()["remoteip"] - print("Sending request to REPServer for: ", self.desired_test) + # Creates a socket to talk to the server + # print("Connecting to the testing server...") + socket = context.socket(zmq.REQ) + socket.connect("tcp://{ip_address}:5555".format(ip_address = remote_ip)) + except: + logger.error("No remote_ip specified, please modify config") + + debug_msg = "Sending request to REPServer for: " + self.desired_test + logger.info(debug_msg) + # Tell the server what test to run socket.send_string(sending_msg) - print("Request sent. Waiting for confirmation receipt...") + + # Timeout feature for the socket + # The poller is responsible for stopping the socket send after a certain time + # Poller is in milliseconds + #poller = zmq.Poller() + #poller.register(socket, zmq.POLLIN) + #if not poller.poll(6*1000): + # raise IOError("Timeout processing the REQClient request from socket") + + + logger.info("Request sent. Waiting for confirmation receipt...") # Get the reply - self.message = socket.recv() - received = self.message.decode('UTF-8') + + # Recording the number of tries to open the socket and receive a string + tries = 0 + + + REQUEST_TIMEOUT = 2500 + REQUEST_RETRIES = 3 + # socket.connect("tcp://{ip_address}:5555".format(ip_address = grabbed_ip)) + + retries_left = REQUEST_RETRIES + while (len(self.message)< 1) and tries < 1000: + + try: + #print("\nTrying to receive a message from the socket") + #logger.debug("REQClient: Trying to receive a message from the socket receive") + #self.message = socket.recv_string() + #print("\n\n\nSelf.message: {}\n\n\n".format(self.message)) + + if(socket.poll(REQUEST_TIMEOUT) & zmq.POLLIN) != 0: + self.message = socket.recv_string() + retries_left = REQUEST_RETRIES + logger.info("Request received.") + break + + retries_left -= 1 + logger.warning("No response from server") + socket.setsockopt(zmq.LINGER, 0) + socket.close() + + # Out of retries + if retries_left == 0: + logger.error("Server seems to be offline, abandoning...") + break + + logger.info("Attempts remaining... Reconnecting to the server...") + + socket = context.socket(zmq.REQ) + + socket.connect("tcp://{ip_address}:5555".format(ip_address = grabbed_ip)) + + logger.info("Resending...") + + socket.send_string(sending_msg) + + + except: + logger.info("couldn't get info - {}".format(tries)) + tries = tries + 1 + #messagebox.showerror("No Message Received", "REQClient: No message received from the request.") + # Closes the client so the code in the GUI can continue once the request is sent. - socket.close() + try: + logger.info("Trying to close the socket") + socket.close() + except: + logger.info("Unable to close the socket") ################################################# diff --git a/PythonFiles/utils/Redirector.py b/PythonFiles/utils/Redirector.py new file mode 100644 index 00000000..eb0f86a2 --- /dev/null +++ b/PythonFiles/utils/Redirector.py @@ -0,0 +1,41 @@ +import http.server +import socketserver +import sys +import argparse + + + +def redirect_handler_factory(url): + """ + Returns a request handler class that redirects to supplied `url` + """ + class RedirectHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(301) + self.send_header('Location', url) + self.end_headers() + + return RedirectHandler + + +def main(): + + parser = argparse.ArgumentParser(description='HTTP redirect server') + + parser.add_argument('--port', '-p', action="store", type=int, default=80, help='port to listen on') + parser.add_argument('--ip', '-i', action="store", default="", help='host interface to listen on') + parser.add_argument('redirect_url', action="store") + + myargs = parser.parse_args() + + redirect_url = myargs.redirect_url + port = myargs.port + host = myargs.ip + + redirectHandler = redirect_handler_factory(redirect_url) + + handler = socketserver.TCPServer((host, port), redirectHandler) + handler.serve_forever() + +if __name__ == "__main__": + main() diff --git a/PythonFiles/utils/SSHHandler.py b/PythonFiles/utils/SSHHandler.py new file mode 100644 index 00000000..927061ad --- /dev/null +++ b/PythonFiles/utils/SSHHandler.py @@ -0,0 +1,113 @@ +import multiprocessing as mp +import subprocess as sp +import zmq, sys, os, signal, logging, json + +sys.path.append("{}".format(os.getcwd())) +sys.path.append("{}/Tests".format(os.getcwd())) + +# Class needed for briding the gap between request, running test +# and sending results back + +# This takes the place of the REPserver and PUBServer which are +# used when tests are run via ZMQ + +# Note that this process needs to start on instantiation of the GUI +# to avoid any overlapping event monitors. So, conn_trigger +# is used to trigger a new test via REQClient + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.utils.SUBClient') + +class SSHHandler: + + def __init__(self, gui_cfg, gui_cfg_host, conn_trigger, q): + + queue = mp.Queue() + + # Listen for test request + while True: + logger.info("SSHHandler: New PUB proc") + request = json.loads(conn_trigger.recv()) + process_PUB = mp.Process(target = self.task_local, args=(queue,q)) + process_PUB.start() + + if request is not None: + + desired_test = request["desired_test"] + test_info = {"full_id": request["full_id"], "tester": request["tester"]} + + logger.info("SSHHandler: New test proc") + self.process_test = mp.Process(target = self.task_test, args=(queue, gui_cfg, gui_cfg_host, desired_test, test_info)) + self.process_test.start() + + # Hold until test finish + logger.info("SSHHandler: Joining test proc") + self.process_test.join() + + logger.info("SSHHandler: Terminate PUB proc") + process_PUB.terminate() + + + try: + queue.close() + + except Exception as e: + logger.error("SSHHandler: PUB and test queue could not be closed: {}".format(e)) + + try: + process_PUB.terminate() + except Exception as e: + logger.error("SSHHandler: PUB and test process could not be terminated: {}".format(e)) + + def task_local(self, queue, q): + # listens for incoming data and attaches the correct topic before sending it on to SUBClient + try: + while 1 > 0: + prints = queue.get() + if "Done." in prints: + prints = 'print ; ' + str(prints) + q.put(prints) + + json = queue.get() + json = 'JSON ; ' + str(json) + q.put(str(json)) + break + else: + prints = 'print ; ' + str(prints) + q.put(prints) + + logger.info("SSHHandler: Loop has been broken.") + except: + logger.critical("SSHHandler: Local server has crashed.") + + + def task_test(self, conn_test, gui_cfg, gui_cfg_host, desired_test, test_info): + + logger.info("SSHHandler: task_test has started.") + + # Dynamically import test class and testing info + full_id = test_info["full_id"] + tester = test_info["tester"] + + test_path = gui_cfg["Test"][desired_test]['TestPath'] + test_command = gui_cfg["Test"][desired_test]['TestScript'] + + username = gui_cfg_host['TestHandler']['username'] + hostname = gui_cfg_host['TestHandler']['hostname'] + + #command to run test on ssh + #username, hostname, test_command, and test_config are specified in GUI Config + #full id, tester, and test_config are extra arguments passed to the python command being run + cmd = ['ssh', '-t', username + '@' + hostname, test_path + '/' + test_command, full_id, tester] + + #runs ssh command using python subprocess + proc = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.STDOUT, universal_newlines=True) + logger.info("SSHHandler: test has been started") + + #iterates over lines as they are received, sends them to task_local + for line in iter(proc.stdout.readline, ''): + conn_test.put(line.replace('\n', '\r\n')) + + + + + diff --git a/PythonFiles/utils/SUBClient.py b/PythonFiles/utils/SUBClient.py index 510b8905..4bfe1f70 100644 --- a/PythonFiles/utils/SUBClient.py +++ b/PythonFiles/utils/SUBClient.py @@ -1,61 +1,127 @@ # Importing necessary modules -import zmq +import zmq, logging +import PythonFiles +import os + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.utils.SUBClient') + # Creating a class for the SUBSCRIBE socket-type Client class SUBClient(): - def __init__(self, conn, queue): - - print("SUBClient has started") - # Insantiates variables + def __init__(self, conn, queue, gui_cfg, q): + with open("{}/utils/server_ip.txt".format(PythonFiles.__path__[0]), "r") as openfile: + grabbed_ip = openfile.read()[:-1] + logger.info("SUBClient has started") + # Instantiates variables self.conn = conn self.message = "" + if gui_cfg["TestHandler"]["name"] == "Local" or gui_cfg['TestHandler']['name'] == 'SSH': + self.local(conn, queue, gui_cfg, q) + else: + self.SUB_ZMQ(conn, queue, gui_cfg) + + + def local(self, conn, queue, gui_cfg, q): + try: + while 1 > 0: + # gets the signal from the Handler and splits it into topic and message + # the topic determines what SUBClient will do with the message + try: + signal = q.get() + self.topic, self.message = signal.split(" ; ") + except Exception as e: + logger.error("SUBClient: There was an error trying to get the topic and/or message from the socket") + logger.exception(e) + + # Tests what topic was received and then does the appropriate code accordingly + if self.topic == "print": + + # Places the message in the queue. the queue.get() is in + # TestInProgressScene's begin_update() method + queue.put(self.message) + + elif self.topic == "JSON": + + # Places the message in the queue. the queue.get() is in + # TestInProgressScene's begin_update() method + queue.put("Results received successfully.\r\n") + + # Sends the JSON to GUIWindow on the pipe. + self.conn.send(self.message) + + elif self.topic == "LCD": + logging.info("SUBClient: The topic of LCD has been selected. This method is empty") + pass + + else: + logging.error("SUBClient: Invalid topic sent. Must be 'print' or 'JSON'.") + + # Places the message in the queue. the queue.get() is in + # TestInProgressScene's begin_update() method + queue.put("SUBClient: An error has occurred. Check logs for more details.") + + except Exception as e: + logger.exception(e) + logger.critical("SUBClient has crashed. Please restart the software.") + + + # Responsible for listening for ZMQ messages from teststand + def SUB_ZMQ(self, conn, queue, gui_cfg): + + + grabbed_ip = gui_cfg["TestHandler"]["remoteip"] # Creates the zmq.Context object cxt = zmq.Context() # Creates the socket as the SUBSCRIBE type listen_socket = cxt.socket(zmq.SUB) - listen_socket.connect("tcp://192.168.23.23:5556") + + # TODO ZMQ is this always the correct port number? + listen_socket.connect("tcp://{ip_address}:5556".format(ip_address = grabbed_ip)) + # Sets the topics that the server will listen for listen_socket.setsockopt(zmq.SUBSCRIBE, b'print') listen_socket.setsockopt(zmq.SUBSCRIBE, b'JSON') - while 1 > 0: - # Splits up every message that is received into topic and message - # the space around the semi-colon is necessary otherwise the topic and messaage - # will have extra spaces. - self.topic, self.message = listen_socket.recv_string().split(" ; ") - # Tests what topic was received and then does the appropriate code accordingly - if self.topic == "print": - - # DEBUG - print(self.topic) - print(self.message) - # END DEBUG + try: + while 1 > 0: + # Splits up every message that is received into topic and message + # the space around the semi-colon is necessary otherwise the topic and messaage + # will have extra spaces. + try: + self.topic, self.message = listen_socket.recv_string().split(" ; ") - # Places the message in the queue. the queue.get() is in - # TestInProgressScene's begin_update() method - queue.put(self.message) - elif self.topic == "JSON": + except Exception as e: + logger.error("SUBClient: There was an error trying to get the topic and/or message from the socket") + logger.exception(e) + - # DEBUG - print("JSON Received.") - # END DEBUG + poller = zmq.Poller() + poller.register(listen_socket, zmq.POLLIN) - # Places the message in the queue. the queue.get() is in - # TestInProgressScene's begin_update() method - queue.put("JSON Received.") - + # Tests what topic was received and then does the appropriate code accordingly + if self.topic == "print": - self.conn.send(self.message) + # Places the message in the queue. the queue.get() is in + # TestInProgressScene's begin_update() method + queue.put(self.message) + elif self.topic == "JSON": + + # Places the message in the queue. the queue.get() is in + # TestInProgressScene's begin_update() method + queue.put("Results received successfully.") + # Sends the JSON to GUIWindow on the pipe. + self.conn.send(self.message) - else: + else: + logger.error("Invalid topic sent. Must be 'print' or 'JSON'.") - # DEBUG - print("Invalid topic sent. Must be 'print' or 'JSON'.") - # END DEBUG + # Places the message in the queue. the queue.get() is in + # TestInProgressScene's begin_update() method + queue.put("SUBClient: An error has occurred. Check logs for more details.") - # Places the message in the queue. the queue.get() is in - # TestInProgressScene's begin_update() method - queue.put("Invalid topic sent. Must be 'print' or 'JSON'.") + except Exception as e: + logger.exception(e) + logger.critical("SUBClient: SUBClient has crashed. Please restart the software.") diff --git a/PythonFiles/utils/ThermalREQClient.py b/PythonFiles/utils/ThermalREQClient.py new file mode 100644 index 00000000..043aba1c --- /dev/null +++ b/PythonFiles/utils/ThermalREQClient.py @@ -0,0 +1,191 @@ +##################################################################### +# # +# Adaptation from REQClient specifically for Thermal Tester # +# Additional parameter support for specifying args for test # +# # +##################################################################### + +################################################################################# + +# Importing necessary modules +import zmq, logging +import PythonFiles +import multiprocessing as mp +import os +import json +import time + +from PythonFiles.utils.LocalHandler import LocalHandler + +################################################################################# + +logger = logging.getLogger('HGCALTestGUI.PythonFiles.utils.ThermalREQClient') +#FORMAT = '%(asctime)s|%(levelname)s|%(message)s|' +#logging.basicConfig( +# filename="/home/{}/GUILogs/gui.log".format(os.getlogin()), +# filemode = 'a', +# format=FORMAT, +# level=logging.INFO +# ) + +# Making the Client Server a class +class ThermalREQClient(): + + ################################################ + + # Ensures nothing happens on instantiantion + def __init__(self, gui_cfg, desired_test, thermal_dict, full_id, tester, conn_trigger): + + logger.info("Initializing ThermalREQClient...") + + with open("{}/utils/server_ip.txt".format(PythonFiles.__path__[0]),"r") as openfile: + grabbed_ip = openfile.read()[:-1] + self.message = "" + + test_handler_name = gui_cfg.getTestHandler()["name"] + + # Run the ZMQ server on test stand and make requests via ZMQ client + if test_handler_name == "ThermalZMQ": + + self.ThermalZMQClient(gui_cfg, desired_test, thermal_dict, full_id, tester) + + # Run tests only on the current computer + elif test_handler_name == "Local": + + self.ThermalLocalClient(conn_trigger, desired_test, thermal_dict, full_id, tester) + + # Run tests on another machine via SHH (key required) + elif test_handler_name == "SSH": + + self.ThermalSSHClient(conn_trigger, desired_test, thermal_dict, full_id, tester) + + + # Handling tests run on the local machine + def ThermalLocalClient(self, conn_trigger, desired_test, thermal_dict, full_id, tester): + + desired_test = int(desired_test[4:]) + + trigger_dict = {"desired_test": desired_test, "full_id": full_id, "tester": tester} + trigger_message = json.dumps(trigger_dict) + + conn_trigger.send(trigger_message) + + # Handling tests run via SSH + def ThermalSSHClient(self, conn_trigger, desired_test, thermal_dict, full_id, tester): + + desired_test = int(desired_test[4:]) + + trigger_dict = {"desired_test": desired_test, "full_id": full_id, "tester": tester} + trigger_message = json.dumps(trigger_dict) + + conn_trigger.send(trigger_message) + + def ThermalZMQClient(self, gui_cfg, desired_test, thermal_dict, serial, tester): + + sending_msg = (desired_test, thermal_dict, tester) + # sending_msg = f"{desired_test};{thermal_dict};{tester}" + + # Establishing variable for use + self.desired_test = desired_test + # Necessary for zmqClient + context = zmq.Context() + + try: + remote_ip = gui_cfg.getTestHandler()["remoteip"] + + # Creates a socket to talk to the server + socket = context.socket(zmq.REQ) + socket.connect("tcp://{ip_address}:8898".format(ip_address = remote_ip)) + except: + logger.error("No remote_ip specified, please modify config") + + debug_msg = "Sending request to REPServer for: " +str(self.desired_test) + logger.info(debug_msg) + + # Tell the server what test to run + socket.send_json(sending_msg) + + # Timeout feature for the socket + # The poller is responsible for stopping the socket send after a certain time + # Poller is in milliseconds + #poller = zmq.Poller() + #poller.register(socket, zmq.POLLIN) + #if not poller.poll(6*1000): + # raise IOError("Timeout processing the REQClient request from socket") + + + logger.info("Request sent. Waiting for confirmation receipt...") + # Get the reply + + # Recording the number of tries to open the socket and receive a string + tries = 0 + + + REQUEST_TIMEOUT = 2000 + REQUEST_RETRIES = 3 + # socket.connect("tcp://{ip_address}:5555".format(ip_address = grabbed_ip)) + + retries_left = REQUEST_RETRIES + while (len(self.message)< 1) and tries < 1000: + + try: + #print("\nTrying to receive a message from the socket") + #logger.debug("ThermalREQClient: Trying to receive a message from the socket receive") + #self.message = socket.recv_string() + #print("\n\n\nSelf.message: {}\n\n\n".format(self.message)) + + if(socket.poll(REQUEST_TIMEOUT) & zmq.POLLIN) != 0: + self.message = socket.recv_string() + retries_left = REQUEST_RETRIES + logger.info("Request received.") + break + + retries_left -= 1 + logger.warning("No response from server") + socket.setsockopt(zmq.LINGER, 0) + socket.close() + + # Out of retries + if retries_left == 0: + logger.error("Server seems to be offline, abandoning...") + break + + logger.info("ThermalREQClient: Attempts remaining...Reconnecting to the server...") + + socket = context.socket(zmq.REQ) + + # TODO ZMQ What is "grabbed_ip" supposed to be? + socket.connect("tcp://{ip_address}:5555".format(ip_address = grabbed_ip)) + + logger.info("ThermalREQClient: Resending...") + + socket.send_string(sending_msg) + + + except: + logger.debug("No Message received from the request.") + tries = tries + 1 + #messagebox.showerror("No Message Received", "REQClient: No message received from the request.") + + + # Closes the client so the code in the GUI can continue once the request is sent. + try: + logger.debug("Trying to close the socket") + socket.close() + except: + logger.debug("Unable to close the socket") + + ################################################# + + def get_message(self): + return self.message + + ################################################# + + def set_message(self, string): + self.message = string + + ################################################# + + +################################################################################# diff --git a/PythonFiles/utils/__init__.py b/PythonFiles/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PythonFiles/utils/helper.py b/PythonFiles/utils/helper.py new file mode 100644 index 00000000..ec6327db --- /dev/null +++ b/PythonFiles/utils/helper.py @@ -0,0 +1,12 @@ +# Helper functions for smooth operation + +from pathlib import Path +import os + +def get_install_path(): + + return Path(__file__).parent.parent.parent + +def get_logging_path(): + return os.getenv("HOME") + "/GUILogs/testgui.log" + diff --git a/PythonFiles/utils/server_ip.txt b/PythonFiles/utils/server_ip.txt new file mode 100644 index 00000000..12db4b8a --- /dev/null +++ b/PythonFiles/utils/server_ip.txt @@ -0,0 +1 @@ +192.168.140.32 diff --git a/README.md b/README.md index 18121628..8db77358 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ -# WagonTestGUI +# HGCALTestGUI +Edited on 5/19/25 -This repository contains the GUI used to run the quality control testing for all HGCAL LD Wagons. +This repository contains the GUI used to run the quality control testing for all HGCAL Wagons and Engines. +It also contains the CheckIn GUI and the Photo Taking GUI. ## Setup Run these commands from your working area to pull the code in: - git clone git@github.com:UMN-CMS/WagonTestGUI.git + git clone git@github.com:UMN-CMS/HGCALTestGUI.git When updating the code, you can use the following commands: @@ -19,22 +21,90 @@ When updating the code, you can use the following commands: You can then open a pull request (PR) by going to the Github repo and then we can merge your code into the master branch. +## Package Installation +_You must use python3_. To install all of the dependancies for this project, you will need to run the following installation commands. Notice that the versioning for the opencv is important (this is to prevent extensive installation times). +``` +# If permissions denied, add a 'sudo' before each line +apt-get install python3-pil python3-pil.imagetk +apt install python3-tk +pip3 install -U numpy +pip3 install pyzmq +pip3 install pyyaml + +# Version type matters here +pip3 install opencv-contrib-python==4.5.3.56 +``` +For Photo Taking Stand Only +``` +apt install -y python3-picamera2 +``` +The drivers for the scanner can be found here; choose the appropriate drivers for your OS: +https://www.zebra.com/us/en/support-downloads/software/scanner-software/scanner-sdk-for-linux.html?downloadId=dfb89068-1045-4411-961e-6499333ef749 + +After downloading the .zip file, run +``` +unzip +cd +sudo dpkg -i * +``` + +You will also need to run "make" in the following directory of each GUI for the scanner to work: +``` +./PythonFiles/Scanner/ +``` + ## To run the program: To run the program: -Open files in VS Code (or any application that runs Python) and run the following commands in the terminal: +You will need to run "python REPServer.py" on the appropriate machine after pulling the server code from `REPSERVER GITHUB REPO LINK HERE`. + +Note: + + +You may need to update the ip address the sockets connect to in the following files: + +``` +REPServer.py +PUBServer.py +SUBClient.py +REQClient.py ``` -python -m venv virtenv +Be sure to update REPServer.py and PUBServer.py on the testing station and SUBClient.py and REQClient.py on the computer running the GUI + +You will also need to install and set up the database that you will be using at `DATABASE GITHUB REPO LINK HERE` + +This program is built to be compatible with a Zebra `MODEL NUMBER GOES HERE` Scanner. Please connect it to the computer and do any required setup before running the program. -virtenv/Scripts/activate +The database and webpage script hostnames need to be specified in the Configuration file, along with how running of the test is to be handled (locally, through SSH, or through ZMQ). +With those set up and running, open these files in VS Code (or any application that runs Python) and run the following commands in the terminal: + +``` pip install -r requests.txt +./__main__.py for HGCAL Testing GUI +./MainFunction.py for PhotoTakingGUI and CheckInGUI -python ./MainFunction.py ``` +## Using the program + +When the GUI loads in there will be a loading screen, this will change to the login scene when the GUI is loaded. +The login scene will require you to choose a user to continue into the program further. If you wish to add a new user or specify information about the testing components being used, you may click the "Admin Tools" button. However, this will require admin privileges within the database. + +The scan scene will require a board id number to be scanned to progress forward. Simply scan the QR code on the board and then hit the "Submit" button. If you scan the wrong QR Code, you may push the "Rescan" button to scan another QR code. An id number can also be entered manually. + +Most scenes in the GUI will contain the "Change Boards" and "Logout" buttons. This will allow you to return to scan scene and scan a new board in or return to login scene to choose a new user respectively. + +When using the Testing GUI, the sidebar will allow you to navigate to any frame that is not greyed out simply by click on the respective named button. If a test has been completed, it cannot be selected from the navigator, but instead requires the user to use the respective "Retest" button found on the test summary scene. + +While a test is in progress, the GUI will disable sidebar navigation and prevent you from leaving the GUI. This can be overriden using CTRL + C, but it will not halt the test. That would require a similar override on the test station's computer. + +The test summary scene contains a "More Info" button. This button will display the information found in the "data" portion of the test results in the python dictionary. This information is also viewable from the website. It also contain's a "Next Test" button. This functions identically to the "Change Boards" button found on other scenes. + +To access the website, you simply need to type "cmslab1.spa.umn.edu/Factory/EngineDB/home_page.py" into the web browser of your choice + ## Goals for this Framework: @@ -60,9 +130,12 @@ The purpose of wagons in the front-end readout train is to tranasmit data and co ### What tests are being run? -There are four tests that need to be run in order to verify a wagon is funcitioning properly: +There are four tests that need to be run in order to verify a wagon is funcitioning properly plus an initial test to make sure the tester is working properly: +- ADC Self Check: checks that the ADC functions properly before running further tests - Analog line connection check: measure the resistance of each of the analog lines on the wagon to ensure good connection - Measurement of ID resistor: each wagon has a precision resistor used for identification of wagon type that must be measured and compared to the nominal value - I2C read/write test: verify that the slow control communication along the wagon lines is working -- Bit error rate measurement: check the quality of the data sent along the wagon elinks \ No newline at end of file +- Bit error rate measurement: check the quality of the data sent along the wagon elinks + +These are just the tests for LD Wagons, to view all tests for LD and HD Wagons and Engine, visit the testing webpage. diff --git a/Tests/Test.py b/Tests/Test.py new file mode 100644 index 00000000..294a878e --- /dev/null +++ b/Tests/Test.py @@ -0,0 +1,130 @@ +#!/usr/bin/python3 + +''' +Utility class for running HGCAL tests via GUI + +This class assumes that your results dictionary stores only certain fields. +Make sure that you are syncronizing your return from the test function with that of the test class. +This should return a three object tuple: pass/fail (bool), result (string, human readable test outcome), data (dictionary) +''' + +import json +import os +import yaml + +from datetime import datetime + +class Test(): + + def __init__(self, test_func, info_dict, conn, config_path): + + self.conn = conn + self.test_func = test_func + + self.load_config(config_path) + + self.send("Initializing a test") + + # All information that should be provided from the GUI to every test + try: + self.name = info_dict['name'] + self.board_sn = info_dict['board_sn'] + self.tester = info_dict['tester'] + except: + self.send("Please provide the name of the test, board serial number, and tester name") + + + def run(self): + # Info that will be provided from running the test + self.passed, self.result, self.data = self.run_test(self.test_func, self.test_cfg) + + # Package up results into a dictionary for parsing into a JSON + self.results = {'name': self.name, 'board_sn': self.board_sn, 'tester': self.tester, 'pass': self.passed, 'data': self.data, 'result': self.result, 'time': str(datetime.now())} + + self.send(self.dump_results()) + + self.save_results() + + self.send_results(self.result) + + # Dump results in JSON format for uploading to the database + def dump_results(self): + return json.dumps(self.results) + + # Save JSON file under _.json + def save_results(self): + save_dir = "./jsons" + self.send("\nSaving results to {}\n".format("./jsons/{0}/{0}_{1}.json".format(self.name.replace(" ",""), self.board_sn))) + if not os.path.exists(save_dir): + os.makedirs(save_dir) + if not os.path.exists("{}/{}".format(save_dir, self.name.replace(" ",""))): + os.makedirs("{}/{}".format(save_dir, self.name.replace(" ",""))) + with open("./jsons/{0}/{0}_{1}.json".format(self.name.replace(" ",""), self.board_sn), "w") as f: + f.write(self.dump_results()) + + f.close() + + # Handler for sending test printouts in whatever fashion is necessary + def send(self, message): + if self.conn is None: + print(message) + else: + self.conn.send("print ; " + message) + + # Other types of connections to be implemented: MP pipes, os pipes + + # Need separate send function for results + def send_results(self): + + if self.conn is None: + print(self.results) + else: + self.conn.send("JSON ; " + json.dumps(self.results)) + + + # Get results as a python dictionary + def get_results(self): + self.send(self.results) + return self.results + + # Send results via the PUB Server + #def send_results(self): + # self.send("Test Outcome: " + self.results["result"]) + + # Loading configuration needed by test function, needs to be a yaml file + # Returns dictionary object that should be taken as an argument in test function + def load_config(self, config_path): + self.test_cfg = yaml.safe_load(open(config_path, "r")) + + # Function for running your test, kwargs must agree with defined kwargs for your test + # This function assumes that the output of the test will be the pass/fail condition (bool) + # and a dictionary (of any depth) containing the extra data to store for the test + def run_test(self, test_func, test_config): + self.send("Running the test:") + return test_func(test_config) + + +if __name__ == "__main__": + + import argparse + + parser = argparse.ArgumentParser(prog="Test Demo", description="Run your own tests by giving --config and --test command line options", epilog="Both config and test flags must be set to run your test") + + parser.add_argument("-c", "--config", dest="config_path", action="store", type=str, help="path to your test configuration file") + + args = parser.parse_args() + + test_cfg = yaml.safe_load(open(args.config_path, "r")) + test_path = test_cfg["test_path"].split(".py")[0].replace("/", ".") + test_class_name = test_cfg["test_class"] + + test_mod = __import__(test_path, fromlist=[test_class_name]) + test_class = getattr(test_mod, test_class_name) + + conn = None + board_sn = 123 + tester = "Demo Tester" + + test_class(conn, board_sn, tester, args.config_path) + + diff --git a/Tests/__init__.py b/Tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Tests/demo_count.py b/Tests/demo_count.py new file mode 100644 index 00000000..6d19af84 --- /dev/null +++ b/Tests/demo_count.py @@ -0,0 +1,62 @@ +import time, sys, os, random + +sys.path.append("{}/Tests/".format(os.getcwd())) + +from Test import Test + +# Simple test counting from 1 to 10 for demo purposes +# This is an example of how a test can be written to run locally +# Note the inheritance of the test demo class for full functionality +class counting(Test): + + def __init__(self, conn, board_sn=-1, tester="Jane Doe", config_path = "./Tests/test_configs/counting.yaml"): + + # Test metadata for storage + self.info_dict = {'name': "Count to 10", 'board_sn': board_sn, 'tester': tester} + + # One end of a multiprocessing pipe to send output to GUI console + self.conn = conn + + # Initialization of the parent test object handles result saving, + # message passing between tests and GUI + # Note that tests are run on initialization of the Test object + Test.__init__(self, self.run_count, self.info_dict, conn, config_path) + + self.run() + + def initialization(self): + + # Do some initialization of test setup + time.sleep(3) + + def run_count(self, test_cfg): + + self.initialization() + + self.send("Beginning count test:") + + max_range = test_cfg["max_range"]+1 + + try: + for i in range(1, max_range): + self.send("{}".format(i)) + time.sleep(1) + + # Simulating a failure case to show how that is handled by the Test API + if random.random() < 0.03: + raise Exception("This is a simulated random failure case") + + except: + self.send("Something went wrong while counting to {}... Don't worry, this is a simulated test failure to illustrate failure handling :)".format(max_range-1)) + test_passed = False + result = "Test failed, maximum value counted to is {}".format(i) + data = {"StartVal": 1, "EndVal": i} + return test_passed, result, data + + test_passed = True + data = {"StartVal": 1, "EndVal": i} + result = "Test passed!" + self.send("Done.") + + return test_passed, result, data + diff --git a/Tests/test_configs/counting.yaml b/Tests/test_configs/counting.yaml new file mode 100644 index 00000000..0f8ddc80 --- /dev/null +++ b/Tests/test_configs/counting.yaml @@ -0,0 +1,9 @@ +--- +# Path to where your test lives +test_path: demo_count.py +# Class to instantiate to run the test +test_class: counting + +# Optional arguments needed to run your test go below +max_range: 10 +... diff --git a/VisualInspectionGUI/MainFunctionVI.py b/VisualInspectionGUI/MainFunctionVI.py deleted file mode 100644 index ce7d843b..00000000 --- a/VisualInspectionGUI/MainFunctionVI.py +++ /dev/null @@ -1,12 +0,0 @@ -# Imports the GUIWindow -from PythonFiles.GUIWindow import GUIWindow - - -# Creates a main function to initialize the GUI -def main(): - # creates the main_window as an instantiation of GUIWindow - main_window = GUIWindow() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/VisualInspectionGUI/PythonFiles/Data/DataHolder.py b/VisualInspectionGUI/PythonFiles/Data/DataHolder.py deleted file mode 100644 index 657629f9..00000000 --- a/VisualInspectionGUI/PythonFiles/Data/DataHolder.py +++ /dev/null @@ -1,79 +0,0 @@ -################################################################################# - - -class DataHolder(): - - ################################################# - - # List of the variables being held by data holder - def __init__(self): - self.user_ID = "" # Tester's Name - self.test_stand = "" # Test stand for the unit - self.current_serial_ID = 0 # Unit Serial Number - self.test1_completed = False # Whether the test has been completed - self.test2_completed = False # Whether the test has been completed - self.test3_completed = False # Whether the test has been completed - self.test4_completed = False # Whether the test has been completed - self.test1_pass = False # Whether the test has been passed - self.test2_pass = False # Whether the test has been passed - self.test3_pass = False # Whether the test has been passed - self.test4_pass = False # Whether the test has been passed - self.board_chipped_bent = False - self.wagon_connection_pin_bent = False - self.engine_connection_pin_bent = False - self.visual_scratches = False - - ################################################# - - # Clears the data by reseting all the values to the initial state - def clear_DataHolder(self): - self.__init__() - - ################################################# - - # Future method to send data to the database - def send_to_DB(self): - pass - - ################################################# - - # Prints all the variable values inside data_holder - def print(self): - print("user_ID: ", self.user_ID) - print("test_stand: ", self.test_stand) - print("current_serial_ID: ", self.current_serial_ID) - print("test1_completed: ", self.test1_completed) - print("test2_completed: ", self.test2_completed) - print("test3_completed: ", self.test3_completed) - print("test4_completed: ", self.test4_completed) - print("test1_pass: ", self.test1_pass) - print("test2_pass: ", self.test2_pass) - print("test3_pass: ", self.test3_pass) - print("test4_pass: ", self.test4_pass) - print("board_chipped_bent: ", self.board_chipped_bent) - print("wagon_connection_pin_bent: ", self.wagon_connection_pin_bent) - print("engine_connection_pin_bent: ", self.engine_connection_pin_bent) - print("visual_scratches: ", self.visual_scratches) - - ################################################# - - # Gets all the variables from data_holder - def get(self): - return ( - self.user_ID + - self.test_stand + - str(self.current_serial_ID) + - str(self.test1_completed) + - str(self.test2_completed) + - str(self.test3_completed) + - str(self.test4_completed) + - str(self.test1_pass) + - str(self.test2_pass) + - str(self.test3_pass) + - str(self.test4_pass) - ) - - ################################################ - - -################################################################################# \ No newline at end of file diff --git a/VisualInspectionGUI/PythonFiles/GUIWindow.py b/VisualInspectionGUI/PythonFiles/GUIWindow.py deleted file mode 100644 index 5f031867..00000000 --- a/VisualInspectionGUI/PythonFiles/GUIWindow.py +++ /dev/null @@ -1,200 +0,0 @@ -################################################################################# - -# Importing all neccessary modules -from pickle import NONE -import tkinter as tk -from turtle import bgcolor -from PythonFiles.Data.DataHolder import DataHolder -from PythonFiles.Scenes.LoginScene import LoginScene -from PythonFiles.Scenes.ScanScene import ScanScene -from PythonFiles.Scenes.SplashScene import SplashScene -from PythonFiles.Scenes.TestSummaryScene import TestSummaryScene -from PythonFiles.Scenes.InspectionScenes.Inspection1 import Inspection1 - - -################################################################################# - - -# Create a class for creating the basic GUI Window to be called by the main function to -# instantiate the actual object -class GUIWindow(): - - ################################################# - - def __init__(self): - # Create the window named "master_window" - # global makes master_window global and therefore accessible outside the function - global master_window - master_window = tk.Tk() - master_window.title("Bethel Interns' Window") - - # Creates the size of the window and disables resizing - master_window.geometry("850x500+25+100") - master_window.resizable(0,0) - - # Removes the tkinter logo from the window - # master_window.wm_attributes('-toolwindow', 'True') - - # Creates and packs a frame that exists on top of the master_frame - master_frame = tk.Frame(master_window, width=850, height= 500) - master_frame.grid(column = 0, row = 0, columnspan = 4) - - # Creates the "Storage System" for the data during testing - self.data_holder = DataHolder() - - - ################################################# - # Creates all the different frames in layers # - ################################################# - - # At top so it can be referenced by other frames' code... Order of creation matters - self.test_summary_frame = TestSummaryScene(self, master_frame, self.data_holder) - self.test_summary_frame.grid(row=0, column=0) - - self.login_frame = LoginScene(self, master_frame, self.data_holder) - self.login_frame.grid(row=0, column=0) - - self.scan_frame = ScanScene(self, master_frame, self.data_holder) - self.scan_frame.grid(row=0, column=0) - - self.inspection_frame = Inspection1(self, master_frame, self.data_holder) - self.inspection_frame.grid(row=0,column=0) - - - # Near bottom so it can reference other frames with its code - self.splash_frame = SplashScene(self, master_frame) - self.splash_frame.grid(row=0, column=0) - - ################################################# - # End Frame Creation # - ################################################# - - # Tells the master window that its exit window button is being given a new function - master_window.protocol('WM_DELETE_WINDOW', self.exit_function) - - # Sets the current frame to the splash frame - self.set_frame_splash_frame() - - master_frame.after(500, self.set_frame_login_frame) - - master_window.mainloop() - - ################################################# - - def set_frame_login_frame(self): - - self.set_frame(self.login_frame) - - ################################################# - - def set_frame_inspection_frame(self): - self.inspection_frame.update_frame(self) - self.set_frame(self.inspection_frame) - - ################################################# - - def set_frame_scan_frame(self): - - self.scan_frame.is_current_scene = True - self.scan_frame.scan_QR_code() - self.set_frame(self.scan_frame) - - ################################################# - - def set_frame_splash_frame(self): - - self.set_frame(self.splash_frame) - - ################################################# - - def set_frame_test_summary(self): - self.test_summary_frame.update_frame() - self.set_frame(self.test_summary_frame) - - - - ################################################# - - # Called to change the frame to the argument _frame - def set_frame(self, _frame): - - ############################################################################# - # The Following Code Determines What Buttons Are Visible On The Side Bar # - ############################################################################# - - - - # Hides the submit button on scan frame until an entry is given to the computer - if (_frame is not self.scan_frame): - self.scan_frame.is_current_scene = False - self.scan_frame.hide_submit_button() - - - ############################################################################# - # End Button Visibility Code # - ############################################################################# - - # Raises the passed in frame to be the current frame - _frame.tkraise() - - ################################################# - - # New function for clicking on the exit button - def exit_function(self): - - # Creates a popup to confirm whether or not to exit out of the window - global popup - popup = tk.Tk() - # popup.wm_attributes('-toolwindow', 'True') - popup.title("Exit Confirmation Window") - popup.geometry("300x150") - popup.eval("tk::PlaceWindow . center") - - - # Creates frame in the new window - frm_popup = tk.Frame(popup) - frm_popup.pack() - - # Creates label in the frame - lbl_popup = tk.Label(frm_popup, text = "Are you sure you would like to exit?") - lbl_popup.grid(column = 0, row = 0, columnspan = 2, pady = 25) - - # Creates yes and no buttons for exiting - btn_yes = tk.Button( - frm_popup, - text = "Yes", - relief = tk.RAISED, - command = lambda: self.destroy_function() - ) - btn_yes.grid(column = 0, row = 1) - - btn_no = tk.Button( - frm_popup, - text = "No", - relief = tk.RAISED, - command = lambda: self.destroy_popup() - ) - btn_no.grid(column = 1, row = 1) - - ################################################# - - # Called when the no button is pressed to destroy popup and return you to the main window - def destroy_popup(self): - popup.destroy() - - ################################################# - - # Called when the yes button is pressed to destroy both windows - def destroy_function(self): - - # Destroys the popup and master window - popup.destroy() - master_window.destroy() - - # Ensures the application closes with the exit button - exit() - - ################################################# - - -################################################################################# \ No newline at end of file diff --git a/VisualInspectionGUI/PythonFiles/Scenes/InspectionScenes/Inspection1.py b/VisualInspectionGUI/PythonFiles/Scenes/InspectionScenes/Inspection1.py deleted file mode 100644 index 30e8e4f4..00000000 --- a/VisualInspectionGUI/PythonFiles/Scenes/InspectionScenes/Inspection1.py +++ /dev/null @@ -1,250 +0,0 @@ -################################################################################# - -# Importing Necessary Modules -import tkinter as tk -import tkinter.font as font - - -################################################################################# - - -# Creating class for the window -class Inspection1(tk.Frame): - - ################################################# - - def __init__(self, parent, master_frame, data_holder): - super().__init__(master_frame, width=850, height=500) - - self.test_name = "SOMETHING STRING" - self.data_holder = data_holder - - self.update_frame(parent) - - ################################################# - - def update_frame(self, parent): - - # Creates a font to be more easily referenced later in the code - font_scene = ('Arial', 12) - font_scene_14 = ('Arial', 14) - - # Create a centralized window for information - frm_window = tk.Frame(self, width = 675, height = 500) - frm_window.grid(column=0, row=0, padx=10, pady=10) - - # Create a label for the tester's name - lbl_tester = tk.Label( - frm_window, - text = "Tester: ", - font = font_scene - ) - lbl_tester.grid(row=0, column=0, pady=15 ) - - # Create an entry for the tester's name - ent_tester = tk.Entry( - frm_window, - font = font_scene - ) - ent_tester.insert(0, self.data_holder.user_ID) - ent_tester.grid(row=0, column=1, pady=15 ) - ent_tester.config(state = "disabled") - - # Create a label for the serial number box - lbl_snum = tk.Label( - frm_window, - text = "Serial Number: ", - font = font_scene - ) - lbl_snum.grid(row=0, column=2, pady=15 ) - - # Create a entry for the serial number box - ent_snum = tk.Entry( - frm_window, - font = font_scene - ) - ent_snum.insert(0, self.data_holder.current_serial_ID) - ent_snum.grid(row=0, column=3, pady=15) - ent_snum.config(state = "disabled") - - - self.board_chipped_bent = tk.BooleanVar() - self.wagon_connection_pin_bent = tk.BooleanVar() - self.engine_connection_pin_bent = tk.BooleanVar() - self.visual_scratches = tk.BooleanVar() - - - - # Beginning of the CheckButtons - - # Checkbutton1 - c1 = tk.Checkbutton( - frm_window, - font = font_scene_14, - text='Board Chipped/Bent', - variable= self.board_chipped_bent, - onvalue=1, - offvalue=0 - # command=print_selection - ) - c1.grid(row = 1, column= 1, sticky='w', columnspan=2) - - # Checkbutton2 - c2 = tk.Checkbutton( - frm_window, - font = font_scene_14, - text='Wagon Connection Pin Bent', - variable= self.wagon_connection_pin_bent, - onvalue=True, - offvalue=False - # command=print_selection - ) - c2.grid(row = 2, column= 1, sticky='w', columnspan=2) - - # Checkbutton3 - c3 = tk.Checkbutton( - frm_window, - font = font_scene_14, - text='Engine Connection Pin Bent', - variable= self.engine_connection_pin_bent, - onvalue=True, - offvalue=False - # command=print_selection - ) - c3.grid(row = 3, column= 1, sticky='w', columnspan=2) - - # Checkbutton4 - c4 = tk.Checkbutton( - frm_window, - font = font_scene_14, - text='Visual Scratches', - variable= self.visual_scratches, - onvalue=True, - offvalue=False - # command=print_selection - ) - c4.grid(row = 4, column= 1, sticky='w', columnspan=2) - - - - - - - # Create a label for the serial number box - lbl_snum = tk.Label( - frm_window, - text = "Comments:", - font = font_scene - ) - lbl_snum.grid(row=5, column=1, pady=(25, 0) ) - - # Comment Box - comment_box = tk.Text( - frm_window, - font = font_scene, - state= 'normal', - width= 75, - height = 5 - ) - comment_box.grid(row = 6, column =1, sticky='w', columnspan=5) - - - - - - - - # Create a button for confirming test - btn_confirm = tk.Button( - frm_window, - text = "Confirm", - relief = tk.RAISED, - command = lambda:self.btn_confirm_action(parent) - ) - btn_confirm.grid(row = 9, column= 1, pady= 50) - btn_confirm['font'] = font.Font(family = 'Arial', size = 13) - - - - - - - # Create frame for logout button - nav_frame = tk.Frame(self) - nav_frame.grid(column = 1, row = 0, sticky = 'ne', padx =5) - - - # Create a rescan button - btn_rescan = tk.Button( - nav_frame, - text = "Change Boards", - relief = tk.RAISED, - command = lambda: self.btn_rescan_action(parent)) - btn_rescan.pack(anchor = 'ne', pady=15) - - # Create a logout button - btn_logout = tk.Button( - nav_frame, - text = "Logout", - relief = tk.RAISED, - command = lambda: self.btn_logout_action(parent)) - btn_logout.pack(anchor = 'se') - - # # # # # # # # # - - - self.grid_columnconfigure(4, weight=1) - self.grid_rowconfigure(0, weight=1) - - - frm_window.grid_propagate(0) - self.grid_propagate(0) - - ################################################# - - # Rescan button takes the user back to scanning in a new board - def btn_rescan_action(self, _parent): - _parent.set_frame_scan_frame() - - ################################################# - - # Back button action takes the user back to the scanning device - def btn_back_action(self, _parent): - pass - - ################################################# - - def update_data_holder(self): - self.data_holder.board_chipped_bent = self.board_chipped_bent.get() - self.data_holder.wagon_connection_pin_bent = self.wagon_connection_pin_bent.get() - self.data_holder.engine_connection_pin_bent = self.engine_connection_pin_bent.get() - self.data_holder.visual_scratches = self.visual_scratches.get() - - self.data_holder.print() - - - ################################################# - - # Confirm button action takes the user to the test in progress scene - def btn_confirm_action(self, _parent): - - self.update_data_holder() - - # # # # # # # # # # # # # # # # # # # # # # # # # # # - # ++ GOAL CODE ++ # - # def confirm(): # - # set_frame_TIPS() # - # Runs_Test() # Might include multithread # - # Get_Results() # - # Update_Dataholder() # - # Go_To_Next_Test() # - # # # # # # # # # # # # # # # # # # # # # # # # # # # - pass - - ################################################# - - # functionality for the logout button - def btn_logout_action(self, _parent): - _parent.set_frame_login_frame() - - ################################################# \ No newline at end of file diff --git a/VisualInspectionGUI/PythonFiles/Scenes/ScanScene.py b/VisualInspectionGUI/PythonFiles/Scenes/ScanScene.py deleted file mode 100644 index 2faa0747..00000000 --- a/VisualInspectionGUI/PythonFiles/Scenes/ScanScene.py +++ /dev/null @@ -1,271 +0,0 @@ -################################################################################# - -# importing necessary modules -import threading, time -import tkinter as tk -from tkinter import * -from turtle import back -from PIL import ImageTk as iTK -from PIL import Image -import tkinter.font as font - -################################################################################# - - -# Creating variable for testing QR Code entry -QRcode = "1090201033667425" - - -# creating the Scan Frame's class (called ScanScene) to be instantiated in the GUIWindow -# instantiated as scan_frame by GUIWindow -# @param parent -> passes in GUIWindow as the parent. -# @param master_frame -> passes master_frame as the container for everything in the class. -# @param data_holder -> passes data_holder into the class so the data_holder functions can -# be accessed within the class. -class ScanScene(tk.Frame): - - ################################################# - - # Runs upon creation - def __init__(self, parent, master_frame, data_holder): - self.data_holder = data_holder - self.is_current_scene = False - - # Runs the initilize_GUI function, which actually creates the frame - # params are the same as defined above - self.initialize_GUI(parent, master_frame) - - - # Creates a thread for the scanning of a barcode - # Needs to be updated to run the read_barcode function in the original GUI - def scan_QR_code(self): - print("Begin to scan") - ent_snum.config(state = 'normal') - self.QR_thread = threading.Thread(target=self.insert_QR_ID) - self.QR_thread.daemon = True - self.QR_thread.start() - - # Updates the QR ID in the task - # Place holder function to insert the QRcode into the textbox - def insert_QR_ID(self): - - # Clears the textbox of anything possibly in the box - ent_snum.delete(0, END) - - # Disables the rescan button until after the scanning is complete - self.hide_rescan_button() - - # Runs the hide_submit_button function and sets a default value to the QR_value - self.hide_submit_button() - self.scanned_QR_value = 000 - - # Delay to simulate scanning a QRcode - for i in range(1): - time.sleep(1) - print(i + 1) - time.sleep(0.5) - print("Finished Scan") - ent_snum.insert(0, QRcode) - ent_snum.config(state = 'disabled') - - # Restores access to the rescan button - self.show_rescan_button() - - - # Sets the scanned_QR_value to 0 when the function is not in use - if (not self.is_current_scene): - self.scanned_QR_value = 0 - else: - self.scanned_QR_value = QRcode - self.data_holder.current_serial_ID = self.scanned_QR_value - - self.data_holder.print() - - - # Creates the GUI itself - def initialize_GUI(self, parent, master_frame): - - self.master_frame = master_frame - - super().__init__(self.master_frame, width = 850, height = 500) - - # Create a photoimage object of the QR Code - QR_image = Image.open("./PythonFiles/Images/QRimage.png") - QR_PhotoImage = iTK.PhotoImage(QR_image) - QR_label = tk.Label(self, image=QR_PhotoImage) - QR_label.image = QR_PhotoImage - - # the .grid() adds it to the Frame - QR_label.grid(column=1, row = 0) - - Scan_Board_Prompt_Frame = Frame(self,) - Scan_Board_Prompt_Frame.grid(column=0, row = 0) - - # creates a Label Variable, different customization options - lbl_scan = tk.Label( - master= Scan_Board_Prompt_Frame, - text = "Scan the QR Code on the Board", - font = ('Arial', 18) - ) - lbl_scan.pack(padx = 50, pady = 50) - - # Create a label to label the entry box - lbl_snum = tk.Label( - Scan_Board_Prompt_Frame, - text = "Serial Number:", - font = ('Arial', 16) - ) - lbl_snum.pack() - - # Entry for the serial number to be displayed. Upon Scan, update and disable? - global ent_snum - - # Creating intial value in entry box - user_text = tk.StringVar(self) - - # Creates an entry box - ent_snum = tk.Entry( - Scan_Board_Prompt_Frame, - font = ('Arial', 16), - textvariable= user_text, - ) - ent_snum.pack(padx = 50) - - # Traces an input to show the submit button once text is inside the entry box - user_text.trace( - "w", - lambda name, - index, - mode, - sv=user_text: self.show_submit_button() - ) - - # Rescan button creation - self.btn_rescan = tk.Button( - Scan_Board_Prompt_Frame, - text="Rescan", - padx = 50, - pady =10, - relief = tk.RAISED, - command = lambda: self.scan_QR_code() - ) - self.btn_rescan.pack(padx=10, pady=30) - - # Submit button creation - self.btn_submit = tk.Button( - Scan_Board_Prompt_Frame, - text="Submit", - padx = 50, - pady = 10, - relief = tk.RAISED, - command= lambda: self.btn_submit_action(parent) - ) - self.btn_submit.pack(padx=10) - - # Creating frame for logout button - frm_logout = tk.Frame(self) - frm_logout.grid(column = 1, row = 1, sticky= 'se') - - # Creating the logout button - btn_logout = tk.Button( - frm_logout, - relief = tk.RAISED, - text = "Logout", - command = lambda: self.btn_logout_action(parent) - ) - btn_logout.pack(anchor = 'se', padx = 230, pady = 180) - - # Locks frame size to the master_frame size - self.grid_propagate(0) - - ################################################# - - # Function for the submit button - def btn_submit_action(self, _parent): - _parent.set_frame_inspection_frame() - - ################################################# - - # Function for the log out button - def btn_logout_action(self, _parent): - - # Send user back to login frame - _parent.set_frame_login_frame() - - ################################################# - - # Function to activate the submit button - def show_submit_button(self): - self.btn_submit["state"] = "active" - - ################################################# - - # Function to disable to the submit button - def hide_submit_button(self): - self.btn_submit["state"] = "disabled" - - ################################################# - - # Function to activate the rescan button - def show_rescan_button(self): - self.btn_rescan["state"] = "active" - - ################################################# - - # Function to disable to the rescan button - def hide_rescan_button(self): - self.btn_rescan["state"] = "disabled" - - ################################################# - - # Creates a thread for the scanning of a barcode - # Needs to be updated to run the read_barcode function in the original GUI - def scan_QR_code(self): - print("Begin to scan") - ent_snum.config(state = 'normal') - self.QR_thread = threading.Thread(target=self.insert_QR_ID) - self.QR_thread.daemon = True - self.QR_thread.start() - - ################################################# - - # Updates the QR ID in the task - # Place holder function to insert the QRcode into the textbox - def insert_QR_ID(self): - - # Clears the textbox of anything possibly in the box - ent_snum.delete(0, END) - - # Disables the rescan button until after the scanning is complete - self.hide_rescan_button() - - # Runs the hide_submit_button function and sets a default value to the QR_value - self.hide_submit_button() - self.scanned_QR_value = 000 - - # Delay to simulate scanning a QRcode - - time.sleep(1) - print(1) - time.sleep(0.5) - print("Finished Scan") - ent_snum.insert(0, QRcode) - ent_snum.config(state = 'disabled') - - # Restores access to the rescan button - self.show_rescan_button() - - - # Sets the scanned_QR_value to 0 when the function is not in use - if (not self.is_current_scene): - self.scanned_QR_value = 0 - else: - self.scanned_QR_value = QRcode - self.data_holder.current_serial_ID = self.scanned_QR_value - - self.data_holder.print() - - ################################################# - - -################################################################################# \ No newline at end of file diff --git a/VisualInspectionGUI/PythonFiles/Scenes/TestSummaryScene.py b/VisualInspectionGUI/PythonFiles/Scenes/TestSummaryScene.py deleted file mode 100644 index d9cb124a..00000000 --- a/VisualInspectionGUI/PythonFiles/Scenes/TestSummaryScene.py +++ /dev/null @@ -1,383 +0,0 @@ -################################################################################# - -import json -import tkinter as tk -from PIL import ImageTk as iTK -from PIL import Image -from matplotlib.pyplot import table -from pyparsing import col - -################################################################################# - - - -# Frame that shows all of the final test results -# @param parent -> References a GUIWindow object -# @param master_frame -> Tkinter object that the frame is going to be placed on -# @param data_holder -> DataHolder object that stores all relevant data - -class TestSummaryScene(tk.Frame): - - ################################################# - - def __init__(self, parent, master_frame, data_holder): - - self.parent = parent - - # Call to the super class's constructor - # Super class is the tk.Frame class - super().__init__(master_frame, width=850, height=500) - - self.data_holder = data_holder - - # Setting weights of columns so the column 4 is half the size of columns 0-3 - self.columnconfigure(0, weight = 2) - self.columnconfigure(1, weight = 2) - self.columnconfigure(2, weight = 2) - self.columnconfigure(3, weight = 2) - self.columnconfigure(4, weight = 1) - # Instantiates an updated table with the current data - self.create_updated_table(parent) - - # Adds the title to the TestSummary Frame - self.title = tk.Label( - self, - fg='#0d0d0d', - text = "Testing Finished!", - font=('Arial',18,'bold') - ) - self.title.grid(row= 0, column= 1, pady = 20) - - # Fits the frame to set size rather than interior widgets - self.grid_propagate(0) - - ################################################# - - - # Creates the table with the updated information from the data_holder - # @param parent -> References the GUIWindow object that creates the class - - def create_updated_table(self, parent): - - - self.list_of_tests = ["General Resistance Test", "ID Resistor Test", "I2C Comm. Test", "Bit Rate Test"] - self.list_of_table_labels = ["Test Name", "Test Status", "Pass/Fail"] - self.list_of_completed_tests = [self.data_holder.test1_completed, self.data_holder.test2_completed, self.data_holder.test3_completed, self.data_holder.test4_completed] - self.list_of_pass_fail = [self.data_holder.test1_pass, self.data_holder.test2_pass, self.data_holder.test3_pass, self.data_holder.test4_pass] - - - # Adds Board Serial Number to the TestSummaryFrame - self.lbl_snum = tk.Label( - self, - text = "Serial Number: " + str(self.data_holder.current_serial_ID), - font=('Arial', 14) - ) - self.lbl_snum.grid(column = 2, row = 0, pady = 20) - - # Adds Tester Name to the TestSummary Frame - self.lbl_tester = tk.Label( - self, - text = "Tester: " + self.data_holder.user_ID, - font=('Arial', 14) - ) - self.lbl_tester.grid(column = 0, row = 0, pady = 20) - - - # Creates the "table" as a frame object - self.frm_table = tk.Frame(self) - self.frm_table.grid(row = 1, column= 0, columnspan = 4, rowspan = 4) - - # Setting weights of columns so the column 4 is half the size of columns 0-3 - self.frm_table.columnconfigure(0, weight = 2) - self.frm_table.columnconfigure(1, weight = 2) - self.frm_table.columnconfigure(2, weight = 2) - self.frm_table.columnconfigure(3, weight = 1) - self.frm_table.columnconfigure(4, weight = 1) - - - - # Adds the labels to the top of the table - for index in range(len(self.list_of_table_labels)): - _label = tk.Label( - self.frm_table, - text = self.list_of_table_labels[index], - relief = 'ridge', - width=25, - height=1, - font=('Arial', 11, "bold") - ) - _label.grid(row= 0, column=index) - - - # Adds the test names to the first column - for index in range(len(self.list_of_tests)): - _label= tk.Label( - self.frm_table, - text = self.list_of_tests[index], - relief = 'ridge', - width=25, - height=5, - font=('Arial', 11) - ) - _label.grid(row=index + 1, column=0) - - - - # Create Labels that tell whether or not a test was completed - for index in range(len(self.list_of_completed_tests)): - - # Instantiates a Label - _label = tk.Label( - self.frm_table, - relief = 'ridge', - width=25, - height=5, - font=('Arial',11) - ) - - # if the test is completed, set the label to "Complete" - if (self.list_of_completed_tests[index]): - _label.config( - text = "COMPLETED" - ) - # else, set the label to "Unfinished" - else: - _label.config( - text = "UNFINISHED" - ) - - # Puts the completed/unfinished label into the table - _label.grid(row=index + 1, column=1) - - - # Adds the Image as to whether the test was completed or not - for index in range(len(self.list_of_pass_fail)): - if(self.list_of_pass_fail[index]): - # Create a photoimage object of the QR Code - Green_Check_Image = Image.open("./PythonFiles/Images/GreenCheckMark.png") - Green_Check_Image = Green_Check_Image.resize((75,75), Image.ANTIALIAS) - Green_Check_PhotoImage = iTK.PhotoImage(Green_Check_Image) - GreenCheck_Label = tk.Label(self.frm_table, image=Green_Check_PhotoImage, width=75, height=75) - GreenCheck_Label.image = Green_Check_PhotoImage - - GreenCheck_Label.grid(row=index + 1, column=2) - - else: - # Create a photoimage object of the QR Code - Red_X_Image = Image.open("./PythonFiles/Images/RedX.png") - Red_X_Image = Red_X_Image.resize((75,75), Image.ANTIALIAS) - Red_X_PhotoImage = iTK.PhotoImage(Red_X_Image) - RedX_Label = tk.Label(self.frm_table, image=Red_X_PhotoImage, width=75, height=75) - RedX_Label.image = Red_X_PhotoImage - - RedX_Label.grid(row=index + 1, column=2) - - - self.create_retest_more_info_btns(parent) - - self.grid_propagate(0) - - ################################################# - - # Creates all of the retest button - def create_retest_more_info_btns(self, parent): - - - row1 = tk.Frame(self.frm_table) - row1.grid(column = 3, row = 1) - - btn_retest1 = tk.Button( - row1, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest1_action(parent) - ) - btn_retest1.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info1 = tk.Button( - row1, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info1_action(parent) - ) - btn_more_info1.grid(column=0, row = 0) - - - - - row2 = tk.Frame(self.frm_table) - row2.grid(column = 3, row = 2) - - btn_retest2 = tk.Button( - row2, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest2_action(parent) - ) - btn_retest2.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info2 = tk.Button( - row2, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info2_action(parent) - ) - btn_more_info2.grid(column=0, row = 0) - - - - - row3 = tk.Frame(self.frm_table) - row3.grid(column = 3, row = 3) - - btn_retest3 = tk.Button( - row3, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest3_action(parent) - ) - btn_retest3.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info3 = tk.Button( - row3, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info3_action(parent) - ) - btn_more_info3.grid(column=0, row = 0) - - - - - - - row4 = tk.Frame(self.frm_table) - row4.grid(column = 3, row = 4) - - btn_retest4 = tk.Button( - row4, - text = "RETEST", - padx= 5, - pady=5, - command = lambda: self.btn_retest4_action(parent) - ) - btn_retest4.grid(column = 1, row = 0, padx=5, pady=5) - - btn_more_info4 = tk.Button( - row4, - text = "MORE INFO", - padx= 5, - pady=5, - command = lambda: self.btn_more_info4_action(parent) - ) - btn_more_info4.grid(column=0, row = 0) - - - - - btn_next_test = tk.Button( - self.frm_table, - text = "NEXT TEST", - font = ('Arial', 15), - command = lambda: self.btn_next_test_action(parent) - ) - btn_next_test.grid(column = 3, row = 5) - - ################################################# - - # A function to be called within GUIWindow to create the console output - # when the frame is being brought to the top - def create_JSON_popup(self, JSON_String): - - # Creating a popup window for the JSON Details - self.JSON_popup = tk.Tk() - self.JSON_popup.geometry("500x300+750+100") - self.JSON_popup.title("JSON Details") - # self.JSON_popup.wm_attributes('-toolwindow', 'True') - - - - # Creating a Frame For Console Output - frm_JSON = tk.Frame(self.JSON_popup, width = 500, height = 300, bg = 'green') - frm_JSON.pack_propagate(0) - frm_JSON.pack() - - # Placing an entry box in the frm_console - self.JSON_entry_box = tk.Text( - frm_JSON, - bg = '#6e5e5d', - fg = 'white', - font = ('Arial', 14) - ) - self.JSON_entry_box.pack(anchor = 'center', fill=tk.BOTH, expand=1) - - current_JSON_file = open(JSON_String) - current_JSON_data = json.load(current_JSON_file) - - - temp = "" - for key, value in current_JSON_data.items(): - temp = temp + "{} : {}".format(key, value) + "\n" - - - self.JSON_entry_box.delete(1.0,"end") - self.JSON_entry_box.insert(1.0, temp) - - ################################################# - - # All of the different methods for what the retest buttons should do - def btn_retest1_action(self, _parent): - _parent.set_frame(_parent.test1_frame) - - def btn_retest2_action(self, _parent): - _parent.set_frame(_parent.test2_frame) - - def btn_retest3_action(self, _parent): - _parent.set_frame(_parent.test3_frame) - - def btn_retest4_action(self, _parent): - _parent.set_frame(_parent.test4_frame) - - ################################################# - - def btn_more_info1_action(self, _parent): - self.create_JSON_popup(".\PythonFiles\JSONFiles\DummyJSONTest.JSON") - - def btn_more_info2_action(self, _parent): - self.create_JSON_popup(".\PythonFiles\JSONFiles\GarrettJSONTest.JSON") - - def btn_more_info3_action(self, _parent): - self.create_JSON_popup(".\PythonFiles\JSONFiles\DummyJSONTest.JSON") - - def btn_more_info4_action(self, _parent): - self.create_JSON_popup(".\PythonFiles\JSONFiles\GarrettJSONTest.JSON") - - ################################################# - - # Next test button action - def btn_next_test_action(self, _parent): - _parent.set_frame(_parent.scan_frame) - - ################################################# - - # Updates the frame to show current data - def update_frame(self): - self.create_updated_table(self.parent) - - ################################################# - - # TODO Check what this is used for - def add_new_test(self, _list_of_completed_tests, _list_of_pass_fail): - self.list_of_completed_tests = _list_of_completed_tests - self.list_of_pass_fail = _list_of_pass_fail - - ################################################# - - -################################################################################# \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/__main__.py b/__main__.py new file mode 100755 index 00000000..a57624fa --- /dev/null +++ b/__main__.py @@ -0,0 +1,250 @@ +#!/TestingEnv/bin/python + +# Including information about both Engine and Wagon GUIs + + +# Need to make the log file path before any imports +import os +from pathlib import Path +from PythonFiles.utils.helper import get_logging_path + +guiLogPath = "{}".format(get_logging_path()) +guiLogDir = "/".join(guiLogPath.split("/")[:-1]) + +if not os.path.exists(guiLogDir): + os.makedirs(guiLogDir) + +# Importing necessary modules +import multiprocessing as mp +import socket +# Imports the GUIWindow and Handlers +from PythonFiles.GUIWindow import GUIWindow +from PythonFiles.utils.SUBClient import SUBClient +from PythonFiles.update_config import update_config +from PythonFiles.utils.LocalHandler import LocalHandler +from PythonFiles.utils.SSHHandler import SSHHandler +import sys +import logging +import logging.handlers +from logging.handlers import QueueHandler, QueueListener +import yaml +from pathlib import Path + +# create logger with 'HGCALTestGUI' +logger = logging.getLogger('HGCALTestGUI') +logger.setLevel(logging.DEBUG) + +# avoid duplicate handlers +if not logger.handlers: + # file handler which rotates every day + fh = logging.handlers.TimedRotatingFileHandler(guiLogPath, when="midnight", interval=1) + fh.setLevel(logging.DEBUG) + + # create console handler with a higher log level + ch = logging.StreamHandler() + ch.setLevel(logging.ERROR) + + # create formatter and add it to the handlers + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + fh.setFormatter(formatter) + ch.setFormatter(formatter) + + # add the handlers to the logger + logger.addHandler(fh) + logger.addHandler(ch) + +class StreamToLogger(object): + def __init__(self, logger, level): + self.logger = logger + self.level = level + self.buffer = '' + + def write(self, message): + if message != '\n': + self.logger.log(self.level, message.strip()) + + def flush(self): + pass + +sys.stdout = StreamToLogger(logger, logging.DEBUG) +#sys.stderr = StreamToLogger(logger, logging.ERROR) + +def handle_exception(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) + +sys.excepthook = handle_exception + + +# Creates a task of creating the GUIWindow +def task_GUI(conn, conn_trigger, queue, board_cfg, curpath): + # creates the main_window as an instantiation of GUIWindow + try: + main_window = GUIWindow(conn, conn_trigger, queue, board_cfg, curpath) + except Exception: + logger.error("Uncaught exception in task_GUI", exc_info=True) + raise + +# Creates a task of creating the SUBClient +def task_SUBClient(conn, queue, board_cfg, sub_pipe): + # Creates the SUBSCRIBE Socket Client + try: + sub_client = SUBClient(conn, queue, board_cfg, sub_pipe) + except Exception: + logger.error("Uncaught exception in task_SUBClient", exc_info=True) + raise + + +# Function to create the handler of the type specified in the config file +def task_LocalHandler(gui_cfg, conn_trigger, local_pipe): + + try: + LocalHandler(gui_cfg, conn_trigger, local_pipe) + except Exception: + logger.error("Uncaught exception in task_LocalHandler", exc_info=True) + raise + +def task_SSHHandler(gui_cfg, host_cfg, conn_trigger, queue): + + try: + SSHHandler(gui_cfg, host_cfg, conn_trigger, queue) + except Exception: + logger.error("Uncaught exception in task_SSHHandler", exc_info=True) + raise + +def run(board_cfg, curpath, host_cfg): + + # Creates a Pipe for the SUBClient to talk to the GUI Window + conn_SUB, conn_GUI = mp.Pipe() + + # Create another pipe to trigger the tests when needed + if host_cfg["TestHandler"]["name"] != "ZMQ": + conn_trigger_GUI, conn_trigger_Handler = mp.Pipe() + + # Creates a queue to send information to the testing window + queue = mp.Queue() + + #logging.FileHandler(guiLogPath + "gui.log", mode='a') + + # Turns creating the GUI and creating the SUBClient tasks into processes + if host_cfg["TestHandler"]["name"] == "Local": + # Creates a Queue to connect SUBClient and Handler + q = mp.Queue() + process_GUI = mp.Process(target = task_GUI, args=(conn_GUI, conn_trigger_GUI, queue, board_cfg, curpath)) + process_Handler = mp.Process(target = task_LocalHandler, args=(board_cfg, conn_trigger_Handler, q)) + process_SUBClient = mp.Process(target = task_SUBClient, args = (conn_SUB, queue, board_cfg, q)) + + elif host_cfg["TestHandler"]["name"] == "SSH": + q = mp.Queue() + process_GUI = mp.Process(target = task_GUI, args=(conn_GUI, conn_trigger_GUI, queue, board_cfg, curpath)) + process_Handler = mp.Process(target = task_SSHHandler, args=(board_cfg, host_cfg, conn_trigger_Handler, q)) + process_SUBClient = mp.Process(target = task_SUBClient, args = (conn_SUB, queue, board_cfg, q)) + + else: + process_GUI = mp.Process(target = task_GUI, args=(conn_GUI, None, queue, board_cfg, curpath)) + process_SUBClient = mp.Process(target = task_SUBClient, args = (conn_SUB, queue, host_cfg, None)) + + # Starts the processes + process_GUI.start() + if host_cfg["TestHandler"]["name"] == "Local" or host_cfg['TestHandler']['name'] == 'SSH': + process_Handler.start() + process_SUBClient.start() + + # holds the code at this line until the GUI process ends + process_GUI.join() + + try: + #closes multiprocessing connections + conn_SUB.close() + conn_GUI.close() + conn_trigger_GUI.close() + conn_trigger_Handler.close() + except: + logger.debug("Pipe close is unnecessary.") + + try: + # Cleans up the SUBClient process + process_SUBClient.terminate() + process_Handler.kill() + except: + logger.debug("Terminate is unnecessary.") + pass + + +def import_yaml(config_path): + + return yaml.safe_load(open(config_path,"r")) + +def main(args): + pass + +if __name__ == "__main__": + + logger.info("Creating new instance of HGCALTestGUI") + + try: + if sys.argv[1] is not None: + config_path = sys.argv[1] + except: + config_path = None + + try: + if sys.argv[2] is not None: + host_path = sys.argv[2] + except: + if config_path: + host_path = sys.argv[1] + else: + host_path = None + + curpath = Path(__file__).parent.absolute() + logger.info("Current path is: %s" % curpath) + + node = socket.gethostname() + logger.info("Node is: %s" % socket.gethostname()) + + ld_wagon_computers = [ + "cmsfactory4.cmsfactorynet", + "cmsfactory5.cmsfactorynet", + ] + hd_wagon_computers = [] + ld_engine_computers = [ + "cmsfactory1.cmsfactorynet", + "cmsfactory2.cmsfactorynet", + ] + hd_engine_computers = [] + + if config_path is not None: + board_cfg = import_yaml(config_path) + host_cfg = import_yaml(host_path) + logger.info("Board Config: " + config_path) + logger.info("Host Config: " + host_path) + + run(board_cfg, curpath, host_cfg) + elif any((node in x for x in ld_wagon_computers)): + board_cfg = import_yaml(Path(__file__).parent / "Configs/LD_Wagon_cfg.yaml") + + run(board_cfg, curpath, board_cfg) + + elif any((node in x for x in hd_wagon_computers)): + board_cfg = import_yaml(Path(__file__).parent / "Configs/HD_Wagon_cfg.yaml") + + run(board_cfg, curpath, board_cfg) + + elif any((node in x for x in ld_engine_computers)): + board_cfg = import_yaml(Path(__file__).parent / "Configs/LD_Engine_cfg.yaml") + + run(board_cfg, curpath, board_cfg) + + elif any((node in x for x in hd_engine_computers)): + board_cfg = import_yaml(Path(__file__).parent / "Configs/HD_Engine_cfg.yaml") + + run(board_cfg, curpath, board_cfg) + + else: + board_cfg = import_yaml(Path(__file__).parent / "Configs/LD_Wagon_cfg.yaml") + + run(board_cfg, curpath, board_cfg) + diff --git a/awthemes-10.4.0/LICENSE b/awthemes-10.4.0/LICENSE new file mode 100644 index 00000000..80d3caa6 --- /dev/null +++ b/awthemes-10.4.0/LICENSE @@ -0,0 +1,48 @@ + +Theme licenses: + Theme License Copyright + arc GPLv3 2015 Sergei Golovan + http://chiselapp.com/user/sgolovan/repository/ttk-themes/home + black Tcl 2007-2008 Mats Bengtsson + https://wiki.tcl-lang.org/page/black+ttk+theme + breeze GPLv3 2019 Maximilian Lika + https://github.com/MaxPerl/ttk-Breeze + breeze-dark GPLv3 2020 Bartek Jasicki + https://github.com/thindil/tkBreeze + clearlooks Tcl Regents of the UC, Scriptics, etc. + Based on a GTK theme. + http://chiselapp.com/user/sgolovan/repository/ttk-themes/home + winxpblue Tcl 2004 Pat Thoyts + +The awthemes package does not use any code or files from the above packages. +The look and feel of an included theme may not match the original precisely. + + +Copyright 2018-2021 Brad Lanam, Walnut Creek, CA, USA + +Note: This license also applies to any .svg files packaged with +awthemes that are included in the i/awthemes/ directory. + +The program used to edit the .svg files may automatically add a +license to the existing .svg files or new .svg files. This license +overrides any license specified within the i/awthemes/*/*/*.svg files. + +This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + diff --git a/awthemes-10.4.0/README.txt b/awthemes-10.4.0/README.txt new file mode 100644 index 00000000..72cf624b --- /dev/null +++ b/awthemes-10.4.0/README.txt @@ -0,0 +1,399 @@ + +The following files are needed: + awthemes.tcl - main + colorutils.tcl - color utilities + pkgIndex.tcl - package index + awarc.tcl - scalable arc theme + awblack.tcl - scalable black theme + awclearlooks.tcl - scalable clearlooks theme + awdark.tcl - awdark theme + awlight.tcl - awlight theme + awwinxpblue.tcl - scalable winxpblue theme + awbreeze.tcl - scalable breeze theme + awtemplate.tcl - example file to start a new theme + i/ - images + LICENSE - zlib/libpng LICENSE + +Demonstration scripts: + demottk.tcl, demoscaled.tcl, demoscaledb.tcl + +Try: + # application scaling + tclsh demottk.tcl awwinxpblue -fontscale 1.2 + # tk scaling only + tclsh demottk.tcl awwinxpblue -ttkscale 2.0 + # user with high dpi, smaller font + tclsh demottk.tcl awwinxpblue -ttkscale 2.0 -fontscale 0.7 + + # scaled styling + tclsh demoscaled.tcl awdark + + # multiple scaled styling, alternate colors + # Colors are shared between all styles, they do not each have + # their own set of colors. Only a few colors used in the graphics + # can be changed safely. + tclsh demoscaledb.tcl awdark + + # original no-tksvg version of awdark/awlight + tclsh demottk.tcl -notksvg awdark + + # option db testing + echo "*TkTheme: awdark" | xrdb -merge - + TCLLIBPATH=$(pwd) tclsh demottk.tcl -optionnone -optiondflt awdark + +To load other theme files, use the -autopath option to +adjust the ::auto_path variable: + + # loads the original awwinxpblue + tclsh demottk.tcl winxpblue -notksvg -autopath $HOME/mystuff + # loads the scalable awwinxpblue when -notksvg is not present + tclsh demottk.tcl awwinxpblue -autopath $HOME/mystuff + +demottk.tcl options: + -accentcolor Change the accent color (awthemes). + -autopath Set ::auto_path. + -background Set the background color using 'setBackgroundColor' + (awthemes). + -focuscolor Set the graphics and focus color using + 'setHighlightColor' (awthemes). + -fontscale Change the font scaling factor (awthemes). + -fontsize Set the initial font size. + -foreground Set the foreground color (awthemes). + -macstyles Turn on some of the new styles available in the + mac_styles branch. + -nocbt Do not load checkButtonToggle. + -noflex Do not load flexmenu. + -notable Do not load or use tablelist. + -notksvg Do not load or use tksvg. + -optiondb Use the -optiondb method for setMenuColors, + setListboxColors and setTextColors. + -optiondflt Let the *TkTheme option db setting determine the theme. + -optionnone Use the internal optiondb settings (9.6.0). + -sizegrip Replace the sizegrip with the svg version. + True for the aqua theme (requires tksvg). + -styledemo A demonstration of changing widget styles (awthemes). + Changes the progressbar and scale widget styles, turns + off the scrollbar grip and arrows. + -ttkscale Set the the [tk scaling] factor. + +10.4.0 2021-06-18 + - awdark/awlight : change to use the solid widget theme for combobox + arrows. This fixes scaling issues when the combobox font is changed. + - Added combobox.color.arrow option. + - Fix incorrect colors in arrow/solid widget theme. + - Fix incorrect combobox/solid-bg settings.tcl. + +10.3.2 2021-06-11 + - Handle ::notksvg properly for 8.7 + - Use tk version, not tcl version for 8.7 checks. + - Fix package vcompare. + +10.3.1 2021-06-10 + - Check for Tcl version 8.7 + - Update check for svg image support. + +10.3.0 2021-03-22 + - Add awbreezedark by Bartek Jasicki + - Add active.color color for use by some widget themes. + - Internal changes to support active color. + - Fixed checkbutton width issues. + - Cleaned up treeview chevron widget theme. + +10.2.1 (2021-02-11) + - Set text area -insertbackground so that the cursor has the proper color. + +10.2.0 (2021-01-02) + - Add 'getScaleFactor' procedure so that the user can scale + their images appropriately. + +10.1.2 (2020-12-20) + - Menus: add support for menu foreground (menu.fg). + - Option database initialization: Do not initialize the menu colors + on Windows. Using 'setMenuColors' on Windows leaves the top menubar + a light color, and the menu colors dark with a large border. + Use: ::ttk::theme::${theme}::setMenuColors -optiondb + to apply anyways. + - setTextColors: Set text foreground colors appropriately. + - Toolbutton: set selected color. + - Menus: add support for menu relief (menu.relief). Default to 'raised'. + Always keep the borderwidth set to 1, unscaled. + - Menus: change background color for menus to a darker color. + - Listbox: change -activestyle to none. + +10.1.1 +10.1.0 + - Development releases. Not intended for public release. + +10.0.0 (2020-12-2) + - option database is always updated. The text widget colors will + default to -entry. + - add ttk::theme:: package names so that the option db can + be used to set the theme and the old setTheme and ttk::themes + procedures may be used. + - Breaking change: + Theme name changes to prevent conflicts with the originals. + arc -> awarc, black -> awblack, breeze -> awbreeze, + clearlooks -> awclearlooks, winxpblue -> awwinxpblue. + Required due to the addition of the ttk::theme:: package names. + - Added manual page. + +9.5.1.1 (2020-11-16) + - update licensing information + +9.5.1 (2020-11-10) + - progressbar/rect-bord: fix: set trough image border. + - setMenuColors: add ability to set the option database. + - setTextColors: add ability to set the option database. + - setListboxColors: add ability to set the option database. + - setMenuColors: change selectColor to use fg.fg (for option database). + - setTextColors: add -background option. + - setTextColors: deprecate -dark option. + +9.5.0 (2020-10-29) + - Fix so that multiple scaled styles will work. + - Change so that scaled styles can have (a few of) their own colors. + - Code cleanup + +9.4.2 (2020-10-23) + - Renamed internal color names. + This may break backwards compatibility for anyone using + 'setThemeColors' or 'getColor'. + - removed 'setThemeGroupColor' function. + - Fix so that a missing or incorrect widget style will fallback + to 'none' and use the parent theme's style. + - breeze, arc: fix active vertical scale handle. + - Added $::themeutils::awversion to allow version checks. + - Fix scalable themes so that they will fail to load if tksvg is + not present. + - Improve scaling/layout of combobox/solid-bg. + - demottk.tcl: added 'package require' as a method to load the themes. + - clean demo code before production releases. + +9.4.1 (2020-10-16) + - fix mkpkgidx.sh script for clearlooks theme. + +9.4.0 (2020-10-16) + - added scalable clearlooks theme. + - scrollbar style: Fix so that a separate scrollbar slider style + can be set, but still uses the progressbar/ directory. + - arrow/solid, arrow/solid-bg, combobox/solid-bg: reduce arrow height. + - treeview heading: improve colors. + - setTextColors: set background color appropriately if the widget + is in a normal state. + - awdark/awlight: no tksvg: improved/fixed arrow colors. + - arc: improved some colors. fixed tab height. + - renamed scale/rect-bord-circle to scale/rect-bord-grip. clean up. + - progressbar/rect-bord: clean up. + - combobox/rounded: new widget style. + - progressbar/rect-diag: new widget style. + - button/roundedrect-gradient: new widget style. + - scale/rect-narrow: new scale/scale-trough widget style. + - demottk.tcl: beta: added a tablelist tab if tablelist 6.11+ is available. + - demottk.tcl: added an '-autopath' option. + +9.3.2 (2020-10-5) + - setListboxColors: Fixed to properly set colors on + removal/reinstantiation of a listbox. + - Minor code cleanup. + - setTextColors: Removed configuration of border width. + +9.3.1 (2020-9-17) + - Remove debug. + +9.3 (2020-9-17) + - Fixed inappropriate toolbutton width setting. + +9.2.4 (2020-8-14) + - remove unneeded options for scrollbar + +9.2.3 (2020-7-17) + - remove focus ring from treeview selection. + +9.2.2 (2020-6-6) + - fix: settextcolors: background color. + +9.2.1 (2020-5-20) + - fix: progressbar: rect, rect-bord border size. + +9.2 (2020-4-30) + - arc: notebook: use roundedtop-dark style. + - fix: set of background/highlight colors: remove extra adjustment. + - fix: setBackground() color adjustments. + +9.1.1 (2020-4-27) + - fix package provide statements. + +9.1 (2020-4-27) + - progressbar: rect-bord: fixed sizing. + - progressbar: rect-bord: removed border on trough. + - various fixes for all themes. + - Added 'arc' theme by Sergei Golovan + +9.0 (2020-4-23) + - added 'awtemplate.tcl' as an example to start a new theme. + - simplified and regularized all images. + - reworked color settings, made much easier to use. + - reworked all radiobuttons and checkbuttons. + - treeview: added selected arrow right and selected arrow down images. + - arrows: added solid, open triangle styles. + - progressbar: rounded-line: reduced width (breeze). + - various fixes and improvements to all themes. + - fix combobox listbox handler. + - fix combobox color mappings. + +8.1 (2020-4-20) + - rename all colors names so that they can be grouped properly + - fix: slider/trough display (padding). + - fix: incorrect combobox state coloring. + - fix background changes so that it only modifies the + properly associated background colors. + - added helper routine 'setThemeGroupColor' + - changed 'setHighlightColor' to also change the select background color. + +8.0 (2020-4-18) + - menu radiobuttons and menu checkbuttons are now dynamically generated + and any corresponding .svg files have been removed. + This also fixes menu radio/check button sizing issues for themes + other than awdark and awlight. + - treeview arrows default to inheriting from standard arrows. + - The themes have been reworked such that each widget has different + styles that can be applied. All widget styles are now found in + the i/awthemes/ directory, and individual theme directories are no + longer needed. A theme's style may be overridden by the user. + - style: slider/rect-bord: cleaned up some sizing issues + (awdark/awlight) + - style: arrow/solid-bg: cleaned up some sizing issues (awdark/awlight) + - fix: disabled progressbar display. + - fix: disabled trough display. + +7.9 (2020-4-12) + - winxpblue: fixed minor focus color issues (entry, combobox). + - fixed incorrect scrollbar background color. + - button: added state {active focus}. + - entry: added ability to set graphics. + - notebook: added hover, disabled graphics. + - combobox: graphics will be set if entry graphics are present. + - combobox: readonly graphics will be set to button graphics if + both entry and button graphics are present (breeze theme). + - menubutton: option to use button graphics for menubuttons. + - toolbutton: option to use button graphics for toolbuttons. + - 'setListBoxColors': remove borderwidth and relief settings. + - spinbox: graphics will be set if entry graphics are present. + - internal code cleanup: various theme settings have been renamed. + - added breeze theme (based on Maximilian Lika's breeze theme 0.8). + - add new helper routines to ::themeutils to set the background color + and to set the focus/highlight color. + - awdark/awlight: no tksvg: Fixed some grip/slider colors. + - fix user color overrides + +7.8 (2020-3-8) + - fix highlight background/color for text/label widgets. + +7.7 (2020-1-17) + - fix crash when tksvg not present. + - improve awdark border colors. + +7.6 (2019-12-7) + - better grip design + +7.5 (2019-12-4) + - reworked all .svg files. + - cleaned up notebook colors. + - fixed scaling issue with scaled style scaling. + - fixed combobox scaling. + - fixed scrollbar arrows. + - scaled combobox listbox scrollbar. + +7.4 (2019-12-3) + - added hasImage routine for use by checkButtonToggle + - Fix menu highlight color + +7.3 (2019-12-2) + - fix spinbox scaled styling + +7.2 (2019-12-2) + - setBackground will not do anything if the background color is unchanged. + - fixed a bug with graphical buttons. + - make setbackground more robust. + +7.1 (2019-12-1) + - fix border/padding scaling, needed for rounded buttons/tabs. + +7.0 (2019-11-30) + - clean up .svg files to use alpha channel for disabled colors. + - calculate some disabled colors. + - fix doc. + - split out theme specific code into separate files. + - Fix scaledStyle set of treeview indicator. + - make the tab topbar a generalized option. + - merge themeutils package + - clean up notebook tabs. + - winxpcblue: notebook tab graphics. + - winxpcblue: disabled images. + - black: disabled cb/rb images. + - black: add labelframe color. +6.0 (2019-11-23) + - fix !focus colors + - slider border color + - various styling fixes and improvements + - separate scrollbar color + - optional scrollbar grip + - button images are now supported + - added winxpblue scalable theme + - fixed missing awdark and awlight labelframe + +awthemes 5.1 (2019-11-20) + - add more colors to support differing spinbox and scroll arrow colors. + - awlight, awdark, black theme cleanup + - rename menubutton arrow .svg files. + - menubutton styling fixes + +awthemes 5.0 + - rewrite so that the procedures are no longer duplicated. + - rewrite set of arrow height/width and combobox arrow height. + - Add scaledStyle procedure to add a scaled style to the theme. + - Added a user configurable scaling factor. + +awthemes 4.2.1 + - fix pkgIndex.tcl to be able to load the themes + +awthemes 4.2 + - fix scaling of images. + - size menu radiobutton and checkbutton images. + - add support for flexmenu. + +awthemes 4.1 + - breaking change: renamed tab.* color names to base.tab.* + - fix bugs in setBackground and setHighlight caused by the color + renaming. + - fix where the hover color for check and radio buttons is set. + +awthemes 4.0 + - added support for other clam based themes. + - breaking change: the .svg files are now loaded from the filesystem + in order to support multiple themes. + - breaking change: All of the various colors and derived colors have + been renamed. + - awdark/awlight: Fixed empty treeview indicator. + - added scalable 'black' theme. + +awthemes 3.1 + - Added themeutils.tcl. + ::themeutils::setThemeColors awdark color-name color ... + allows the colors to be set. The graphical colors will be + changed when tksvg is in use. See themeutils.tcl for a list + of color names. + +awthemes 3.0 + - Breaking change: The package name has been renamed so + that 'package require awdark' works. + + - Support for tksvg has been added. + New graphics have been added to support tksvg, and the graphics + will scale according to the 'tk scaling' setting. + + 'tk scaling' must be set prior to the package require statement. + + - demottk.tcl has been updated to have scalable fonts. + The 'tk scaling' factor may be specified on the command line: + demottk.tcl [-scale ] diff --git a/awthemes-10.4.0/awarc.tcl b/awthemes-10.4.0/awarc.tcl new file mode 100644 index 00000000..cea0ed53 --- /dev/null +++ b/awthemes-10.4.0/awarc.tcl @@ -0,0 +1,116 @@ +#!/usr/bin/tclsh +# +# arc: +# - selection colors are all blue. +# - button focus uses blue color rather than focus ring. +# + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awarc { + + proc setBaseColors { } { + variable colors + + array set colors { + style.arrow chevron + style.button roundedrect-flat + style.checkbutton roundedrect-check-rev + style.combobox - + style.entry roundedrect + style.menubutton - + style.notebook roundedtop-dark + style.progressbar rounded-line + style.radiobutton circle-circle-rev + style.scale circle-rev + style.scrollbar-grip none + style.treeview plusminus-box + bg.bg #f5f6f7 + fg.fg #000000 + graphics.color #5294e2 + } + } + + proc setDerivedColors { } { + variable colors + + set colors(bg.light) #fcfdfd + set colors(bg.lightest) #ffffff + set colors(bg.dark) #eaebed + set colors(bg.darker) #d3d8e2 + set colors(bg.darkest) #5c616c + set colors(trough.color) #cfd6e6 + + set colors(accent.color) #ffffff + set colors(border) $colors(bg.darker) + set colors(button) $colors(bg.light) + set colors(button.active) $colors(trough.color) + set colors(button.pressed) $colors(bg.darker) + set colors(tab.disabled) $colors(bg.dark) + set colors(tab.inactive) $colors(bg.dark) + set colors(border.button.active) $colors(bg.darker) + set colors(button.anchor) {} + set colors(button.active) $colors(bg.lightest) + set colors(notebook.tab.focusthickness) 3 + set colors(notebook.tab.padding) {3 0} + set colors(button.image.padding) {6 3} + set colors(button.padding) {8 4} + set colors(checkbutton.scale) 0.95 + set colors(combobox.entry.image.padding) {6 5} + set colors(combobox.padding) {0 0} + set colors(entrybg.bg) $colors(bg.lightest) + set colors(entry.image.padding) {6 5} + set colors(entry.padding) {0 0} + set colors(arrow.color) $colors(bg.darkest) + set colors(graphics.color.light) $colors(bg.bg) + set colors(spinbox.color.bg) $colors(bg.bg) + set colors(menubutton.padding) {8 3} + set colors(menubutton.use.button.image) true + set colors(parent.theme) default + set colors(scale.trough) $colors(graphics.color) + set colors(scrollbar.color.active) #d3d4d8 + set colors(scrollbar.color) #b8babf + set colors(scrollbar.has.arrows) false + set colors(scrollbar.color.pressed) $colors(graphics.color) + set colors(scrollbar.trough) $colors(bg.bg) + set colors(select.bg) $colors(graphics.color) + set colors(spinbox.image.padding) {4 0} + set colors(spinbox.padding) {0 0} + set colors(toolbutton.image.padding) {8 7} + set colors(toolbutton.use.button.image) true + set colors(tree.arrow.selected) $colors(bg.darkest) + } + + proc init { } { + set theme awarc + set version 1.6.1 + try { + set ti [image create photo -data {} -format svg] + image delete $ti + set havetksvg true + } on error {err res} { + lassign [dict get $res -errorcode] a b c d + if { $c ne "PHOTO_FORMAT" } { + set havetksvg true + } + } + if { ([info exists ::notksvg] && $::notksvg) || ! $havetksvg } { + namespace delete ::ttk::theme::${theme} + error "no tksvg package present: cannot load scalable ${theme} theme" + } + package provide ${theme} $version + package provide ttk::theme::${theme} $version + ::ttk::awthemes::init ${theme} + } + + init +} diff --git a/awthemes-10.4.0/awblack.tcl b/awthemes-10.4.0/awblack.tcl new file mode 100644 index 00000000..b2b37de2 --- /dev/null +++ b/awthemes-10.4.0/awblack.tcl @@ -0,0 +1,98 @@ +#!/usr/bin/tclsh +# +# black: +# - Added the labelframe box. +# - Changed selection color to match the background color. +# - Scale, progressbar, scrollbar, spinbox button design are different. +# - sizegrip design is different. +# +# 7.8 +# - set menu.relief to solid. + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awblack { + + proc setBaseColors { } { + variable colors + + array set colors { + style.arrow solid-bg + style.checkbutton square-x + style.combobox solid-bg + style.radiobutton octagon-circle + style.menubutton solid + style.treeview solid + bg.bg #424242 + fg.fg #ffffff + graphics.color #424242 + is.dark true + } + } + + proc setDerivedColors { } { + variable colors + + set colors(bg.darkest) #121212 + + set colors(border) #000000 + set colors(button) $colors(bg.bg) + set colors(border.button) $colors(bg.darkest) + set colors(button.padding) {5 1} + set colors(border.checkbutton) $colors(bg.darker) + set colors(checkbutton.padding) {4 0 0 3} + set colors(checkbutton.scale) 0.7 + set colors(combobox.padding) {2 0} + set colors(entrybg.bg) #ffffff + set colors(entryfg.fg) #000000 + set colors(entry.padding) {3 0} + set colors(focus.color) #000000 + set colors(arrow.color) #000000 + set colors(scrollbar.color.grip) #000000 + set colors(scrollbar.color.arrow) #000000 + set colors(sizegrip.color) #000000 + set colors(menu.relief) solid + set colors(menubutton.padding) {5 1} + set colors(menubutton.relief) raised + set colors(menubutton.width) -8 + set colors(notebook.tab.focusthickness) 1 + set colors(notebook.tab.padding) {4 2 4 2} + set colors(border.scale) $colors(bg.darkest) + set colors(select.bg) $colors(bg.darkest) + set colors(tree.arrow.selected) $colors(fg.fg) + set colors(trough.color) $colors(bg.darkest) + } + + proc init { } { + set theme awblack + set version 7.8.1 + try { + set ti [image create photo -data {} -format svg] + image delete $ti + set havetksvg true + } on error {err res} { + lassign [dict get $res -errorcode] a b c d + if { $c ne "PHOTO_FORMAT" } { + set havetksvg true + } + } + if { ([info exists ::notksvg] && $::notksvg) || ! $havetksvg } { + namespace delete ::ttk::theme::${theme} + error "no tksvg package present: cannot load scalable ${theme} theme" + } + package provide ${theme} $version + package provide ttk::theme::${theme} $version + ::ttk::awthemes::init ${theme} + } + + init +} diff --git a/awthemes-10.4.0/awbreeze.tcl b/awthemes-10.4.0/awbreeze.tcl new file mode 100644 index 00000000..51038e17 --- /dev/null +++ b/awthemes-10.4.0/awbreeze.tcl @@ -0,0 +1,116 @@ +#!/usr/bin/tclsh +# +# breeze: +# - Notebook background is not graphical as it was in the original. +# - Disabled checkbutton/radiobutton look match the enabled look. +# - readonly combobox is not identical. +# - toolbutton and menubutton press states are set the same as +# the button press. +# - treeview arrow selected color is changed. +# - sizegrip design is different. +# + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awbreeze { + + proc setBaseColors { } { + variable colors + + # original breeze base foreground is #31363b + # I personally want as high of a contrast as is possible. + # the combobox style must be set to -, otherwise the awthemes + # default (solid-bg) is used. + + array set colors { + style.arrow chevron + style.button roundedrect-flat + style.checkbutton roundedrect-square + style.combobox - + style.entry roundedrect + style.labelframe square + style.menubutton chevron + style.notebook roundedtop-dark + style.progressbar rounded-line + style.radiobutton circle-circle + style.scale circle + style.scrollbar-grip none + style.treeview chevron + bg.bg #eff0f1 + fg.fg #000000 + graphics.color #3daee9 + } + } + + proc setDerivedColors { } { + variable colors + + # the alternate color would be defined, but we need a copy now + # for button-af + set colors(graphics.color.alternate) \ + [::colorutils::opaqueBlendPerc $colors(graphics.color) #ffffff 0.7 2] + + set colors(active.color) $colors(graphics.color.alternate) + set colors(arrow.color) $colors(bg.darkest) + set colors(border.button.active) $colors(graphics.color) + set colors(border.checkbutton) $colors(graphics.color) + set colors(button.active.focus) $colors(graphics.color.alternate) + set colors(button.anchor) {} + set colors(button.image.padding) {6 4} + set colors(button.padding) {8 3} + set colors(button.pressed) $colors(graphics.color) + set colors(checkbutton.focusthickness) 1 + set colors(checkbutton.padding) {4 3 0 3} + set colors(combobox.entry.image.padding) {6 7} + set colors(entrybg.bg) #fcfcfc + set colors(entrybg.checkbutton) $colors(bg.bg) + set colors(entry.image.padding) {5 8} + set colors(entry.padding) {2 0} + set colors(menubutton.padding) {10 2} + set colors(menubutton.use.button.image) true + set colors(notebook.tab.focusthickness) 4 + set colors(parent.theme) default + set colors(scale.trough) $colors(graphics.color) + set colors(scrollbar.has.arrows) false + set colors(select.bg) $colors(graphics.color) + set colors(spinbox.color.arrow) $colors(bg.darkest) + set colors(spinbox.image.padding) {4 4} + set colors(toolbutton.image.padding) {10 7} + set colors(toolbutton.use.button.image) true + set colors(tree.arrow.selected) #ffffff + set colors(trough.color) $colors(bg.darker) + } + + proc init { } { + set theme awbreeze + set version 1.9.1 + try { + set ti [image create photo -data {} -format svg] + image delete $ti + set havetksvg true + } on error {err res} { + lassign [dict get $res -errorcode] a b c d + if { $c ne "PHOTO_FORMAT" } { + set havetksvg true + } + } + if { ([info exists ::notksvg] && $::notksvg) || ! $havetksvg } { + namespace delete ::ttk::theme::${theme} + error "no tksvg package present: cannot load scalable ${theme} theme" + } + package provide ${theme} $version + package provide ttk::theme::${theme} $version + ::ttk::awthemes::init ${theme} + } + + init +} diff --git a/awthemes-10.4.0/awbreezedark.tcl b/awthemes-10.4.0/awbreezedark.tcl new file mode 100644 index 00000000..0b39dc7c --- /dev/null +++ b/awthemes-10.4.0/awbreezedark.tcl @@ -0,0 +1,118 @@ +#!/usr/bin/tclsh +# +# breeze-dark: +# - Notebook background is not graphical as it was in the original. +# - Improved tab hover color. +# - Add button press color, remove button focus color (not focus ring). +# - toolbutton and menubutton press states are set the same as +# the button press. +# - sizegrip design is different. +# - entry and button backgrounds are lighter. +# - cleaned up some background color issues. +# + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awbreezedark { + + proc setBaseColors { } { + variable colors + + # original breeze base foreground is #31363b + # I personally want as high of a contrast as is possible. + # the combobox style must be set to -, otherwise the awthemes + # default (solid-bg) is used. + + array set colors { + style.arrow chevron + style.button roundedrect-flat + style.checkbutton roundedrect-square + style.combobox - + style.entry roundedrect + style.labelframe square + style.menubutton chevron + style.notebook roundedtop-dark + style.progressbar rounded-line + style.radiobutton circle-circle + style.scale circle + style.scrollbar-grip none + style.treeview chevron + bg.bg #2f3336 + fg.fg #ffffff + graphics.color #3984ac + is.dark true + } + } + + proc setDerivedColors { } { + variable colors + + # entry-active: 56707c + # entry-focus: 3986af + # cb-active: 556d7a + # border: 53575a + set colors(active.color) #56707c + set colors(arrow.color) $colors(bg.lightest) + set colors(border) #53575a + set colors(border.checkbutton) $colors(graphics.color) + set colors(button) $colors(bg.light) + set colors(button.anchor) {} + set colors(button.image.padding) {6 4} + set colors(button.padding) {8 4} + set colors(button.pressed) $colors(graphics.color) + set colors(checkbutton.focusthickness) 1 + set colors(checkbutton.padding) {4 3 0 3} + set colors(combobox.entry.image.padding) {6 8} + set colors(entrybg.bg) $colors(bg.light) ; # #31363b + set colors(entry.image.padding) {5 8} + set colors(entry.padding) {2 0} + set colors(focus.color) #3986af + set colors(graphics.color.light) $colors(graphics.color) + set colors(menubutton.padding) {10 2} + set colors(menubutton.use.button.image) true + set colors(notebook.tab.focusthickness) 4 + set colors(parent.theme) default + set colors(scale.trough) $colors(graphics.color) + set colors(scrollbar.has.arrows) false + set colors(select.bg) $colors(graphics.color) + set colors(spinbox.color.arrow) $colors(bg.lightest) + set colors(spinbox.image.padding) {4 4} + set colors(toolbutton.image.padding) {10 7} + set colors(toolbutton.use.button.image) true + set colors(tree.arrow.selected) #ffffff + set colors(trough.color) $colors(bg.bg) + } + + proc init { } { + set theme awbreezedark + set version 1.0.1 + try { + set ti [image create photo -data {} -format svg] + image delete $ti + set havetksvg true + } on error {err res} { + lassign [dict get $res -errorcode] a b c d + if { $c ne "PHOTO_FORMAT" } { + set havetksvg true + } + } + if { ([info exists ::notksvg] && $::notksvg) || ! $havetksvg } { + namespace delete ::ttk::theme::${theme} + error "no tksvg package present: cannot load scalable ${theme} theme" + } + package provide ${theme} $version + package provide ttk::theme::${theme} $version + ::ttk::awthemes::init ${theme} + } + + init +} diff --git a/awthemes-10.4.0/awclearlooks.tcl b/awthemes-10.4.0/awclearlooks.tcl new file mode 100644 index 00000000..6d5aab08 --- /dev/null +++ b/awthemes-10.4.0/awclearlooks.tcl @@ -0,0 +1,119 @@ +#!/usr/bin/tclsh +# +# clearlooks: +# - changed blue focus, selection and progressbar colors to a color +# matching the overall theme +# - +# +# 1.3 +# - fix toolbutton height. +# - fix select foreground color. +# + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awclearlooks { + + proc setBaseColors { } { + variable colors + + array set colors { + style.arrow solid-bg + style.button roundedrect-gradient + style.checkbutton square-check-gradient + style.combobox rounded + style.entry roundedrect + style.menubutton solid + style.notebook - + style.progressbar rect-diag + style.radiobutton circle-circle + style.scale rect-narrow + style.scrollbar rect-bord + style.scrollbar-grip none + style.treeview open + bg.bg #efebe7 + fg.fg #000000 + graphics.color #c9ac9a + } + } + + proc setDerivedColors { } { + variable colors + + set colors(bg.light) #f5f3f0 + set colors(bg.lightest) #ffffff + set colors(bg.dark) #e7ddd8 + set colors(bg.darker) #c9c1bc + set colors(bg.darkest) #9c9284 + + set colors(accent.color) #000000 + set colors(border) $colors(bg.darkest) + set colors(button) $colors(bg.dark) + set colors(button.active) $colors(bg.bg) + set colors(button.pressed) $colors(bg.darker) + set colors(tab.active) $colors(bg.darker) + set colors(tab.inactive) $colors(bg.darker) + set colors(button.anchor) {} + set colors(button.padding) {8 2} + set colors(checkbutton.scale) 0.8 + set colors(combobox.entry.image.border) {4 4} + set colors(combobox.entry.image.padding) {3 1} + set colors(entry.active) $colors(bg.darkest) + set colors(entrybg.bg) $colors(bg.lightest) + set colors(entry.image.padding) {3 1} + set colors(entry.padding) {0 1} + set colors(focus.color) #c9ac9a + set colors(arrow.color) #000000 + set colors(pbar.color) $colors(focus.color) + set colors(pbar.color.border) $colors(border) + set colors(scrollbar.color.arrow) #000000 + set colors(sizegrip.color) $colors(bg.darkest) + set colors(spinbox.color.bg) $colors(bg.dark) + set colors(menubutton.padding) {0 2} + set colors(menubutton.use.button.image) true + set colors(notebook.tab.focusthickness) 2 + set colors(notebook.tab.padding) {3 2} + set colors(parent.theme) clam + set colors(scrollbar.color.active) $colors(bg.light) + set colors(scrollbar.color) $colors(spinbox.color.bg) + set colors(scrollbar.has.arrows) true + set colors(select.bg) $colors(focus.color) + set colors(select.fg) #000000 + set colors(trough.color) #d7cbbe + set colors(toolbutton.image.padding) {4 4} + set colors(toolbutton.use.button.image) true + } + + proc init { } { + set theme awclearlooks + set version 1.3.1 + try { + set ti [image create photo -data {} -format svg] + image delete $ti + set havetksvg true + } on error {err res} { + lassign [dict get $res -errorcode] a b c d + if { $c ne "PHOTO_FORMAT" } { + set havetksvg true + } + } + if { ([info exists ::notksvg] && $::notksvg) || ! $havetksvg } { + namespace delete ::ttk::theme::${theme} + error "no tksvg package present: cannot load scalable ${theme} theme" + } + package provide ${theme} $version + package provide ttk::theme::${theme} $version + ::ttk::awthemes::init ${theme} + } + + init +} diff --git a/awthemes-10.4.0/awdark.tcl b/awthemes-10.4.0/awdark.tcl new file mode 100644 index 00000000..99016ff5 --- /dev/null +++ b/awthemes-10.4.0/awdark.tcl @@ -0,0 +1,73 @@ +#!/usr/bin/tclsh +# +# +# +# 7.11 +# - set menu.relief to solid. + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awdark { + + proc setBaseColors { } { + variable colors + + array set colors { + style.arrow solid-bg + style.checkbutton roundedrect-check + style.menubutton solid + style.radiobutton circle-circle-hlbg + style.treeview solid + bg.bg #33393b + fg.fg #ffffff + graphics.color #215d9c + is.dark true + } + } + + proc setDerivedColors { } { + variable colors + + set colors(arrow.color) $colors(fg.fg) + set colors(border) #000000 + set colors(border.scale) $colors(bg.darkest) + set colors(border.tab) $colors(bg.light) + set colors(button) $colors(bg.darker) + set colors(button.active) $colors(bg.light) + set colors(button.anchor) {} + set colors(button.padding) {5 3} + set colors(entrybg.bg) $colors(bg.darkest) + set colors(entry.padding) {5 1} + set colors(menubutton.padding) {5 2} + set colors(menu.relief) solid + set colors(notebook.tab.focusthickness) 5 + set colors(scrollbar.color.grip) #000000 + set colors(select.bg) $colors(graphics.color) + set colors(spinbox.color.bg) $colors(graphics.color) + set colors(tab.active) $colors(bg.darker) + set colors(tab.disabled) $colors(bg.darker) + set colors(tab.inactive) $colors(bg.darker) + set colors(tab.selected) $colors(bg.darker) + set colors(tab.use.topbar) true + set colors(trough.color) $colors(bg.darkest) + } + + proc init { } { + set theme awdark + set version 7.12 + ::ttk::awthemes::init $theme + package provide $theme $version + package provide ttk::theme::${theme} $version + } + + init +} diff --git a/awthemes-10.4.0/awlight.tcl b/awthemes-10.4.0/awlight.tcl new file mode 100644 index 00000000..d5ef868a --- /dev/null +++ b/awthemes-10.4.0/awlight.tcl @@ -0,0 +1,66 @@ +#!/usr/bin/tclsh +# +# + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +namespace eval ::ttk::theme::awlight { + + proc setBaseColors { } { + variable colors + + array set colors { + style.arrow solid-bg + style.checkbutton roundedrect-check + style.menubutton solid + style.radiobutton circle-circle-hlbg + style.treeview solid + bg.bg #e8e8e7 + fg.fg #000000 + graphics.color #1a497c + } + } + + proc setDerivedColors { } { + variable colors + + set colors(arrow.color) #000000 + set colors(border.tab) $colors(bg.light) + set colors(button) $colors(bg.dark) + set colors(button.active) $colors(bg.light) + set colors(button.anchor) {} + set colors(button.padding) {5 3} + set colors(entrybg.bg) $colors(bg.lightest) + set colors(entry.padding) {5 1} + set colors(notebook.tab.focusthickness) 5 + set colors(scrollbar.color.grip) #ffffff + set colors(select.bg) $colors(graphics.color) + set colors(spinbox.color.bg) $colors(bg.bg) + set colors(tab.active) $colors(bg.dark) + set colors(tab.disabled) $colors(bg.dark) + set colors(tab.inactive) $colors(bg.dark) + set colors(tab.selected) $colors(bg.dark) + set colors(tab.use.topbar) true + set colors(tree.arrow.selected) #ffffff + set colors(trough.color) $colors(bg.lightest) + } + + proc init { } { + set theme awlight + set version 7.10 + ::ttk::awthemes::init $theme + package provide $theme $version + package provide ttk::theme::${theme} $version + } + + init +} diff --git a/awthemes-10.4.0/awtemplate.tcl b/awthemes-10.4.0/awtemplate.tcl new file mode 100644 index 00000000..3e51671f --- /dev/null +++ b/awthemes-10.4.0/awtemplate.tcl @@ -0,0 +1,220 @@ +#!/usr/bin/tclsh +# +# 2020-4-23 +# Template for creating a new theme using the awthemes package. +# This code is in the public domain. +# +# Search for 'CHANGE:' within this file to locate the three changes +# that must be made to set up a new theme. +# + +# CHANGE: 'awtemplate' to the filename containing your theme. +# Within the pkgIndex.tcl file, use the following layout to load +# the new theme: +# package ifneeded awtemplate 1.0 \ +# [list source [file join $dir awtemplate.tcl]] + +set ap [file normalize [file dirname [info script]]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +set ap [file normalize [file join [file dirname [info script]] .. code]] +if { $ap ni $::auto_path } { + lappend ::auto_path $ap +} +unset ap +package require awthemes + +# CHANGE: 'awtemplate' to the name of your theme. +namespace eval ::ttk::theme::awtemplate { + + # To set a widget style, in the 'setBaseColors' procedure, add + # style.