├── __init__.py ├── BATT_HIL ├── __init__.py ├── LTC2944 │ ├── __init__.py │ ├── LTC2944_test.py │ └── LTC2944.py ├── MCP2221_Multiple │ ├── __init__.py │ └── MCP2221_Multiple.py ├── ADS1219 │ ├── images │ │ └── ads1219-breakout.jpg │ ├── .gitattributes │ ├── LICENSE │ ├── example-single-conversion.py │ ├── example-single-conversion-process.py │ ├── example-continuous-interrupt.py │ ├── ads1219.py │ └── README.md ├── PCA9685_Test.py ├── FET_BOARD │ └── FET_BOARD.py ├── PCA9539 │ └── PCA9539_test.py ├── fet_board_management.py ├── Multiple_MCP_Test.py └── Eload_DL3000.py ├── .gitignore ├── .gitattributes ├── test_scripts ├── A2D_Eload_Calibrate.py ├── test_calibration_values.py ├── test_A2D_4CH_ISO_ADC_led.py ├── test_A2D_64CH_DAQ_dig_in.py └── test_A2D_Eload.py ├── lab_equipment ├── DMM_FET_BOARD_EQ.py ├── A2D_DAQ_Config_MSXIV_Module.csv ├── DMM_Fake.py ├── A2D_DAQ_config.py ├── DMM_VIRTUAL.py ├── PSU_Fake.py ├── A2D_DAQ_Config_All_Dig_In.csv ├── Eload_Fake.py ├── A2D_DAQ_Config_Default.csv ├── DMM_A2D_SENSE_BOARD.py ├── PSU_N8700.py ├── PSU_KAXXXXP.py ├── DMM_A2D_4CH_Isolated_ADC.py ├── PSU_E3631A.py ├── PSU_BK9100.py ├── PSU_DP800.py ├── PSU_MP71025X.py ├── OTHER_A2D_Relay_Board.py ├── DMM_SDM3065X.py ├── DMM_DM3000.py ├── Eload_KEL10X.py ├── SMU_A2D_POWER_BOARD.py ├── PSU_SPD1000.py ├── Eload_DL3000.py ├── Eload_BK8600.py ├── Eload_PARALLEL.py ├── A2D_DAQ_Config_All_Thermistor_NXRT15XV.csv ├── Eload_IT8500.py ├── A2D_DAQ_control.py ├── Eload_A2D_Eload.py ├── OTHER_Arduino_IO_Module.py └── PyVisaDeviceTemplate.py ├── PSU_Test_Script.py ├── live_graph_temp.py ├── requirements.txt ├── TempChannels.py ├── add_temp_to_logs.py ├── Measurement_Script.py ├── voltage_to_temp.py ├── DC_DC_Graph.py ├── Eload_cv_sweep_graph.py ├── FileIO.py ├── A2D_DAQ_read_all_analog_pullup.py ├── Eload_cv_sweep.py ├── Calibration_Gui.py ├── PlotTemps.py ├── MSXIV_Battery_Module_Wiring_Test.py ├── jsonIO.py ├── dc_dc_test.py ├── Calibration_Script.py └── Templates.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BATT_HIL/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /BATT_HIL/LTC2944/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | venv/ -------------------------------------------------------------------------------- /BATT_HIL/MCP2221_Multiple/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/images/ads1219-breakout.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mbA2D/Test_Equipment_Control/HEAD/BATT_HIL/ADS1219/images/ads1219-breakout.jpg -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/.gitattributes: -------------------------------------------------------------------------------- 1 | # Per default everything gets normalized and gets LF line endings on checkout. 2 | * text eol=lf 3 | 4 | # These should never be modified by git. 5 | *.png binary 6 | *.jpg binary 7 | *.dxf binary 8 | *.mpy binary -------------------------------------------------------------------------------- /BATT_HIL/PCA9685_Test.py: -------------------------------------------------------------------------------- 1 | import board 2 | from adafruit_pca9685 import PCA9685 3 | 4 | i2c = board.I2C() 5 | 6 | print("Scan I2C {}".format([hex(x) for x in i2c.scan()])) 7 | 8 | pca = PCA9685(i2c, address=0x60) 9 | pca.frequency = 250 10 | pca.channels[0].duty_cycle = 0xFFFF 11 | -------------------------------------------------------------------------------- /test_scripts/A2D_Eload_Calibrate.py: -------------------------------------------------------------------------------- 1 | #run this test from the test_equipment_control directory using the command: 2 | #python -m test_scripts.test_A2D_Eload 3 | import equipment as eq 4 | import time 5 | 6 | eload = eq.eLoads.choose_eload()[1] 7 | 8 | print(eload.get_cal_i()) 9 | 10 | eload.calibrate_current(0.5037, 0.1245, 5.0618, 1.25) 11 | eload.cal_i_save() 12 | 13 | print(eload.get_cal_i()) 14 | -------------------------------------------------------------------------------- /BATT_HIL/FET_BOARD/FET_BOARD.py: -------------------------------------------------------------------------------- 1 | #class that controls and measures a battery during testing. 2 | 3 | 4 | #I2C Addresses for control section 5 | CTRL_IO_EXP_I2C_ADDR = 0x77 6 | PWM_DRIVER_I2C_ADDR = 0x60 7 | I2C_EXP_I2C_ADDR = 0x70 8 | CTRL_ADC_I2C_ADDR = 0x48 9 | 10 | #I2C Addresses for each channel 11 | CH_CC_I2C_ADDR = 0x64 12 | CH_ADC_I2C_ADDR = 0x48 13 | CH_IO_EXP_I2C_ADDR = 0x74 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lab_equipment/DMM_FET_BOARD_EQ.py: -------------------------------------------------------------------------------- 1 | #This class interacts with the actual instrument with queues and events. 2 | #The instrument object is stored in another process. 3 | 4 | from .DMM_VIRTUAL import VirtualDMM 5 | 6 | class FET_BOARD_EQ(VirtualDMM): 7 | def __init__(self, resource_id, event_and_queue_dict = None): 8 | super().__init__(event_and_queue_dict) 9 | self.board_name = resource_id['board_name'] 10 | self.ch_num = resource_id['ch_num'] 11 | -------------------------------------------------------------------------------- /test_scripts/test_calibration_values.py: -------------------------------------------------------------------------------- 1 | target_offset = 0.1 2 | target_gain = 12.75 3 | 4 | dut_val_2 = -0.3 5 | dmm_val_2 = target_offset + dut_val_2 / target_gain 6 | dut_val_1 = -0.2 7 | dmm_val_1 = (dut_val_1 - dut_val_2) / target_gain + dmm_val_2 8 | 9 | _v_scaling = (dut_val_2 - dut_val_1) / (dmm_val_2 - dmm_val_1) #rise in actual / run in measured 10 | _v_offset = dmm_val_2 - (1/_v_scaling) * dut_val_2 11 | 12 | print(_v_offset) 13 | print(_v_scaling) 14 | -------------------------------------------------------------------------------- /PSU_Test_Script.py: -------------------------------------------------------------------------------- 1 | #Quick and dirty python script to read a voltage a bunch of times 2 | 3 | import equipment as eq 4 | import time 5 | import easygui as eg 6 | 7 | def main(): 8 | psu = eq.powerSupplies.choose_psu()[1] 9 | psu.set_current(1) 10 | psu.set_voltage(3.3) 11 | print(psu.get_output()) 12 | psu.toggle_output(True) 13 | print(psu.get_output()) 14 | psu.toggle_output(False) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | -------------------------------------------------------------------------------- /lab_equipment/A2D_DAQ_Config_MSXIV_Module.csv: -------------------------------------------------------------------------------- 1 | Channel,Input_Type,Voltage_Scaling,Temp_A,Temp_B,Temp_C 2 | 16,voltage,2,0,0,0 3 | 17,voltage,3,0,0,0 4 | 18,voltage,2,0,0,0 5 | 19,voltage,3,0,0,0 6 | 20,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 7 | 21,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 8 | 22,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 9 | 23,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 10 | 24,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 11 | -------------------------------------------------------------------------------- /live_graph_temp.py: -------------------------------------------------------------------------------- 1 | #python script to create a live updating graph from a file 2 | #intended for use with the A2D_DAQ 64 channel data acquisition unit 3 | #but can easily be modified for other uses 4 | 5 | import matplotlib.pyplot as plt 6 | import matplotlib.animation as animation 7 | import time 8 | 9 | def mv_to_temp(mv): 10 | return 25 11 | 12 | def start_graph(file_dir): 13 | #reference for live updating graphs: 14 | #https://pythonprogramming.net/python-matplotlib-live-updating-graphs/ 15 | 16 | 17 | 18 | 19 | #TODO: 20 | #if __name__ == "__main__": 21 | #ask for a file, create a graph, save it and show it 22 | -------------------------------------------------------------------------------- /test_scripts/test_A2D_4CH_ISO_ADC_led.py: -------------------------------------------------------------------------------- 1 | #run this test from the test_equipment_control directory using the command: 2 | #python -m test_scripts.test_A2D_4CH_ISO_ADC_led 3 | import equipment as eq 4 | import time 5 | 6 | num_readings = 100 7 | 8 | dmm = eq.dmms.choose_dmm()[1] 9 | 10 | start_time = time.time() 11 | time_list = list() 12 | for i in range(num_readings): 13 | s_time = time.time() 14 | dig_val = dmm.get_rs485_addr() 15 | time_list.append(time.time() - s_time) 16 | end_time = time.time() 17 | 18 | print(f"total_time: {end_time - start_time}s") 19 | print(f"avg_time_single: {sum(time_list) / len(time_list)}s") 20 | -------------------------------------------------------------------------------- /lab_equipment/DMM_Fake.py: -------------------------------------------------------------------------------- 1 | #python implementation for a fake power supply to test programs - has all functions and returns some values 2 | 3 | #DMM 4 | class Fake_DMM: 5 | 6 | def __init__(self, resource_id = None, resources_list = None): 7 | self.inst_idn = 'Fake_DMM' 8 | 9 | def measure_voltage(self, nplc = None, volt_range = None): 10 | return 4.15 11 | 12 | def set_mode(self, mode = "DCV"): 13 | pass 14 | 15 | def set_auto_zero_dcv(self, state): 16 | pass 17 | 18 | def set_range_dcv(self, volt_range = None): 19 | pass 20 | 21 | def set_nplc(self, nplc = None): 22 | pass 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Adafruit-Blinka==6.13.1 2 | adafruit-circuitpython-ads1x15==2.2.8 3 | adafruit-circuitpython-busdevice==5.1.0 4 | adafruit-circuitpython-pca9685==3.3.8 5 | adafruit-circuitpython-register==1.9.5 6 | adafruit-circuitpython-tca9548a==0.5.0 7 | Adafruit-GPIO==1.0.3 8 | Adafruit-PlatformDetect==3.15.3 9 | Adafruit-PureIO==1.1.9 10 | DiffCapAnalyzer==0.1.1 11 | easygui==0.98.2 12 | gpib-ctypes==0.3.0 13 | hidapi==0.10.1 14 | matplotlib==3.4.3 15 | numpy==1.21.2 16 | pandas==1.3.4 17 | pyparsing==2.4.7 18 | PyQt6==6.2.1 19 | PyQt6-Qt6==6.2.1 20 | PyQt6-sip==13.1.0 21 | pyqtgraph==0.12.3 22 | pyserial==3.5 23 | python-dateutil==2.8.2 24 | pyusb==1.1.0 25 | PyVISA==1.14.1 26 | PyVISA-py==0.5.2 27 | scipy==1.7.2 28 | tk==0.1.0 29 | keyboard==0.13.5 30 | retry==0.9.2 31 | -------------------------------------------------------------------------------- /test_scripts/test_A2D_64CH_DAQ_dig_in.py: -------------------------------------------------------------------------------- 1 | #run this test from the test_equipment_control directory using the command: 2 | #python -m test_scripts.test_A2D_64CH_DAQ_dig_in 3 | import equipment as eq 4 | import time 5 | 6 | dmm = eq.dmms.choose_dmm()[1] 7 | 8 | num_readings = 100 9 | num_channels = 64 10 | 11 | start_time = time.time() 12 | time_list = list() 13 | for i in range(num_readings): 14 | s_time = time.time() 15 | dig_val = dmm.get_dig_in(channel = 10) 16 | time_list.append(time.time() - s_time) 17 | end_time = time.time() 18 | 19 | start_channel_time = time.time() 20 | for i in range(num_channels): 21 | dig_val = dmm.get_dig_in(channel = i) 22 | end_channel_time = time.time() 23 | 24 | print(f"total_time: {end_time - start_time}s") 25 | print(f"avg_time_single: {sum(time_list) / len(time_list)}s") 26 | print(f"read_all_channels: {end_channel_time - start_channel_time}s") 27 | -------------------------------------------------------------------------------- /test_scripts/test_A2D_Eload.py: -------------------------------------------------------------------------------- 1 | #run this test from the test_equipment_control directory using the command: 2 | #python -m test_scripts.test_A2D_Eload 3 | import equipment as eq 4 | import time 5 | 6 | eload = eq.eLoads.choose_eload()[1] 7 | 8 | #set output current to a low value and print v,i,t. 9 | eload.toggle_output(True) 10 | eload.set_current(10) 11 | 12 | try: 13 | start_time = time.time() 14 | while time.time() - start_time < (10*60): 15 | temp_c = eload.measure_temperature() 16 | volt_v = eload.measure_voltage_supply() 17 | curr_a = eload.measure_current() 18 | ctrl_a = eload.measure_current_control() 19 | 20 | print(f"temp: {temp_c} volt: {volt_v} curr: {curr_a} ctrl: {ctrl_a}") 21 | time.sleep(1) 22 | eload.toggle_output(False) 23 | print("Test Complete") 24 | except KeyboardInterrupt: 25 | eload.toggle_output(False) 26 | print("Closing") 27 | exit() 28 | -------------------------------------------------------------------------------- /lab_equipment/A2D_DAQ_config.py: -------------------------------------------------------------------------------- 1 | #Configuration settings for the A2D DAQ 2 | #This exposes a menu to setup the channels for I/O type 3 | 4 | #Input, output, drive high, drive low 5 | 6 | import pandas as pd 7 | import easygui as eg 8 | from os import path 9 | 10 | #CSV File Format - needs these column headers 11 | #Channel, Input_Type, Voltage_Scaling, Temp_A, Temp_B, Temp_C 12 | 13 | def get_config_dict(default = False): 14 | if default: 15 | filename = 'A2D_DAQ_Config_Default.csv' 16 | filepath = path.join(path.dirname(__file__), filename) 17 | else: 18 | #Choose CSV to load from 19 | filepath = eg.fileopenbox(title = "Choose the CSV to load config for A2D_64CH_DAQ from", filetypes = [['*.csv', 'CSV Files']]) 20 | 21 | #Load dict from CSV 22 | df = pd.read_csv(filepath) 23 | df = df.astype({'Channel': 'int32'}) 24 | df.set_index('Channel', inplace = True) 25 | 26 | return df.to_dict('index') 27 | -------------------------------------------------------------------------------- /BATT_HIL/LTC2944/LTC2944_test.py: -------------------------------------------------------------------------------- 1 | #test to read voltage, current, temperature, and charge 2 | 3 | import time 4 | import board 5 | import LTC2944 6 | 7 | import adafruit_tca9548a 8 | 9 | i2c = board.I2C() 10 | 11 | print("Scan I2C {}".format([hex(x) for x in i2c.scan()])) 12 | 13 | tca = adafruit_tca9548a.TCA9548A(i2c) 14 | 15 | print("Scan I2C TCA {}".format([hex(x) for x in tca[2].scan()])) 16 | 17 | #gauge = LTC2944.LTC2944(i2c) 18 | gauge = LTC2944.LTC2944(tca[2]) 19 | gauge.RSHUNT = 0.002 #Set 2mOhm Shunt Resistance 20 | gauge.adc_mode = LTC2944.LTC2944_ADCMode.AUTO #Continuous conversions. 21 | gauge.prescaler = LTC2944.LTC2944_Prescaler.PRESCALER_1 22 | 23 | while True: 24 | print( 25 | "Current: %.4f A Voltage: %.4f V Temp: %.2f C Charge: %X" 26 | % (gauge.get_current_a(), gauge.get_voltage_v(), \ 27 | gauge.get_temp_c(), gauge.charge) 28 | ) 29 | time.sleep(0.1) #should be enough time to get a new reading every time 30 | -------------------------------------------------------------------------------- /lab_equipment/DMM_VIRTUAL.py: -------------------------------------------------------------------------------- 1 | #Class for a single instrument with a single communication interface that could be connected to multiple channels 2 | #Since a single communication interface cannot be recreated in each process then we need to keep the 3 | #equipment object at a higher level and communicate with it via thread-safe events and queues. 4 | 5 | class VirtualDMM: 6 | def __init__(self, event_and_queue_dict = None): 7 | #need to know which queues and events to connect to request measurements and receive data 8 | self.event_and_queue_dict = event_and_queue_dict 9 | 10 | def measure_voltage(self): 11 | self.event_and_queue_dict['v_event'].set() 12 | return float(self.event_and_queue_dict['v_queue'].get(timeout = 10)) 13 | 14 | def measure_current(self): 15 | self.event_and_queue_dict['i_event'].set() 16 | return float(self.event_and_queue_dict['i_queue'].get(timeout = 10)) 17 | 18 | def measure_temperature(self): 19 | self.event_and_queue_dict['t_event'].set() 20 | return float(self.event_and_queue_dict['t_queue'].get(timeout = 10)) 21 | 22 | -------------------------------------------------------------------------------- /TempChannels.py: -------------------------------------------------------------------------------- 1 | #which channels are connected to what cells 2 | 3 | class TempChannels: 4 | def __init__(self): 5 | #organized into Cell_Name and Location 6 | self.channels = { 7 | 'EMS_SC1' : { 8 | 'pos' : [56,57,58,59,60,61,62], 9 | 'neg' : [48,49,50,51,52,53,54], 10 | 'side' : [55] 11 | }, 12 | 'EMS_NI1' : { 13 | 'pos' : [0,1,2,3,4,5,6], 14 | 'neg' : [7,8,9,11,13,14], 15 | 'side' : [12] 16 | }, 17 | 'EMS_SC_EPOXY' : { 18 | 'pos' : [40,41,42,43,44,45,46], 19 | 'neg' : [32,33,34,35,36,37,38], 20 | 'side' : [39] 21 | }, 22 | 'EMS_NI_EPOXY' : { 23 | 'pos' : [16,17,18,19,20,21,22], 24 | 'neg' : [24,25,26,27,28,29,30], 25 | 'side' : [23] 26 | } 27 | } 28 | 29 | #returns the location of the given channel on a cell 30 | def find_location(self, cell_name, channel_num): 31 | for location in list(self.channels[cell_name].keys()): 32 | if int(channel_num) in self.channels[cell_name][location]: 33 | return location + str(self.channels[cell_name][location].index(int(channel_num))) 34 | return 'Location Not Found' 35 | -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Teachman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /add_temp_to_logs.py: -------------------------------------------------------------------------------- 1 | #adds temperature to python logs that do not have it from early testing 2 | #this is fairly inefficient but it does work. 3 | 4 | import pandas as pd 5 | import voltage_to_temp as V2T 6 | import easygui as eg 7 | import os 8 | 9 | pull_up_v = 3.3 10 | pull_up_r = 3300.0 11 | 12 | filepath = eg.fileopenbox(title = "Select the log to add temperature to", filetypes = [['*.csv', 'CSV Files']]) 13 | filedir = os.path.dirname(filepath) 14 | filename = os.path.split(filepath)[-1] 15 | 16 | #add temp to the filename 17 | filename = 'temp_' + filename 18 | new_filepath = os.path.join(filedir, filename) 19 | 20 | df = pd.read_csv(filepath) 21 | 22 | for ch in range(64): 23 | df['Channel_C_{}'.format(ch)] = 9999 24 | df['Channel_C_{}'.format(ch)] = df['Channel_C_{}'.format(ch)].astype('float64') 25 | new_col = df['Channel_C_{}'.format(ch)] 26 | col = df['Channel_{}'.format(ch)] 27 | #print(col.head()) 28 | for meas in range(len(df.index)): 29 | new_col[meas] = V2T.voltage_to_C(col[meas]/1000.0, pull_up_r, pull_up_v) 30 | df['Channel_C_{}'.format(ch)] = new_col 31 | print('Done Ch {}\n'.format(ch)) 32 | 33 | df.to_csv(new_filepath, index=False) 34 | -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/example-single-conversion.py: -------------------------------------------------------------------------------- 1 | #modified to use board instead of Pin and I2C. 2 | 3 | import board 4 | import ads1219 5 | import adafruit_tca9548a 6 | import time 7 | 8 | # This example demonstrates how to use the ADS1219 using single-shot conversion mode 9 | # The ADC1219 will initiate a conversion when adc.read_data() is called 10 | 11 | 12 | i2c = board.I2C() 13 | 14 | #time.sleep(0.25) 15 | 16 | print("Scan I2C {}".format([hex(x) for x in i2c.scan()])) 17 | 18 | tca = adafruit_tca9548a.TCA9548A(i2c) 19 | 20 | print("Scan I2C {}".format([hex(x) for x in tca[0].scan()])) 21 | 22 | adc = ads1219.ADS1219(tca[0], 0x40) 23 | #adc = ads1219.ADS1219(i2c, 0x40) 24 | 25 | 26 | adc.set_channel(ads1219.ADS1219_MUX.AIN2) 27 | adc.set_conversion_mode(ads1219.ADS1219_CONV_MODE.CM_SINGLE) 28 | adc.set_gain(ads1219.ADS1219_GAIN.GAIN_1) 29 | adc.set_data_rate(ads1219.ADS1219_DATA_RATE.DR_20_SPS) # 20 SPS is the most accurate 30 | adc.set_vref(ads1219.ADS1219_VREF.VREF_EXTERNAL) 31 | 32 | print("CONFIG: {}".format(adc.read_config())) 33 | 34 | while True: 35 | result = adc.read_data() 36 | print('result = {}, V = {:.5f}'.format(result, 37 | result * 2.5 * ((18.7+2) / 2) / ads1219.ADS1219.POSITIVE_CODE_RANGE)) 38 | time.sleep(0.06) -------------------------------------------------------------------------------- /lab_equipment/PSU_Fake.py: -------------------------------------------------------------------------------- 1 | #python implementation for a fake power supply to test programs - has all functions and returns some values 2 | 3 | # Power Supply 4 | class Fake_PSU: 5 | has_remote_sense = False 6 | can_measure_v_while_off = True 7 | 8 | def __init__(self, resource_id = None, resources_list = None): 9 | self.current_a = 0 10 | self.voltage_v = 4.1 11 | 12 | self.inst_idn = "Fake PSU" 13 | 14 | # To set power supply limit in Amps 15 | def set_current(self, current_setpoint_A): 16 | pass 17 | 18 | def set_voltage(self, voltage_setpoint_V): 19 | #print("Setting voltage to: {}".format(voltage_setpoint_V)) 20 | if voltage_setpoint_V != 0: 21 | self.voltage_v = voltage_setpoint_V 22 | 23 | def toggle_output(self, state, ch = 1): 24 | pass 25 | 26 | def remote_sense(self, state): 27 | pass 28 | 29 | def lock_commands(self, state): 30 | pass 31 | 32 | def measure_voltage(self): 33 | return self.voltage_v 34 | 35 | def measure_current(self): 36 | return self.current_a 37 | 38 | def measure_power(self): 39 | current = self.measure_current() 40 | voltage = self.measure_voltage() 41 | return float(current*voltage) 42 | -------------------------------------------------------------------------------- /BATT_HIL/PCA9539/PCA9539_test.py: -------------------------------------------------------------------------------- 1 | #Test Program for the PCA9539 IO Expander 2 | 3 | import board 4 | from community_pca9539 import PCA9539 5 | import time 6 | 7 | 8 | i2c = board.I2C() 9 | expander = PCA9539(i2c) 10 | 11 | # Read the configuration of all 16 pins 12 | # 0 = output, 1 = input 13 | in_or_out = expander.configuration_ports 14 | print("Configuration\n{:016b}".format(in_or_out)) 15 | 16 | # Read the polarity inversion state of all 16 pins 17 | polarity_inversion = expander.polarity_inversions 18 | print("Polarity inversion\n{:016b}".format(polarity_inversion)) 19 | 20 | # Read the input state of all 16 pins 21 | input_state = expander.input_ports 22 | print("Input state\n{:016b}".format(input_state)) 23 | 24 | # Read the output state of all 16 pins 25 | # At power up, this defaults to 1111111111111111 26 | output_state = expander.output_ports 27 | print("Output state\n{:016b}".format(output_state)) 28 | 29 | 30 | #configure pin as output 31 | configuration_port_0_pin_2 = False #Output 32 | 33 | while True: 34 | #blink an output and print the state 35 | 36 | expander.output_port_0_pin_2 = True 37 | print("Set 1") 38 | time.sleep(0.5) 39 | value = expander.input_port_0_pin_2 40 | print("Read {}".format(value)) 41 | 42 | expander.output_port_0_pin_2 = False 43 | print("Set 0") 44 | time.sleep(0.5) 45 | value = expander.input_port_0_pin_2 46 | print("Read {}".format(value)) 47 | 48 | print("\n") 49 | -------------------------------------------------------------------------------- /lab_equipment/A2D_DAQ_Config_All_Dig_In.csv: -------------------------------------------------------------------------------- 1 | Channel,Input_Type,Voltage_Scaling,Temp_A,Temp_B,Temp_C 2 | 0,dig_in,1,0,0,0 3 | 1,dig_in,1,0,0,0 4 | 2,dig_in,1,0,0,0 5 | 3,dig_in,1,0,0,0 6 | 4,dig_in,1,0,0,0 7 | 5,dig_in,1,0,0,0 8 | 6,dig_in,1,0,0,0 9 | 7,dig_in,1,0,0,0 10 | 8,dig_in,1,0,0,0 11 | 9,dig_in,1,0,0,0 12 | 10,dig_in,1,0,0,0 13 | 11,dig_in,1,0,0,0 14 | 12,dig_in,1,0,0,0 15 | 13,dig_in,1,0,0,0 16 | 14,dig_in,1,0,0,0 17 | 15,dig_in,1,0,0,0 18 | 16,dig_in,1,0,0,0 19 | 17,dig_in,1,0,0,0 20 | 18,dig_in,1,0,0,0 21 | 19,dig_in,1,0,0,0 22 | 20,dig_in,1,0,0,0 23 | 21,dig_in,1,0,0,0 24 | 22,dig_in,1,0,0,0 25 | 23,dig_in,1,0,0,0 26 | 24,dig_in,1,0,0,0 27 | 25,dig_in,1,0,0,0 28 | 26,dig_in,1,0,0,0 29 | 27,dig_in,1,0,0,0 30 | 28,dig_in,1,0,0,0 31 | 29,dig_in,1,0,0,0 32 | 30,dig_in,1,0,0,0 33 | 31,dig_in,1,0,0,0 34 | 32,dig_in,1,0,0,0 35 | 33,dig_in,1,0,0,0 36 | 34,dig_in,1,0,0,0 37 | 35,dig_in,1,0,0,0 38 | 36,dig_in,1,0,0,0 39 | 37,dig_in,1,0,0,0 40 | 38,dig_in,1,0,0,0 41 | 39,dig_in,1,0,0,0 42 | 40,dig_in,1,0,0,0 43 | 41,dig_in,1,0,0,0 44 | 42,dig_in,1,0,0,0 45 | 43,dig_in,1,0,0,0 46 | 44,dig_in,1,0,0,0 47 | 45,dig_in,1,0,0,0 48 | 46,dig_in,1,0,0,0 49 | 47,dig_in,1,0,0,0 50 | 48,dig_in,1,0,0,0 51 | 49,dig_in,1,0,0,0 52 | 50,dig_in,1,0,0,0 53 | 51,dig_in,1,0,0,0 54 | 52,dig_in,1,0,0,0 55 | 53,dig_in,1,0,0,0 56 | 54,dig_in,1,0,0,0 57 | 55,dig_in,1,0,0,0 58 | 56,dig_in,1,0,0,0 59 | 57,dig_in,1,0,0,0 60 | 58,dig_in,1,0,0,0 61 | 59,dig_in,1,0,0,0 62 | 60,dig_in,1,0,0,0 63 | 61,dig_in,1,0,0,0 64 | 62,dig_in,1,0,0,0 65 | 63,dig_in,1,0,0,0 66 | -------------------------------------------------------------------------------- /lab_equipment/Eload_Fake.py: -------------------------------------------------------------------------------- 1 | #python implementation for a fake power supply to test programs - has all functions and returns some values 2 | 3 | # E-Load 4 | class Fake_Eload: 5 | 6 | has_remote_sense = False 7 | 8 | def __init__(self, resource_id = None, resources_list = None): 9 | self.max_power = 10000 10 | self.max_current = 1000 11 | self.mode = "CURR" 12 | self.current_a = 0 13 | self.voltage_v = 4 14 | 15 | self.inst_idn = "Fake Eload" 16 | 17 | # To Set E-Load in Amps 18 | def set_current(self, current_setpoint_A): 19 | self.current_a = current_setpoint_A 20 | if self.mode != "CURR": 21 | print("ERROR - E-load not in correct mode") 22 | 23 | def set_mode_current(self): 24 | self.current_a = 0 25 | self.mode = "CURR" 26 | 27 | def set_mode_voltage(self): 28 | self.voltage_v = 0 29 | self.mode = "VOLT" 30 | 31 | def set_cv_voltage(self, voltage_setpoint_V): 32 | self.voltage_v = voltage_setpoint_V 33 | if self.mode != "VOLT": 34 | print("ERROR - E-load not in correct mode") 35 | 36 | def toggle_output(self, state): 37 | pass 38 | 39 | def remote_sense(self, state): 40 | pass 41 | 42 | def lock_front_panel(self, state): 43 | pass 44 | 45 | def measure_voltage(self): 46 | return self.voltage_v 47 | 48 | def measure_current(self): 49 | return self.current_a 50 | -------------------------------------------------------------------------------- /lab_equipment/A2D_DAQ_Config_Default.csv: -------------------------------------------------------------------------------- 1 | Channel,Input_Type,Voltage_Scaling,Temp_A,Temp_B,Temp_C 2 | 0,voltage,1,0,0,0 3 | 1,voltage,1,0,0,0 4 | 2,voltage,1,0,0,0 5 | 3,voltage,1,0,0,0 6 | 4,voltage,1,0,0,0 7 | 5,voltage,1,0,0,0 8 | 6,voltage,1,0,0,0 9 | 7,voltage,1,0,0,0 10 | 8,voltage,1,0,0,0 11 | 9,voltage,1,0,0,0 12 | 10,voltage,1,0,0,0 13 | 11,voltage,1,0,0,0 14 | 12,voltage,1,0,0,0 15 | 13,voltage,1,0,0,0 16 | 14,voltage,1,0,0,0 17 | 15,voltage,1,0,0,0 18 | 16,voltage,1,0,0,0 19 | 17,voltage,1,0,0,0 20 | 18,voltage,1,0,0,0 21 | 19,voltage,1,0,0,0 22 | 20,voltage,1,0,0,0 23 | 21,voltage,1,0,0,0 24 | 22,voltage,1,0,0,0 25 | 23,voltage,1,0,0,0 26 | 24,voltage,1,0,0,0 27 | 25,voltage,1,0,0,0 28 | 26,voltage,1,0,0,0 29 | 27,voltage,1,0,0,0 30 | 28,voltage,1,0,0,0 31 | 29,voltage,1,0,0,0 32 | 30,voltage,1,0,0,0 33 | 31,voltage,1,0,0,0 34 | 32,voltage,1,0,0,0 35 | 33,voltage,1,0,0,0 36 | 34,voltage,1,0,0,0 37 | 35,voltage,1,0,0,0 38 | 36,voltage,1,0,0,0 39 | 37,voltage,1,0,0,0 40 | 38,voltage,1,0,0,0 41 | 39,voltage,1,0,0,0 42 | 40,voltage,1,0,0,0 43 | 41,voltage,1,0,0,0 44 | 42,voltage,1,0,0,0 45 | 43,voltage,1,0,0,0 46 | 44,voltage,1,0,0,0 47 | 45,voltage,1,0,0,0 48 | 46,voltage,1,0,0,0 49 | 47,voltage,1,0,0,0 50 | 48,voltage,1,0,0,0 51 | 49,voltage,1,0,0,0 52 | 50,voltage,1,0,0,0 53 | 51,voltage,1,0,0,0 54 | 52,voltage,1,0,0,0 55 | 53,voltage,1,0,0,0 56 | 54,voltage,1,0,0,0 57 | 55,voltage,1,0,0,0 58 | 56,voltage,1,0,0,0 59 | 57,voltage,1,0,0,0 60 | 58,voltage,1,0,0,0 61 | 59,voltage,1,0,0,0 62 | 60,voltage,1,0,0,0 63 | 61,voltage,1,0,0,0 64 | 62,voltage,1,0,0,0 65 | 63,voltage,1,0,0,0 66 | -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/example-single-conversion-process.py: -------------------------------------------------------------------------------- 1 | #modified to use board instead of Pin and I2C. 2 | 3 | import board 4 | import ads1219 5 | import adafruit_tca9548a 6 | import time 7 | from multiprocessing import Process, Event 8 | 9 | # This example demonstrates how to use the ADS1219 using single-shot conversion mode 10 | # The ADC1219 will initiate a conversion when adc.read_data() is called 11 | 12 | def read_adc_values(event = None): 13 | i2c = board.I2C() 14 | 15 | #time.sleep(0.25) 16 | 17 | #print("Scan I2C {}".format([hex(x) for x in i2c.scan()])) 18 | 19 | tca = adafruit_tca9548a.TCA9548A(i2c) 20 | 21 | #print("Scan I2C {}".format([hex(x) for x in tca[0].scan()])) 22 | 23 | adc = ads1219.ADS1219(tca[0], 0x40) 24 | #adc = ads1219.ADS1219(i2c, 0x40) 25 | 26 | 27 | adc.set_channel(ads1219.ADS1219_MUX.AIN2) 28 | adc.set_conversion_mode(ads1219.ADS1219_CONV_MODE.CM_SINGLE) 29 | adc.set_gain(ads1219.ADS1219_GAIN.GAIN_1) 30 | adc.set_data_rate(ads1219.ADS1219_DATA_RATE.DR_20_SPS) # 20 SPS is the most accurate 31 | adc.set_vref(ads1219.ADS1219_VREF.VREF_EXTERNAL) 32 | 33 | 34 | if event == None: 35 | while True: 36 | result = adc.read_data() 37 | print('result = {}, V = {:.5f}'.format(result, 38 | result * 2.5 * (18.7+2)/2 / ads1219.ADS1219.POSITIVE_CODE_RANGE)) 39 | time.sleep(0.06) 40 | else: 41 | while True: 42 | event.wait() #blocks until the event is set by the other process 43 | event.clear() 44 | result = adc.read_data() 45 | print('result = {}, V = {:.5f}'.format(result, 46 | result * 2.5 * (18.7+2)/2 / ads1219.ADS1219.POSITIVE_CODE_RANGE)) 47 | 48 | def event_settings_process(event): 49 | while True: 50 | time.sleep(1) 51 | event.set() 52 | 53 | if __name__ == '__main__': 54 | ch_event = Event() 55 | 56 | multi_ch_device_process = Process(target = read_adc_values, args = (ch_event,)) 57 | multi_ch_device_process.start() 58 | 59 | event_trigger_process = Process(target = event_settings_process, args = (ch_event,)) 60 | event_trigger_process.start() 61 | -------------------------------------------------------------------------------- /lab_equipment/DMM_A2D_SENSE_BOARD.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling an A2D SENSE BOARD with I2C 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import DMMDevice 5 | 6 | # DMM 7 | class A2D_SENSE_BOARD(DMMDevice): 8 | connection_settings = { 9 | 'read_termination': '\r\n', 10 | 'write_termination': '\n', 11 | 'baud_rate': 57600, 12 | 'query_delay': 0.02, 13 | 'chunk_size': 102400, 14 | 'pyvisa_backend': '@py', 15 | 'time_wait_after_open': 2, 16 | 'idn_available': True 17 | } 18 | 19 | def initialize(self): 20 | self.i2c_adc_addr = None 21 | 22 | def reset_calibration(self): 23 | self.inst.write('SENSE:CAL:RESET') 24 | 25 | def calibrate_voltage(self, v1a, v1m, v2a, v2m): #2 points, actual (a) (dmm) and measured (m) (sense board) 26 | self.inst.write('SENSE:CAL:VOLT {},{},{},{}'.format(v1a, v1m, v2a, v2m)) 27 | 28 | def calibrate_current(self, i1a, i1m, i2a, i2m): #2 points, actual (a) (dmm) and measured (m) (sense board) 29 | self.inst.write('SENSE:CAL:CURR {},{},{},{}'.format(v1a, v1m, v2a, v2m)) 30 | 31 | def measure_voltage(self): 32 | return float(self.inst.query("MEAS:VOLT:DC?")) 33 | 34 | def measure_current(self): 35 | return float(self.inst.query("MEAS:CURR:DC?")) 36 | 37 | def measure_temperature(self): 38 | return float(self.inst.query("MEAS:TEMP_C?")) 39 | 40 | def set_i2c_adc_addr(self, addr): 41 | self.i2c_adc_addr = addr 42 | self.inst.write('INSTR:ADC:ADDR x {address}'.format(address = self.i2c_adc_addr)) 43 | 44 | def set_led(self, state): #state is a bool 45 | if state: 46 | self.inst.write('INSTR:SET:LED x {}'.format(True)) 47 | else: 48 | self.inst.write('INSTR:SET:LED x {}'.format(False)) 49 | 50 | def __del__(self): 51 | try: 52 | self.inst.close() 53 | except (AttributeError, pyvisa.errors.InvalidSession): 54 | pass 55 | -------------------------------------------------------------------------------- /lab_equipment/PSU_N8700.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Keysight N8700 series power supplies 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import PowerSupplyDevice 5 | 6 | # Power Supply 7 | class N8700(PowerSupplyDevice): 8 | # Initialize the N8700 Power Supply 9 | 10 | has_remote_sense = False 11 | can_measure_v_while_off = True #Have not checked this. 12 | 13 | connection_settings = { 14 | 'pyvisa_backend': '@py', 15 | 'time_wait_after_open': 0, 16 | 'idn_available': True 17 | } 18 | 19 | def initialize(self): 20 | self.inst.write("*RST") 21 | self.lock_front_panel(True) 22 | self.set_current(0) 23 | self.set_voltage(0) 24 | 25 | def select_channel(self, channel): 26 | pass 27 | 28 | def set_current(self, current_setpoint_A): 29 | self.inst.write("CURR {}".format(current_setpoint_A)) 30 | 31 | def set_voltage(self, voltage_setpoint_V): 32 | self.inst.write("VOLT {}".format(voltage_setpoint_V)) 33 | 34 | def toggle_output(self, state): 35 | if state: 36 | self.inst.write("OUTP ON") 37 | else: 38 | self.inst.write("OUTP OFF") 39 | 40 | def remote_sense(self, state): 41 | #remote sense is done by the wire connection, no software setting 42 | pass 43 | 44 | def lock_front_panel(self, state): 45 | if state: 46 | self.inst.write("SYST:COMM:RLST RWL") 47 | else: 48 | self.inst.write("SYST:COMM:RLST REM") 49 | 50 | def measure_voltage(self): 51 | return float(self.inst.query("MEAS:VOLT?")) 52 | 53 | def measure_current(self): 54 | return float(self.inst.query("MEAS:CURR?")) 55 | 56 | def measure_power(self): 57 | return self.measure_voltage()*self.measure_current() 58 | 59 | def __del__(self): 60 | try: 61 | self.toggle_output(False) 62 | self.lock_front_panel(False) 63 | self.inst.close() 64 | except (AttributeError, pyvisa.errors.InvalidSession): 65 | pass 66 | -------------------------------------------------------------------------------- /Measurement_Script.py: -------------------------------------------------------------------------------- 1 | #Quick and dirty python script to read a voltage a bunch of times 2 | 3 | import equipment as eq 4 | import time 5 | import easygui as eg 6 | 7 | def main(): 8 | dmm = eq.dmms.choose_dmm()[1] 9 | channel = None 10 | try: 11 | channel = eq.choose_channel(dmm.num_channels, dmm.start_channel) 12 | except AttributeError: 13 | pass 14 | 15 | choices = ['voltage', 'current', 'temperature'] 16 | msg = "What do you want to measure?" 17 | title = "Measurement Type" 18 | measurement_type = eg.choicebox(msg = msg, title = title, choices = choices) 19 | 20 | msg = "How Many Measurements to perform?" 21 | title = "Number of Measurements" 22 | num_measurements = int(eg.enterbox(msg = msg, title = title, default = 10, strip = True)) 23 | 24 | msg = "How many seconds to delay between measurements?" 25 | title = "Between Measurement Delay" 26 | delay_s_between = float(eg.enterbox(msg = msg, title = title, default = 0.1, strip = True)) 27 | 28 | msg = "How many seconds to delay before starting to measure?" 29 | title = "Pre-Measurement Delay" 30 | delay_s_before_start = int(eg.enterbox(msg = msg, title = title, default = 5, strip = True)) 31 | 32 | time.sleep(delay_s_before_start) 33 | 34 | for i in range(num_measurements): 35 | if measurement_type == 'voltage': 36 | if channel is not None: 37 | voltage = dmm.measure_voltage(channel) 38 | else: 39 | voltage = dmm.measure_voltage() 40 | print(voltage) 41 | elif measurement_type == 'current': 42 | if channel is not None: 43 | current = dmm.measure_current(channel) 44 | else: 45 | current = dmm.measure_current() 46 | print(current) 47 | elif measurement_type == 'temperature': 48 | if channel is not None: 49 | temperature = dmm.measure_temperature(channel) 50 | else: 51 | temperature = dmm.measure_temperature() 52 | print(temperature) 53 | time.sleep(delay_s_between) 54 | 55 | if __name__ == '__main__': 56 | main() 57 | -------------------------------------------------------------------------------- /voltage_to_temp.py: -------------------------------------------------------------------------------- 1 | #Convert voltage measurements to resistance and temperature 2 | #Specifically for use with the A2D DAQ testing setup 3 | 4 | from math import log 5 | 6 | #https://www.thinksrs.com/downloads/programs/Therm%20Calc/NTCCalibrator/NTCcalculator.htm 7 | #Thermistor Constants Steinhart_hart 8 | 9 | max_temp = 9999 10 | 11 | #NXRT15XV103FA1B 12 | NXRT15XV103FA1B_SH_CONSTANTS = {'SH_A': 1.119349044e-3, 13 | 'SH_B': 2.359019498e-4, 14 | 'SH_C': 0.7926382169e-7} 15 | 16 | #http://hyperphysics.phy-astr.gsu.edu/hbase/Tables/rstiv.html 17 | #in ohm * m^2 / m 18 | RESISTIVITY = {'copper': 1.59e-8, 19 | 'silver': 1.68e-8, 20 | 'aluminum': 2.65e-8, 21 | 'nickel': 6.99e-8} 22 | 23 | #https://www.powerstream.com/Wire_Size.htm 24 | #AWG: mm^2 25 | AWG_TO_MM2 = {30: 0.057, 26 | 28: 0.080, 27 | 26: 0.128, 28 | 24: 0.205, 29 | 22: 0.237, 30 | 20: 0.519, 31 | 18: 0.823, 32 | 16: 1.31, 33 | 14: 2.08, 34 | 12: 3.31, 35 | 10: 5.26, 36 | 8: 8.37, 37 | 6: 13.3, 38 | 4: 21.1, 39 | 2: 33.6, 40 | 1: 42.4, 41 | 0: 53.5} 42 | 43 | 44 | def wire_resistance(length_m = 1.0, awg = 26.0, material = 'copper'): 45 | #ohms = p * l / A 46 | return RESISTIVITY[material] * length_m / (AWG_TO_MM2[awg]/1000.0/1000.0) 47 | 48 | def resistance_to_temp(resistance, sh_dict = NXRT15XV103FA1B_SH_CONSTANTS): 49 | #Convert resistance to temperature (C) 50 | return (1.0/(sh_dict['SH_A'] + sh_dict['SH_B'] * log(resistance) + sh_dict['SH_C'] * (log(resistance)**3.0))) - 273.15 51 | 52 | def voltage_to_C(v_meas, r_pullup, v_pullup, sh_constants = NXRT15XV103FA1B_SH_CONSTANTS, wire_length_m = 3.0, wire_awg = 26.0): 53 | if(v_meas >= v_pullup): 54 | return max_temp 55 | 56 | #calculate bottom resistor in voltage divider 57 | resistance = (float(v_meas) * float(r_pullup)) / (float(v_pullup) - float(v_meas)) 58 | 59 | #compensate for the wire length 60 | resistance = resistance - wire_resistance(length_m = wire_length_m, awg = wire_awg) 61 | 62 | #compensate for the switch resistance of the IO expander 63 | #TODO - measure this 64 | 65 | #calculate temperature in Celcius 66 | temp_C = float(resistance_to_temp(resistance, sh_dict = sh_constants)) 67 | 68 | return temp_C 69 | 70 | -------------------------------------------------------------------------------- /DC_DC_Graph.py: -------------------------------------------------------------------------------- 1 | #DC-DC Test Graphing Results 2 | 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | import FileIO 6 | import os 7 | 8 | def calc_eff(df): 9 | df['p_in'] = df['v_in'] * df['i_in'] 10 | df['p_out'] = df['v_out'] * df['i_out'] #i_out is negative from the eloads - system (DC-DC Converter) is providing current 11 | df['p_loss'] = df['p_in'] + df['p_out'] #p_out is negative - system (DC-DC Converter) is losing power 12 | df['eff'] = abs(df['p_out'] / df['p_in']) 13 | df['i_out_pos'] = -1 * df['i_out'] 14 | 15 | def plot_eff(df, test_name, save_filepath = ''): 16 | if df.size == 0: 17 | return 18 | 19 | fig, ax = plt.subplots() 20 | fig.set_size_inches(12,10) 21 | 22 | set_voltages = pd.unique(df['v_in_set']) 23 | 24 | num_voltages = len(set_voltages) 25 | cm = plt.get_cmap('Set1') 26 | ax.set_prop_cycle('color', [cm(1.*i/num_voltages) for i in range(num_voltages)]) 27 | 28 | for voltage in set_voltages: 29 | df_mask = df[df['v_in_set'] == voltage] 30 | ax.plot('i_out_pos', 'eff', data = df_mask, label = voltage) 31 | 32 | title = "{} Efficiency".format(test_name) 33 | 34 | fig.suptitle(title) 35 | ax.set_ylabel('Efficiency') 36 | ax.set_xlabel('Load Current (A)') 37 | 38 | fig.legend(loc='lower right', title = 'Input Voltage') 39 | ax.grid(b=True, axis='both') 40 | 41 | #save the file if specified 42 | if(save_filepath != ''): 43 | plt.savefig(os.path.splitext(save_filepath)[0]) 44 | 45 | plt.show() 46 | 47 | 48 | def create_graphs(filepaths = None, save_dir = None): 49 | if filepaths == None: 50 | #get all the files to graph 51 | filepaths = FileIO.get_multiple_filepaths() 52 | 53 | if save_dir == None: 54 | save_dir = FileIO.get_directory(title = "Choose a location to save the graphs") 55 | 56 | for filepath in filepaths: 57 | dir_name, filename = os.path.split(filepath) 58 | 59 | split_path = filename.split(" ") 60 | test_name = split_path[0] 61 | graph_filename = os.path.join(save_dir, "{}_Graph".format(test_name)) 62 | 63 | #get the input voltage for this test 64 | df = pd.read_csv(filepath) 65 | 66 | calc_eff(df) 67 | plot_eff(df, test_name, graph_filename) 68 | 69 | ################ PROGRAM ################### 70 | 71 | if __name__ == '__main__': 72 | create_graphs() 73 | -------------------------------------------------------------------------------- /lab_equipment/PSU_KAXXXXP.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Korad KAXXXXP series power supplies 2 | #Korad 3010P 3 | #Korad 3005P 4 | #etc. 5 | 6 | import pyvisa 7 | import time 8 | from .PyVisaDeviceTemplate import PowerSupplyDevice 9 | 10 | # Power Supply 11 | class KAXXXXP(PowerSupplyDevice): 12 | # Initialize the Korad KAXXXXP Power Supply 13 | has_remote_sense = False 14 | can_measure_v_while_off = True #Have not checked this. 15 | 16 | connection_settings = { 17 | 'baud_rate': 9600, 18 | 'query_delay': 0.01, 19 | 'time_wait_after_open': 0, 20 | 'pyvisa_backend': '@py', 21 | 'idn_available': True 22 | } 23 | 24 | def initialize(self): 25 | #no *RST on this model 26 | time.sleep(0.01) 27 | self.lock_commands(False) 28 | time.sleep(0.01) 29 | self.toggle_output(0) 30 | time.sleep(0.01) 31 | self.set_current(0) 32 | time.sleep(0.01) 33 | self.set_voltage(0) 34 | time.sleep(0.01) 35 | 36 | # To set power supply limit in Amps 37 | def set_current(self, current_setpoint_A): 38 | self.inst.write("ISET1:{}".format(current_setpoint_A)) 39 | 40 | def set_voltage(self, voltage_setpoint_V): 41 | self.inst.write("VSET1:{}".format(voltage_setpoint_V)) 42 | 43 | def toggle_output(self, state, ch = 1): 44 | if state: 45 | self.inst.write("OUT1") 46 | else: 47 | self.inst.write("OUT0") 48 | 49 | def remote_sense(self, state): 50 | #these units do not have remote sense 51 | pass 52 | 53 | def lock_commands(self, state): 54 | #these units auto-lock front panel when USB connected 55 | pass 56 | 57 | def measure_voltage(self): 58 | return float(self.inst.query("VOUT1?")) 59 | 60 | def measure_current(self): 61 | return float(self.inst.query("IOUT1?")) 62 | 63 | def measure_power(self): 64 | current = self.measure_current() 65 | voltage = self.measure_voltage() 66 | return float(current*voltage) 67 | 68 | def __del__(self): 69 | try: 70 | self.toggle_output(False) 71 | self.lock_commands(False) 72 | self.inst.close() 73 | except (AttributeError, pyvisa.errors.InvalidSession): 74 | pass 75 | -------------------------------------------------------------------------------- /Eload_cv_sweep_graph.py: -------------------------------------------------------------------------------- 1 | #Eload CV Sweep graphing results - geared for solar panel testing 2 | 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | import FileIO 6 | import os 7 | 8 | def calc_power(df): 9 | df['current_pos'] = -1 * df['current'] 10 | df['power'] = df['voltage'] * df['current_pos'] 11 | 12 | def plot_power(df, test_name, save_filepath = ''): 13 | if df.size == 0: 14 | return 15 | 16 | fig, ax = plt.subplots() 17 | fig.set_size_inches(12,10) 18 | ax2 = ax.twinx() 19 | 20 | ax.plot('current_pos', 'voltage', '.-b', data = df, label = "Voltage (V)") 21 | ax2.plot('current_pos', 'power', '.-r', data = df, label = "Power (W)") 22 | 23 | title = "{} Power".format(test_name) 24 | 25 | fig.suptitle(title) 26 | ax.set_ylabel('Voltage (V)') 27 | ax2.set_ylabel('Power (W)') 28 | ax.set_xlabel('Current (A)') 29 | 30 | ax.grid(b=True, axis='both') 31 | 32 | #Add a point at the MPP and lines for mpp_v, mpp_i 33 | mpp_index = df['power'].argmax() 34 | mpp_p = df['power'].iat[mpp_index] 35 | mpp_i = df['current_pos'].iat[mpp_index] 36 | mpp_v = df['voltage'].iat[mpp_index] 37 | 38 | ax.hlines(y = mpp_v, xmin = min(df['current_pos']), xmax = max(df['current_pos']), color = 'g', label = 'MPP Voltage: {}V'.format(round(mpp_v, 2))) 39 | ax.vlines(x = mpp_i, ymin = min(df['voltage']), ymax = max(df['voltage']), color = 'g', label = 'MPP Current: {}A'.format(round(mpp_i, 2))) 40 | ax.plot(mpp_i, mpp_v, 'o', color = 'r', label = 'MPP Power: {}W'.format(round(mpp_p, 2))) 41 | 42 | fig.legend(loc = 'upper left') 43 | 44 | #save the file if specified 45 | if(save_filepath != ''): 46 | plt.savefig(os.path.splitext(save_filepath)[0]) 47 | 48 | plt.show() 49 | 50 | 51 | def create_graphs(filepaths = None, save_dir = None): 52 | if filepaths == None: 53 | #get all the files to graph 54 | filepaths = FileIO.get_multiple_filepaths() 55 | 56 | if save_dir == None: 57 | save_dir = FileIO.get_directory(title = "Choose a location to save the graphs") 58 | 59 | for filepath in filepaths: 60 | dir_name, filename = os.path.split(filepath) 61 | 62 | split_path = filename.split(" ") 63 | test_name = split_path[0] 64 | graph_filename = os.path.join(save_dir, "{}_Graph".format(test_name)) 65 | 66 | #get the input voltage for this test 67 | df = pd.read_csv(filepath) 68 | 69 | calc_power(df) 70 | plot_power(df, test_name, graph_filename) 71 | 72 | ################ PROGRAM ################### 73 | 74 | if __name__ == '__main__': 75 | create_graphs() 76 | -------------------------------------------------------------------------------- /FileIO.py: -------------------------------------------------------------------------------- 1 | import easygui as eg 2 | import pandas as pd 3 | import os 4 | import csv 5 | import time 6 | from datetime import datetime 7 | from stat import S_IREAD, S_IWUSR 8 | 9 | ####################### FILE IO ########################### 10 | 11 | 12 | def get_directory(title = "Choose Directory"): 13 | return eg.diropenbox(title) 14 | 15 | def write_line_csv(filepath, data_dict): 16 | 17 | with open(filepath, 'a', newline = '') as csvfile: 18 | writer = csv.DictWriter(csvfile, fieldnames = list(data_dict.keys())) 19 | if os.stat(filepath).st_size == 0: 20 | writer.writeheader() 21 | writer.writerow(data_dict) 22 | 23 | #read into pandas dataframe - works, quick to code 24 | #and is likely easy to extend - but one line doesn't really need it - likely quicker ways to do it 25 | #df = pd.DataFrame(data_dict).T 26 | 27 | #save to csv - append, no index, no header 28 | #df.to_csv(filepath, header=False, mode='a', index=False) 29 | 30 | def write_line_txt(filepath, line): 31 | with open(filepath, 'a') as txtfile: 32 | txtfile.write(f'{time.time()}: {line}\n') 33 | 34 | 35 | def start_file(directory, name, extension = '.csv'): 36 | dt = datetime.now().strftime("%Y-%m-%d %H-%M-%S") 37 | filename = f'{name} {dt}{extension}' 38 | 39 | filepath = os.path.join(directory, filename) 40 | 41 | return filepath 42 | 43 | def get_filepath(name = None, mult = False): 44 | if name == None: 45 | title = "Select the file" 46 | else: 47 | title = name 48 | return eg.fileopenbox(title = name, filetypes = [['*.csv', 'CSV Files']], multiple = mult) 49 | 50 | def get_multiple_filepaths(name = None): 51 | return get_filepath(name = name, mult = True) 52 | 53 | def ensure_subdir_exists_dir(filedir, subdir_name): 54 | candidate_dir = os.path.join(filedir, subdir_name) 55 | if not os.path.exists(candidate_dir): 56 | os.makedirs(candidate_dir) 57 | return candidate_dir 58 | 59 | def ensure_subdir_exists_file(filepath, subdir_name): 60 | return ensure_subdir_exists_dir(os.path.dirname(filepath), subdir_name) 61 | 62 | def write_data(filepath, data, printout=False, first_line = False): 63 | data["Log_Timestamp"] = time.time() 64 | 65 | if(printout): 66 | print(data) 67 | 68 | write_line_csv(filepath, data) 69 | 70 | def set_read_only(filepath): 71 | #make the file read-only so we don't lose decimal places if the CSV is opened in excel 72 | os.chmod(filepath, S_IREAD) 73 | 74 | def allow_write(filepath): 75 | #make the file writable 76 | #https://stackoverflow.com/questions/28492685/change-file-to-read-only-mode-in-python 77 | os.chmod(filepath, S_IWUSR|S_IREAD) 78 | -------------------------------------------------------------------------------- /lab_equipment/DMM_A2D_4CH_Isolated_ADC.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling an A2D 4CH Isolated ADC 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import DMMDevice 5 | 6 | # DMM 7 | class A2D_4CH_Isolated_ADC(DMMDevice): 8 | num_channels = 4 9 | start_channel = 1 10 | connection_settings = { 11 | 'read_termination': '\r\n', 12 | 'write_termination': '\n', 13 | 'baud_rate': 115200, 14 | 'query_delay': 0.001, 15 | 'time_wait_after_open': 0, 16 | 'chunk_size': 102400, 17 | 'pyvisa_backend': '@py', 18 | 'idn_available': True 19 | } 20 | 21 | def reset(self): 22 | self.inst.write('*RST') 23 | 24 | def reset_calibration(self, channel = 1): 25 | self.inst.write(f'CAL:RESET {channel}') 26 | 27 | def save_calibration(self, channel = 1): 28 | self.inst.write(f'CAL:SAVE {channel}') 29 | 30 | def get_calibration(self, channel = 1): 31 | #returns a list with offset,gain for each channel. 32 | return [float(val) for val in self.inst.query_ascii_values(f'CAL {channel}?')] 33 | 34 | def calibrate_voltage(self, v1a, v1m, v2a, v2m, channel = 1): #2 points, actual (a - dmm) and measured (m - dut) 35 | self.inst.write(f'CAL:VOLT {channel},{v1a},{v1m},{v2a},{v2m}') 36 | 37 | def measure_voltage(self, channel = 1): 38 | if channel == 0: 39 | #return a list with all 4 values. 40 | return [float(val) for val in self.inst.query_ascii_values(f'MEAS:VOLT {channel}?')] 41 | else: 42 | return float(self.inst.query(f'MEAS:VOLT {channel}?')) 43 | 44 | def measure_voltage_at_adc(self, channel = 1): 45 | if channel == 0: 46 | #return a list with all 4 values. 47 | return [float(val) for val in self.inst.query_ascii_values(f'MEAS:VOLT:ADC {channel}?')] 48 | else: 49 | return float(self.inst.query(f'MEAS:VOLT:ADC {channel}?')) 50 | 51 | def set_led(self, state): #state is a bool 52 | if state: 53 | self.inst.write('INSTR:LED 1') 54 | else: 55 | self.inst.write('INSTR:LED 0') 56 | 57 | def get_rs485_addr(self): 58 | return int(self.inst.query(f'INSTR:RS485?')) 59 | 60 | def get_num_channels(self): 61 | return A2D_4CH_Isolated_ADC.num_channels 62 | 63 | def __del__(self): 64 | try: 65 | self.inst.close() 66 | except (AttributeError, pyvisa.errors.InvalidSession): 67 | pass 68 | -------------------------------------------------------------------------------- /BATT_HIL/MCP2221_Multiple/MCP2221_Multiple.py: -------------------------------------------------------------------------------- 1 | #Testing multiple MCP devices on the same computer 2 | #Reference: https://github.com/adafruit/Adafruit_Blinka/pull/349 3 | #We need to use the HID.open_path method instead of the HID.open that the library uses 4 | 5 | import hid 6 | #need busio to override the I2C initialization so it doesn't use detector to look for the board 7 | import busio 8 | import time 9 | 10 | #The adafruit library creates an instance of the controller. We need to import that one 11 | from adafruit_blinka.microcontroller.mcp2221.mcp2221 import mcp2221 as ada_mcp2221 12 | from adafruit_blinka.microcontroller.mcp2221.mcp2221 import MCP2221_RESET_DELAY 13 | #We need to override the __init__ in adafruit's MCP2221 class so we need that as well 14 | from adafruit_blinka.microcontroller.mcp2221.mcp2221 import MCP2221 as _MCP2221 15 | #We also need to override the custom I2C class since they also use the mcp2221 that the library created 16 | from adafruit_blinka.microcontroller.mcp2221.i2c import I2C as _MCP2221_I2C 17 | 18 | class I2C(busio.I2C): 19 | def __init__(self, mcp2221_i2c): 20 | self._i2c = mcp2221_i2c 21 | 22 | class MCP2221_I2C(_MCP2221_I2C): 23 | def __init__(self, mcp2221, *, frequency=100000): 24 | self._mcp2221 = mcp2221 25 | self._mcp2221._i2c_configure(frequency) 26 | 27 | class MCP2221(_MCP2221): 28 | def __init__(self, address): 29 | self._hid = hid.device() 30 | self._hid.open_path(address) 31 | if MCP2221_RESET_DELAY >= 0: 32 | self._reset() 33 | self._gp_config = [0x07] * 4 # "don't care" initial value 34 | for pin in range(4): 35 | self.gp_set_mode(pin, self.GP_GPIO) # set to GPIO mode 36 | self.gpio_set_direction(pin, 1) # set to INPUT 37 | 38 | 39 | def connect_to_all_mcps(): 40 | #find all the hid addresses associated with devices that match the VID and PID of mcp2221 41 | addresses = [mcp["path"] for mcp in hid.enumerate(ada_mcp2221.VID, ada_mcp2221.PID)] 42 | 43 | print("Num Addresses Found: {}".format(len(addresses))) 44 | 45 | #create array to hold all devices that we find 46 | devices = [] 47 | addrs_used = [] 48 | 49 | #add the device that is auto-created from adafruit libraries 50 | #devices.append(_mcp2221) 51 | #ada_mcp2221._hid.close() 52 | #time.sleep(0.1) 53 | #del ada_mcp2221 54 | 55 | 56 | #TODO - this does not seem to be catching the extra address. 57 | index = 0 58 | for addr in addresses: 59 | print(addr) 60 | #skip the first address? - does not seem to work 61 | if(index == 0): 62 | #index += 1 63 | #continue 64 | pass 65 | try: 66 | device = MCP2221(addr) 67 | devices.append(device) 68 | except OSError: 69 | print("Device path: {} is already used".format(addr)) 70 | 71 | return devices 72 | -------------------------------------------------------------------------------- /lab_equipment/PSU_E3631A.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Keysight E3631A series power supplies 2 | #Commands: https://www.keysight.com/ca/en/assets/9018-01308/user-manuals/9018-01308.pdf?success=true 3 | 4 | import pyvisa 5 | import time 6 | from .PyVisaDeviceTemplate import PowerSupplyDevice 7 | 8 | # Power Supply 9 | class E3631A(PowerSupplyDevice): 10 | has_remote_sense = False 11 | can_measure_v_while_off = True #Have not checked this. 12 | 13 | connection_settings = { 14 | 'query_delay': 0.75, 15 | 'pyvisa_backend': '@ivi', 16 | 'time_wait_after_open': 0, 17 | 'idn_available': True 18 | } 19 | 20 | # Initialize the E3631A Power Supply 21 | def initialize(self): 22 | self.inst.write("*RST") 23 | 24 | split_string = self.inst_idn.split(",") 25 | self.manufacturer = split_string[0] 26 | self.model = split_string[1] 27 | self.zero_number = split_string[2] 28 | self.version_number = split_string[3] 29 | 30 | #Choose channel 2 by default 31 | self.select_channel(2) 32 | 33 | self.lock_front_panel(True) 34 | self.set_current(0) 35 | self.set_voltage(0) 36 | 37 | def select_channel(self, channel): 38 | #channel is a number - 1,2,3 39 | if(channel <= 3) and (channel > 0): 40 | self.inst.write("INST:NSEL {}".format(channel)) 41 | else: 42 | print("Invalid Channel") 43 | 44 | def set_current(self, current_setpoint_A): 45 | self.inst.write("CURR {}".format(current_setpoint_A)) 46 | 47 | def set_voltage(self, voltage_setpoint_V): 48 | self.inst.write("VOLT {}".format(voltage_setpoint_V)) 49 | 50 | def toggle_output(self, state): 51 | if state: 52 | self.inst.write("OUTP ON") 53 | else: 54 | self.inst.write("OUTP OFF") 55 | 56 | def remote_sense(self, state): 57 | pass 58 | 59 | def lock_front_panel(self, state): 60 | if state: 61 | self.inst.write("SYST:REM") 62 | else: 63 | self.inst.write("SYST:LOC") 64 | 65 | def measure_voltage(self): 66 | return float(self.inst.query("MEAS:VOLT?")) 67 | 68 | def measure_current(self): 69 | return float(self.inst.query("MEAS:CURR?")) 70 | 71 | def measure_power(self): 72 | return self.measure_current()*self.measure_voltage() 73 | 74 | def __del__(self): 75 | try: 76 | for ch in range(3): 77 | self.lock_front_panel(False) 78 | self.select_channel(ch+1) 79 | self.toggle_output(False) 80 | self.inst.close() 81 | except (AttributeError, pyvisa.errors.InvalidSession): 82 | pass 83 | -------------------------------------------------------------------------------- /lab_equipment/PSU_BK9100.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling BK Precision BK9100 series power supplies 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import PowerSupplyDevice 5 | 6 | # Power Supply 7 | class BK9100(PowerSupplyDevice): 8 | 9 | has_remote_sense = False 10 | can_measure_v_while_off = True #Have not checked this. 11 | class_name = 'BK9100' 12 | 13 | # Initialize the BK9100 Power Supply 14 | connection_settings = { 15 | 'baud_rate': 9600, 16 | 'write_termination': '\r', 17 | 'read_termination': '\r', 18 | 'idn_available': False, 19 | 'time_wait_after_open': 0, 20 | 'pyvisa_backend': '@py' 21 | } 22 | 23 | def initialize(self): 24 | #IDN and RST not implemented in this PSU 25 | #print("Connected to %s\n" % self.inst.query("*IDN?")) 26 | #self.inst.write("*RST") 27 | 28 | self.toggle_output(0) 29 | self.lock_front_panel(True) 30 | self.set_current(0) 31 | self.set_voltage(0) 32 | 33 | def select_channel(self, channel): 34 | pass 35 | 36 | def float_to_4_dig(self, val): 37 | val = int(val*100) 38 | #pad leading 0's if required 39 | val = str(val) 40 | while len(val) < 4: 41 | val = '0' + val 42 | return val 43 | 44 | #preset 3 is the output 45 | def set_current(self, current_setpoint_A): 46 | self.inst.query("CURR3{}".format(self.float_to_4_dig(current_setpoint_A))) 47 | 48 | def set_voltage(self, voltage_setpoint_V): 49 | self.inst.query("VOLT3{}".format(self.float_to_4_dig(voltage_setpoint_V))) 50 | 51 | def toggle_output(self, state): 52 | if state: 53 | self.inst.query("SOUT1") 54 | else: 55 | self.inst.query("SOUT0") 56 | 57 | def remote_sense(self, state): 58 | pass 59 | 60 | def lock_front_panel(self, state): 61 | pass 62 | 63 | #extra queries to clear the buffer 64 | def measure_voltage(self): 65 | v = self.inst.query("GETD") 66 | v = float(v[0:4])/100.0 67 | self.inst.query("") 68 | return v 69 | 70 | def measure_current(self): 71 | i = self.inst.query("GETD") 72 | i = float(i[4:8])/100.0 73 | self.inst.query("") 74 | return i 75 | 76 | def measure_power(self): 77 | p = self.inst.query("GETD") 78 | v = float(p[0:4])/100.0 79 | i = float(p[4:8])/100.0 80 | self.inst.query("") 81 | return v*i 82 | 83 | def __del__(self): 84 | try: 85 | self.toggle_output(False) 86 | self.lock_front_panel(False) 87 | self.inst.close() 88 | except (AttributeError, pyvisa.errors.InvalidSession): 89 | pass 90 | -------------------------------------------------------------------------------- /lab_equipment/PSU_DP800.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Rigol DP800 series power supplies 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import PowerSupplyDevice 5 | 6 | # Power Supply 7 | class DP800(PowerSupplyDevice): 8 | 9 | has_remote_sense = False 10 | can_measure_v_while_off = True #Have not checked this. 11 | connection_settings = { 12 | 'pyvisa_backend': '@ivi', 13 | 'time_wait_after_open': 0, 14 | 'idn_available': True 15 | } 16 | 17 | def initialize(self): 18 | self.inst.write("*RST") 19 | 20 | split_string = self.inst_idn.split(",") 21 | self.manufacturer = split_string[0] 22 | self.model = split_string[1] 23 | self.serial_number = split_string[2] 24 | self.version_number = split_string[3] 25 | 26 | if 'DP811' in self.model: 27 | self.has_remote_sense = True 28 | 29 | #Choose channel 1 by default 30 | self.select_channel(1) 31 | 32 | self.lock_front_panel(True) 33 | self.set_current(0) 34 | self.set_voltage(0) 35 | 36 | def select_channel(self, channel): 37 | #channel is a number - 1,2,3 38 | if(channel <= 3) and (channel > 0): 39 | self.inst.write(":INST:NSEL {}".format(channel)) 40 | 41 | def set_current(self, current_setpoint_A): 42 | self.inst.write(":CURR {}".format(current_setpoint_A)) 43 | 44 | def set_voltage(self, voltage_setpoint_V): 45 | self.inst.write(":VOLT {}".format(voltage_setpoint_V)) 46 | 47 | def toggle_output(self, state): 48 | if state: 49 | self.inst.write(":OUTP ON") 50 | else: 51 | self.inst.write(":OUTP OFF") 52 | 53 | def remote_sense(self, state): 54 | if self.has_remote_sense: 55 | #only for DP811 56 | if state: 57 | self.inst.write(":OUTP:SENS ON") 58 | else: 59 | self.inst.write(":OUTP:SENS OFF") 60 | 61 | def lock_front_panel(self, state): 62 | if state: 63 | self.inst.write(":SYST:REM") 64 | else: 65 | self.inst.write(":SYST:LOC") 66 | 67 | def measure_voltage(self): 68 | return float(self.inst.query(":MEAS:VOLT?")) 69 | 70 | def measure_current(self): 71 | return float(self.inst.query(":MEAS:CURR?")) 72 | 73 | def measure_power(self): 74 | return float(self.inst.query(":MEAS:POWE?")) 75 | 76 | def __del__(self): 77 | try: 78 | for ch in range(3): 79 | self.select_channel(ch+1) 80 | self.toggle_output(False) 81 | self.lock_front_panel(False) 82 | self.inst.close() 83 | except (AttributeError, pyvisa.errors.InvalidSession): 84 | pass 85 | -------------------------------------------------------------------------------- /lab_equipment/PSU_MP71025X.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling KORAD KWR10X or Multicomp Pro MP71025X series power supplies 2 | #KWR102 - MP710256 - 30V 30A 300W 3 | #KWR103 - MP710257 - 60V 15A 300W 4 | 5 | import pyvisa 6 | import time 7 | from .PyVisaDeviceTemplate import PowerSupplyDevice 8 | 9 | # Power Supply 10 | class MP71025X(PowerSupplyDevice): 11 | # Initialize the KWR102/3 MP710256/7 Power Supply 12 | has_remote_sense = True 13 | can_measure_v_while_off = True #Have not checked this. 14 | 15 | connection_settings = { 16 | 'baud_rate': 115200, 17 | 'read_termination': '\n', 18 | 'query_delay': 0.04, 19 | 'pyvisa_backend': '@py', 20 | 'time_wait_after_open': 0, 21 | 'idn_available': True 22 | } 23 | 24 | def initialize(self): 25 | split_string = self.inst_idn.split(" ") 26 | self.model_number = split_string[0] 27 | self.version_number = split_string[1] 28 | self.serial_number = split_string[2] 29 | 30 | #this unit does not have a reset command 31 | #self.inst.write("*RST") 32 | time.sleep(0.01) 33 | 34 | self.lock_commands(False) 35 | time.sleep(0.01) 36 | self.toggle_output(0) 37 | time.sleep(0.01) 38 | self.set_current(0) 39 | time.sleep(0.01) 40 | self.set_voltage(0) 41 | time.sleep(0.01) 42 | 43 | # To set power supply limit in Amps 44 | def set_current(self, current_setpoint_A): 45 | self.inst.write("ISET:{}".format(current_setpoint_A)) 46 | 47 | def set_voltage(self, voltage_setpoint_V): 48 | self.inst.write("VSET:{}".format(voltage_setpoint_V)) 49 | 50 | def toggle_output(self, state, ch = 1): 51 | if state: 52 | self.inst.write("OUT:1") 53 | else: 54 | self.inst.write("OUT:0") 55 | 56 | def remote_sense(self, state): 57 | if state: 58 | self.inst.write("COMP:1") 59 | else: 60 | self.inst.write("COMP:0") 61 | 62 | def lock_commands(self, state): 63 | if state: 64 | self.inst.write("LOCK:1") 65 | else: 66 | self.inst.write("LOCK:0") 67 | 68 | def measure_voltage(self): 69 | return float(self.inst.query("VOUT?")) 70 | 71 | def measure_current(self): 72 | return float(self.inst.query("IOUT?")) 73 | 74 | def measure_power(self): 75 | current = self.measure_current() 76 | voltage = self.measure_voltage() 77 | return float(current*voltage) 78 | 79 | def __del__(self): 80 | try: 81 | self.toggle_output(False) 82 | self.lock_commands(False) 83 | self.inst.close() 84 | except (AttributeError, pyvisa.errors.InvalidSession): 85 | pass 86 | -------------------------------------------------------------------------------- /BATT_HIL/fet_board_management.py: -------------------------------------------------------------------------------- 1 | #Class to manage all the connected FET boards in a separate process or thread. 2 | 3 | from . import Blinka_Example_MCP_Multiple as bemm 4 | from multiprocessing import Process, Queue, Event 5 | import queue #queue module required for exception handling of multiprocessing.Queue 6 | 7 | 8 | def create_event_and_queue_dicts(num_devices = 4, num_ch_per_device = 4): 9 | dict_for_event_and_queue = {} 10 | for device_num in range(num_devices): 11 | dict_for_event_and_queue[device_num] = {} 12 | for ch_num in range(num_ch_per_device): 13 | dict_for_event_and_queue[device_num][ch_num] = { 14 | 'v_event': Event(), 15 | 'v_queue': Queue(), 16 | 'i_event': Event(), 17 | 'i_queue': Queue(), 18 | 't_event': Event(), 19 | 't_queue': Queue() 20 | } 21 | 22 | #Create a new process to manage all the fet boards 23 | multi_ch_device_process = Process(target = fet_board_management, args = (dict_for_event_and_queue,)) 24 | multi_ch_device_process.start() 25 | 26 | return dict_for_event_and_queue 27 | 28 | def fet_board_management(queue_and_event_dict): 29 | boardManagement = FETBoardManagement(queue_and_event_dict) 30 | boardManagement.connect_to_multiple_devices() 31 | boardManagement.monitor_events() 32 | 33 | 34 | class FETBoardManagement: 35 | 36 | def __init__(self, dict_for_event_and_queue = None): 37 | self.dict_for_event_and_queue = dict_for_event_and_queue 38 | self.multi_ch_eq_dict = {} 39 | 40 | def connect_to_multiple_devices(self): 41 | self.multi_ch_eq_dict = bemm.get_mcp_dict() 42 | print("Devices found:") 43 | print(self.multi_ch_eq_dict) 44 | 45 | def monitor_events(self): 46 | #TODO - split this up into a thread for each I2C bus so we can read faster - don't have to wait for other devices to finish reading first. 47 | while True: 48 | for eq_key in self.multi_ch_eq_dict: 49 | if "FET Board" in eq_key: 50 | device_num = int(eq_key[-1]) 51 | for ch_num in range(self.multi_ch_eq_dict[eq_key].num_channels): 52 | for dict_key in self.dict_for_event_and_queue[device_num][ch_num]: 53 | #check the event to see if it has been set 54 | if 'event' in dict_key and self.dict_for_event_and_queue[device_num][ch_num][dict_key].is_set(): 55 | self.dict_for_event_and_queue[device_num][ch_num][dict_key].clear() 56 | #if set, measure voltage and put it in the appropriate queue 57 | if dict_key == 'v_event': 58 | self.dict_for_event_and_queue[device_num][ch_num]['v_queue'].put_nowait(self.multi_ch_eq_dict[eq_key].measure_voltage(ch_num)) 59 | elif dict_key == 'i_event': 60 | self.dict_for_event_and_queue[device_num][ch_num]['i_queue'].put_nowait(self.multi_ch_eq_dict[eq_key].measure_current(ch_num)) 61 | elif dict_key == 't_event': 62 | self.dict_for_event_and_queue[device_num][ch_num]['t_queue'].put_nowait(self.multi_ch_eq_dict[eq_key].measure_temperature(ch_num)) 63 | -------------------------------------------------------------------------------- /lab_equipment/OTHER_A2D_Relay_Board.py: -------------------------------------------------------------------------------- 1 | #python library for controlling the A2D Relay Board 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import PyVisaDevice 5 | 6 | #A2D Relay Board 7 | class A2D_Relay_Board(PyVisaDevice): 8 | connection_settings = { 9 | 'read_termination': '\r\n', 10 | 'write_termination': '\n', 11 | 'baud_rate': 57600, 12 | 'query_delay': 0.02, 13 | 'chunk_size': 102400, 14 | 'pyvisa_backend': '@py', 15 | 'time_wait_after_open': 2, 16 | 'idn_available': True 17 | } 18 | 19 | def initialize(self): 20 | self.equipment_type_connected = None 21 | self.i2c_expander_addr = None 22 | self._eload_connected = False 23 | self._psu_connected = False 24 | self._num_channels = self._get_num_channels() 25 | 26 | def __del__(self): 27 | try: 28 | self.inst.close() 29 | except AttributeError: 30 | pass 31 | 32 | def connect_psu(self, state): 33 | value = 0 34 | if state: 35 | value = 1 36 | 37 | if self.equipment_type_connected[0] == 'psu': 38 | psu_channel = 0 39 | elif self.equipment_type_connected[1] == 'psu': 40 | psu_channel = 1 41 | 42 | self.inst.write('INSTR:DAQ:SET:OUTP (@{ch}),{val}'.format(ch = psu_channel, val = value)) 43 | 44 | self._psu_connected = state 45 | 46 | def psu_connected(self): 47 | return self._psu_connected 48 | 49 | def connect_eload(self, state): 50 | value = 0 51 | if state: 52 | value = 1 53 | 54 | if self.equipment_type_connected[0] == 'eload': 55 | eload_channel = 0 56 | elif self.equipment_type_connected[1] == 'eload': 57 | eload_channel = 1 58 | 59 | self.inst.write('INSTR:DAQ:SET:OUTP (@{ch}),{val}'.format(ch = eload_channel, val = value)) 60 | 61 | self._eload_connected = state 62 | 63 | def eload_connected(self): 64 | return self._eload_connected 65 | 66 | def reset(self): 67 | self.inst.write('*RST') 68 | 69 | def set_led(self, value = 0): 70 | if(value > 1): 71 | value = 1 72 | #x is a character that we parse but do nothing with (channel must be first) 73 | self.inst.write('INSTR:DAQ:SET:LED x {val}'.format(val = value)) 74 | 75 | def _get_num_channels(self): 76 | return int(self.inst.query('INSTR:DAQ:GET:NCHS?')) 77 | 78 | def get_num_channels(self): 79 | return self._num_channels 80 | 81 | def set_i2c_expander_addr(self, addr): 82 | self.i2c_expander_addr = addr 83 | self.inst.write('INSTR:DAQ:SET:ADDR x {address}'.format(address = self.i2c_expander_addr)) 84 | 85 | if __name__ == "__main__": 86 | #connect to the daq 87 | relay_board = A2D_Relay_Board() 88 | -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/example-continuous-interrupt.py: -------------------------------------------------------------------------------- 1 | from machine import Pin 2 | from machine import I2C 3 | from ads1219 import ADS1219 4 | import utime 5 | 6 | # 7 | # Example for Espressif ESP32 microcontroller 8 | # 9 | # This example demonstrates how to use the ADS1219 in continuous conversion mode 10 | # The DRDY output pin of the ADS1219 is wired to a GPIO input pin on the ESP32. 11 | # DRDY indicates when each ADC conversion has completed 12 | # 13 | # The falling edge of the DRDY pin indicates that a new conversion result is ready for retrieval. 14 | # An ESP32 GPIO pin can be configured to trigger an interrupt on each falling 15 | # edge of DRDY. When DRDY falls an interrupt service routine (ISR) is called. 16 | # The ISR uses I2C to read the conversion result from the ADC. 17 | # 18 | # This is an efficient way to use the ADC. The processor does 19 | # not need to repeatedly poll the ADC to detect that a conversion has completed 20 | # 21 | # In this example, isr_callback() runs every time DRDY indicates that a conversion is done. 22 | # The read_data_irq() method reads the ADC using I2C to retrieve the conversion result 23 | # read_data_irq() is similar to read_data() but is optimized 24 | # for operation in an interrupt service routine 25 | # 26 | # here is the REPL output that is generated when this example is run. 27 | # output shows the 24-bit conversion result and the result converted to mV 28 | # 29 | # enabling DRDY interrupt 30 | # result = 6640930, mV = 1621.32 31 | # result = 6656388, mV = 1625.09 32 | # < ... 18 more results...> 33 | # irq_count = 20 34 | # 35 | 36 | def isr_callback(arg): 37 | global irq_count 38 | result = adc.read_data_irq() 39 | print('result = {}, mV = {:.2f}'.format(result, 40 | result * ADS1219.VREF_INTERNAL_MV / ADS1219.POSITIVE_CODE_RANGE)) 41 | irq_count += 1 42 | 43 | i2c = I2C(scl=Pin(26), sda=Pin(27)) 44 | adc = ADS1219(i2c) 45 | adc.set_channel(ADS1219.CHANNEL_AIN0) 46 | adc.set_conversion_mode(ADS1219.CM_CONTINUOUS) 47 | adc.set_gain(ADS1219.GAIN_1X) 48 | adc.set_data_rate(ADS1219.DR_20_SPS) # 20 SPS is the most accurate 49 | adc.set_vref(ADS1219.VREF_INTERNAL) 50 | drdy_pin = Pin(34, mode=Pin.IN) 51 | adc.start_sync() # starts continuous sampling 52 | irq_count = 0 53 | 54 | # enable interrupts 55 | print("enabling DRDY interrupt") 56 | irq = drdy_pin.irq(trigger=Pin.IRQ_FALLING, handler=isr_callback) 57 | 58 | # from this point onwards the ADS1219 will pull the DRDY pin 59 | # low whenever an ADC conversion has completed. The ESP32 60 | # will detect this falling edge on the GPIO pin (pin 34 in this 61 | # example) which will cause the isr_callback() routine to run. 62 | 63 | # The ESP32 will continue to process interrupts and call 64 | # isr_callback() during the following one second of sleep time. 65 | # The ADS1219 is configured for 20 conversions every second, so 66 | # the ISR will be called 20x during this second of sleep time. 67 | utime.sleep(1) 68 | # disable interrupt by specifying handler=None 69 | irq = drdy_pin.irq(handler=None) 70 | print('irq_count =', irq_count) -------------------------------------------------------------------------------- /A2D_DAQ_read_all_analog_pullup.py: -------------------------------------------------------------------------------- 1 | #python script for reading analog values from A2D DAQ 2 | 3 | from lab_equipment import A2D_DAQ_control as AD_control 4 | import pandas as pd 5 | import time 6 | import easygui as eg 7 | import FileIO 8 | 9 | pull_up_v = 3.3 10 | pull_up_r = 3300 11 | pull_up_cal_ch = 63 12 | 13 | def gather_and_write_data(filepath, timestamp, print_all=True, print_max = False): 14 | daq.set_led(1) 15 | #recalibrate pullup voltage 16 | daq.calibrate_pullup_v(pull_up_cal_ch) 17 | pull_up_v = daq.pullup_voltage 18 | 19 | data = {} 20 | 21 | #add timestamp 22 | data["Timestamp"] = timestamp 23 | 24 | max_temp_c = -273.15 25 | max_temp_c_groups = [-273.15, -273.15, -273.15, -273.15] 26 | 27 | #read all analog values 28 | for ch in range(daq.num_channels): 29 | volts = daq.measure_voltage(ch) 30 | 31 | data["Channel_{}".format(ch)] = volts*1000.0 32 | 33 | if(volts > pull_up_v): 34 | volts = pull_up_v 35 | 36 | temp_c = daq.measure_temperature(ch) 37 | 38 | if(ch/16 < 1): 39 | if(temp_c > max_temp_c_groups[0]): 40 | max_temp_c_groups[0] = temp_c 41 | elif(ch/32 < 1): 42 | if(temp_c > max_temp_c_groups[1]): 43 | max_temp_c_groups[1] = temp_c 44 | elif(ch/48 < 1): 45 | if(temp_c > max_temp_c_groups[2]): 46 | max_temp_c_groups[2] = temp_c 47 | else: 48 | if(temp_c > max_temp_c_groups[3]): 49 | max_temp_c_groups[3] = temp_c 50 | 51 | data["Channel_C_{}".format(ch)] = temp_c 52 | 53 | if(print_all): 54 | print(data) 55 | 56 | if(print_max): 57 | print('Max Temps (C)\tCH0-15: {:.1f}'.format(max_temp_c_groups[0]) + 58 | '\tCH16-31: {:.1f}'.format(max_temp_c_groups[1]) + 59 | '\tCH32-47: {:.1f}'.format(max_temp_c_groups[2]) + 60 | '\tCH48-63: {:.1f}'.format(max_temp_c_groups[3])) 61 | 62 | FileIO.write_line_csv(filepath, data) 63 | 64 | daq.set_led(0) 65 | 66 | 67 | ######################### Program ######################## 68 | if __name__ == '__main__': 69 | #declare and initialize the daq 70 | daq = AD_control.A2D_DAQ() 71 | daq.config_dict = AD_control.A2D_DAQ_config.get_config_dict() 72 | daq.configure_from_dict() 73 | 74 | #Gather the test settings 75 | lb = 1 76 | ub = 60 77 | msg = 'Enter logging interval in seconds (from {} to {}):'.format(lb, ub) 78 | title = 'Logging Interval' 79 | interval_temp = eg.integerbox(msg, title, default = 5, lowerbound = lb, upperbound = ub) 80 | 81 | dir = FileIO.get_directory() 82 | path = FileIO.start_file(dir, 'A2D-DAQ-Results') 83 | 84 | #read all analog channels once every X seconds 85 | starttime = time.time() 86 | while True: 87 | try: 88 | gather_and_write_data(path, timestamp=time.time()) 89 | time.sleep(interval_temp - ((time.time() - starttime) % interval_temp)) 90 | except KeyboardInterrupt: 91 | #make sure all outputs are off before quiting 92 | daq.reset() 93 | exit() 94 | -------------------------------------------------------------------------------- /Eload_cv_sweep.py: -------------------------------------------------------------------------------- 1 | # Script to sweep CV with eload for MPPT Testing 2 | 3 | import equipment as eq 4 | import time 5 | import easygui as eg 6 | import numpy as np 7 | import FileIO 8 | import Templates 9 | import jsonIO 10 | 11 | eload = eq.eLoads.choose_eload()[1] 12 | 13 | def init_instruments(): 14 | eload.set_mode_voltage() 15 | v_test_1 = eload.measure_voltage() 16 | 17 | def remove_extreme_values(list_to_remove, num_to_remove): 18 | for i in range(int(num_to_remove)): 19 | list_to_remove.remove(max(list_to_remove)) 20 | list_to_remove.remove(min(list_to_remove)) 21 | return list_to_remove 22 | 23 | def gather_data(samples_to_avg): 24 | data = dict() 25 | 26 | voltage = list() 27 | current = list() 28 | 29 | for i in range(int(samples_to_avg)): 30 | #The current and voltage measurements are not simultaneous 31 | #so we technically are not measuring true power 32 | #we will average each term individually as the load conditions are not 33 | #changing and any switching noise, mains noise, etc. will be averaged out (hopefully) 34 | time.sleep(0.01) 35 | voltage.append(eload.measure_voltage()) 36 | current.append(eload.measure_current()) 37 | 38 | #discard top and bottom measurements 39 | #average and save the rest of the measurements 40 | #need to be careful of number of samples taken 41 | 42 | #top and bottom amount to remove: 43 | remove_num = int(np.log(samples_to_avg)) 44 | 45 | voltage = remove_extreme_values(voltage, remove_num) 46 | current = remove_extreme_values(current, remove_num) 47 | 48 | #compute average of what's left (non-outliers) 49 | v = sum(voltage) / len(voltage) 50 | i = sum(current) / len(current) 51 | 52 | data['voltage']=(v) 53 | data['current']=(i) 54 | 55 | return data 56 | 57 | def sweep_load_voltage(filepath, test_name, voltage_list, settings): 58 | 59 | for voltage in voltage_list: 60 | eload.set_cv_voltage(voltage) 61 | time.sleep(settings["step_delay_s"]) 62 | data = gather_data(settings["measurement_samples_for_avg"]) 63 | FileIO.write_data(filepath, data, printout = True) 64 | 65 | 66 | if __name__ == '__main__': 67 | directory = FileIO.get_directory("Eload-CV-Sweep-Test") 68 | test_name = eg.enterbox(title = "Test Setup", msg = "Enter the Test Name\n(Spaces will be replaced with underscores)", 69 | default = "TEST_NAME", strip = True) 70 | test_name = test_name.replace(" ", "_") 71 | 72 | test_settings = Templates.EloadCVSweepSettings() 73 | test_settings = test_settings.settings 74 | test_settings = jsonIO.get_cycle_settings(test_settings, cycle_name = test_name) 75 | 76 | #generate a list of sweep settings - changing voltage for each sweep 77 | voltage_list = np.linspace(test_settings["min_cv_voltage"], 78 | test_settings["max_cv_voltage"], 79 | int(test_settings["num_voltage_steps"])) 80 | 81 | filepath = FileIO.start_file(directory, test_name) 82 | 83 | #Turn on power supply and eload to get the converter started up 84 | init_instruments() 85 | eload.set_cv_voltage(0) 86 | time.sleep(0.05) 87 | eload.toggle_output(True) 88 | 89 | sweep_load_voltage(filepath, test_name, voltage_list, test_settings) 90 | 91 | #Turn off power supply and eload 92 | eload.toggle_output(False) 93 | eload.set_cv_voltage(0) 94 | -------------------------------------------------------------------------------- /lab_equipment/DMM_SDM3065X.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Korad SDM3065X series eloads 2 | #Links helpful for finding commands: https://lygte-info.dk/project/TestControllerIntro 3 | 4 | import pyvisa 5 | from .PyVisaDeviceTemplate import DMMDevice 6 | 7 | #DMM 8 | class SDM3065X(DMMDevice): 9 | # Initialize the SDM3065X E-Load 10 | connection_settings = { 11 | 'read_termination': '\n', 12 | 'timeout': 5000, #5 second timeout 13 | 'time_wait_after_open': 0, 14 | 'pyvisa_backend': '@ivi', 15 | 'idn_available': True 16 | } 17 | defaults = {"NPLC": 1, 18 | "VOLT_DC_RANGE": 'AUTO'} 19 | 20 | def initialize(self): 21 | 22 | self.volt_ranges = {0.2: '200mv', 23 | 2: '2V', 24 | 20: '20V', 25 | 200: '200V', 26 | 1000: '1000V', 27 | 'AUTO': 'AUTO'} 28 | self.nplc_ranges = [100, 10, 1, 0.5, 0.05, 0.005] 29 | self.curr_ranges = {0.0002: '200uA', 30 | 0.002: '2mA', 31 | 0.02: '20mA', 32 | 0.2: '200mA', 33 | 2: '2A', 34 | 10: '10A', 35 | 'AUTO': 'AUTO'} 36 | self.mode = "NONE" 37 | self.setup_dcv = {"RANGE": None, 38 | "NPLC": None} 39 | 40 | self.inst.write("*RST") 41 | 42 | #set to remote mode (disable front panel) 43 | #self.lock_front_panel(True) 44 | 45 | 46 | def measure_voltage(self, nplc = defaults["NPLC"], volt_range = defaults["VOLT_DC_RANGE"]): 47 | 48 | if self.mode != "DCV": 49 | self.set_mode(mode = "DCV") 50 | 51 | if self.setup_dcv["NPLC"] != nplc: 52 | self.set_nplc(nplc = nplc) 53 | 54 | if self.setup_dcv["RANGE"] != volt_range: 55 | self.set_range_dcv(volt_range = volt_range) 56 | 57 | self.inst.write("INIT") #set to wait for trigger state 58 | return float(self.inst.query("READ?")) 59 | 60 | #uses auto-range and 10 PLC 61 | #return float(self.inst.query("MEAS:VOLT:DC?")) 62 | 63 | def set_mode(self, mode = "DCV"): 64 | if mode == "DCV": 65 | self.inst.write("CONF:VOLT:DC") 66 | self.set_auto_zero_dcv(False) #faster measurements 67 | self.mode = mode 68 | 69 | def set_auto_zero_dcv(self, state): 70 | self.inst.write("VOLT:DC:AZ {:d}".format(state)) 71 | 72 | def set_range_dcv(self, volt_range = defaults["VOLT_DC_RANGE"]): 73 | if volt_range not in self.volt_ranges.keys(): 74 | print("Invalid Voltage Range Selection") 75 | return 76 | if volt_range == 'AUTO': 77 | self.inst.write("VOLT:DC:RANG:AUTO ON") 78 | else: 79 | self.inst.write("VOLT:DC:RANG {}".format(self.volt_ranges[volt_range])) 80 | self.setup_dcv["RANGE"] = volt_range 81 | 82 | def set_nplc(self, nplc = defaults["NPLC"]): 83 | if nplc not in self.nplc_ranges: 84 | print("Invalid NPLC Selection") 85 | return 86 | self.inst.write("SENS:VOLT:DC:NPLC {}".format(nplc)) 87 | self.setup_dcv["NPLC"] = nplc 88 | 89 | 90 | #def measure_voltage_ac(self): 91 | # return float(self.inst.query("MEAS:VOLT:AC?")) 92 | 93 | #def measure_diode(self): 94 | # return float(self.inst.query("MEAS:DIODE?")) 95 | 96 | #def measure_current(self): 97 | # return float(self.inst.query("MEAS:CURR:DC?")) 98 | 99 | def __del__(self): 100 | #self.conf_voltage_dc(False) 101 | #self.lock_front_panel(False) 102 | try: 103 | self.inst.close() 104 | except (AttributeError, pyvisa.errors.InvalidSession): 105 | pass 106 | -------------------------------------------------------------------------------- /lab_equipment/DMM_DM3000.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Rigol DM3000 series dmm 2 | #Manuals: https://www.rigolna.com/products/digital-multimeters/dm3000/ 3 | 4 | import pyvisa 5 | from .PyVisaDeviceTemplate import DMMDevice 6 | 7 | # DMM 8 | class DM3000(DMMDevice): 9 | # Initialize the DM3000 DMM 10 | connection_settings = { 11 | 'read_termination': '\n', 12 | 'timeout': 5000, #5 second timeout 13 | 'time_wait_after_open': 0, 14 | 'pyvisa_backend': '@ivi', 15 | 'idn_available': True 16 | } 17 | 18 | def initialize(self): 19 | self.inst.write("*RST") 20 | 21 | self.volt_ranges = {0.2: 0, 22 | 2: 1, 23 | 20: 2, 24 | 200: 3, 25 | 1000: 4, 26 | 'AUTO': 'AUTO'} 27 | self.res_ranges = [0, 1, 2] 28 | self.rate_ranges = {20: 'S', 29 | 2: 'M', 30 | 1: 'F'} 31 | self.curr_ranges = {0.0002: '200uA', 32 | 0.002: '2mA', 33 | 0.02: '20mA', 34 | 0.2: '200mA', 35 | 2: '2A', 36 | 10: '10A', 37 | 'AUTO': 'AUTO'} 38 | self.mode = "NONE" 39 | self.setup_dcv = {"MODE": None, 40 | "RANGE": None, 41 | "RES": None, 42 | "NPLC": None} 43 | 44 | def measure_voltage(self, res = 2, volt_range = 'AUTO'): 45 | 46 | if self.mode != "DCV": 47 | self.set_mode(mode = "DCV") 48 | 49 | if self.setup_dcv["RES"] != res: 50 | self.set_res(res) 51 | 52 | if self.setup_dcv["RANGE"] != volt_range: 53 | self.set_range_dcv(volt_range = volt_range) 54 | 55 | return float(self.inst.query(":MEAS:VOLT:DC?")) 56 | 57 | def set_mode(self, mode = "DCV"): 58 | if mode == "DCV": 59 | self.inst.write(":FUNC:VOLT:DC") 60 | self.mode = mode 61 | 62 | def set_range_dcv(self, volt_range = 'AUTO'): 63 | if volt_range not in self.volt_ranges.keys(): 64 | print("Invalid Voltage Range Selection") 65 | return 66 | if volt_range == 'AUTO': 67 | self.inst.write(":MEAS AUTO") 68 | else: 69 | self.inst.write(":MEAS:VOLT:DC {}".format(self.volt_ranges[volt_range])) 70 | self.setup_dcv["RANGE"] = volt_range 71 | 72 | def set_res(self, res = 1): 73 | #res=0 is 4.5 digit 74 | #res=1 is 5.5 digit 75 | #res=2 is 6.5 digit 76 | #if res not in self.res_ranges: 77 | # print("Invalid Resolution Selection") 78 | # return 79 | #self.inst.write(":CONF:VOLT:DC {}".format(res)) 80 | #self.setup_dcv["RES"] = res 81 | pass 82 | 83 | def set_nplc(self, nplc = 1): 84 | #This is not quite NPLC, but we will keep it as NPLC for cross compatibility. 85 | #nplc=F is 123 readings/s, 50Hz refresh rate, almost 1 NPLC 86 | #nplc=M is 20 readings/s, 20Hz refresh rate, almost 2 NPLC 87 | #nplc=S is 2.5 readings/s, 2.5Hz refresh rate, almost 20 NPLC 88 | if nplc not in self.rate_ranges.keys(): 89 | print("Invalid Rate Selection") 90 | return 91 | self.inst.write(":RATE:VOLT:DC {}".format(nplc)) 92 | self.setup_dcv["NPLC"] = nplc 93 | 94 | #def measure_voltage_ac(self): 95 | # return float(self.inst.query("MEAS:VOLT:AC?")) 96 | 97 | #def measure_diode(self): 98 | # return float(self.inst.query("MEAS:DIODE?")) 99 | 100 | #def measure_current(self): 101 | # return float(self.inst.query(":MEAS:CURR?")) 102 | 103 | def __del__(self): 104 | try: 105 | self.inst.close() 106 | except (AttributeError, pyvisa.errors.InvalidSession): 107 | pass 108 | -------------------------------------------------------------------------------- /BATT_HIL/Multiple_MCP_Test.py: -------------------------------------------------------------------------------- 1 | import time 2 | import MCP2221_Multiple.MCP2221_Multiple as MMCP 3 | 4 | mcp_dict_list = [] 5 | 6 | def get_gpio_id(mcp): 7 | value = 0 8 | for pin in range(3): #only 3 pins used for addressing 9 | pin_val = mcp.gpio_get_pin(pin) 10 | print("Pin {} Value: {}".format(pin, pin_val)) 11 | value += (2**pin)*pin_val 12 | return value 13 | 14 | def print_gpio_ids(): 15 | print_string = "" 16 | iteration = 0 17 | for entry in mcp_dict_list: 18 | print_string += "MCP{}: {}\t".format(iteration, entry["GPIO_ID"]) 19 | iteration += 1 20 | print(print_string) 21 | time.sleep(0.5) 22 | 23 | def find_mcp_by_gpio_id(search_id): 24 | for entry in mcp_dict_list: 25 | if(entry["GPIO_ID"] == search_id): 26 | return entry["MCP"] 27 | print("GPIO ID {} not found".format(search_id)) 28 | 29 | def get_devices_dict(): 30 | devices = {} 31 | devices[gpio_to_name[1]] = [] 32 | gpio_ids = [mcp.gpio_id for mcp in mcps] 33 | if (0 in gpio_ids): 34 | devices[gpio_to_name[0]] = find_mcp_by_gpio_id(0) 35 | devices[gpio_to_name[0]].name = gpio_to_name[0] 36 | for i in range(1, 5): 37 | if (i in gpio_ids): 38 | devices[gpio_to_name[i]].append(find_mcp_by_gpio_id(i)) 39 | devices[gpio_to_name[i]][-1].name_id = i-1 40 | devices[gpio_to_name[i]][-1].name = gpio_to_name[i] 41 | return devices 42 | 43 | def print_devices(devices): 44 | print("\nDevices With Names:") 45 | try: 46 | print("{}: {}".format(devices[gpio_to_name[0]].name, 47 | devices[gpio_to_name[0]].gpio_id)) 48 | except KeyError: 49 | pass 50 | for device in devices[gpio_to_name[1]]: 51 | print("{}: {}".format(device.name, device.gpio_id)) 52 | 53 | 54 | gpio_to_name = {0: "Safety Controller", 55 | 1: "FET Board", 56 | 2: "FET Board", 57 | 3: "FET Board", 58 | 4: "FET Board"} 59 | 60 | mcps = MMCP.connect_to_all_mcps() 61 | num_mcps = len(mcps) 62 | 63 | print("Number of MCP2221s connected: {}\n".format(num_mcps)) 64 | 65 | for mcp in mcps: 66 | try: 67 | mcp_dict = {} 68 | mcp_dict["MCP"] = mcp 69 | mcp_dict["Num_GPIOs"] = 4 70 | mcp_dict["I2C_Bus"] = MMCP.I2C(MMCP.MCP2221_I2C(mcp_dict["MCP"], frequency = 100000)) 71 | mcp_dict["GPIO_ID"] = get_gpio_id(mcp_dict["MCP"]) 72 | print("GPIO ID: {}".format(mcp_dict["GPIO_ID"])) 73 | 74 | try: 75 | mcp_dict["Name"] = gpio_to_name[mcp_dict["GPIO_ID"]] 76 | except KeyError: 77 | print("GPIO ID Out of bounds: {}".format(mcp_dict["GPIO_ID"])) 78 | mcp_dict_list.append(mcp_dict) 79 | 80 | except OSError: 81 | continue 82 | 83 | print_gpio_ids() 84 | 85 | #devices = get_devices_dict() 86 | #print_devices(devices) 87 | 88 | #Then scan all the I2C busses: 89 | print("\nScanning I2C Busses") 90 | for entry in mcp_dict_list: 91 | print("\nMCP GPIO ID: {}".format(entry["GPIO_ID"])) 92 | print("I2C Addresses Found:") 93 | print(entry["I2C_Bus"].scan()) 94 | print("\n") 95 | 96 | 97 | #after we get the GPIOs, we don't need the MCP devices any more, just I2C addresses 98 | 99 | #accessing channel reads like this: 100 | #v = tester.measure_voltage(ch=0) 101 | #i = tester.measure_current(ch=0) 102 | 103 | #Do we want to kick the watchdogs all at once or does this introduce too much delay? 104 | #tester.kick_dogs() #this would kick all the watchdogs. Task would run every 0.75s? 105 | 106 | #I want to access devices like this: 107 | #devices["Safety_Controller"].i2c_bus 108 | #devices["Fet_Board"][0].i2c_bus 109 | #devices["Fet_Board"][1].i2c_bus 110 | #devices["Fet_Board"][2].i2c_bus 111 | #devices["Fet_Board"][3].i2c_bus 112 | 113 | #device name, device id, mcp device, i2c bus 114 | -------------------------------------------------------------------------------- /Calibration_Gui.py: -------------------------------------------------------------------------------- 1 | import Calibration_Script 2 | import sys 3 | from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel 4 | 5 | class MultimeterCalibrationApp(QWidget): 6 | def __init__(self): 7 | super().__init__() 8 | self.init_ui() 9 | self.cal_class = Calibration_Script.CalibrationClass() 10 | 11 | def init_ui(self): 12 | self.setWindowTitle('Multimeter Calibration') 13 | self.setGeometry(100, 100, 500, 200) 14 | 15 | self.connect_btn1 = QPushButton('Connect to Multimeter to Calibrate', self) 16 | self.connect_btn1.clicked.connect(self.connect_multimeter_to_calibrate) 17 | self.connected_label1 = QLabel('None', self) 18 | 19 | self.connect_btn2 = QPushButton('Connect to Calibrated Multimeter', self) 20 | self.connect_btn2.clicked.connect(self.connect_calibrated_multimeter) 21 | self.connected_label2 = QLabel('None', self) 22 | 23 | self.connect_btn3 = QPushButton('Connect to Power Supply', self) 24 | self.connect_btn3.clicked.connect(self.connect_power_supply) 25 | self.connected_label3 = QLabel('None', self) 26 | 27 | self.calibrate_btn = QPushButton('Calibrate Multimeter', self) 28 | self.calibrate_btn.clicked.connect(self.calibrate_multimeter) 29 | 30 | self.check_calibration_btn = QPushButton('Check Calibration', self) 31 | self.check_calibration_btn.clicked.connect(self.check_calibration) 32 | 33 | dut_layout = QHBoxLayout() 34 | dut_layout.addWidget(self.connect_btn1) 35 | dut_layout.addWidget(self.connected_label1) 36 | 37 | dmm_layout = QHBoxLayout() 38 | dmm_layout.addWidget(self.connect_btn2) 39 | dmm_layout.addWidget(self.connected_label2) 40 | 41 | psu_layout = QHBoxLayout() 42 | psu_layout.addWidget(self.connect_btn3) 43 | psu_layout.addWidget(self.connected_label3) 44 | 45 | control_layout = QVBoxLayout() 46 | control_layout.addLayout(dut_layout) 47 | control_layout.addLayout(dmm_layout) 48 | control_layout.addLayout(psu_layout) 49 | 50 | action_layout = QHBoxLayout() 51 | action_layout.addWidget(self.calibrate_btn) 52 | action_layout.addWidget(self.check_calibration_btn) 53 | 54 | main_layout = QVBoxLayout() 55 | main_layout.addLayout(control_layout) 56 | main_layout.addLayout(action_layout) 57 | 58 | self.setLayout(main_layout) 59 | 60 | def connect_multimeter_to_calibrate(self): 61 | # Add your connection logic here 62 | self.cal_class.connect_dut() 63 | self.connected_label1.setText(self.cal_class.dut_idn) 64 | 65 | def connect_calibrated_multimeter(self): 66 | # Add your connection logic here 67 | self.cal_class.connect_dmm() 68 | self.connected_label2.setText(self.cal_class.dmm_idn) 69 | 70 | def connect_power_supply(self): 71 | # Add your connection logic here 72 | self.cal_class.connect_psu() 73 | self.connected_label3.setText(self.cal_class.psu_idn) 74 | 75 | def calibrate_multimeter(self): 76 | # Add your calibration logic here 77 | print('Calibrating Multimeter...') 78 | self.cal_class.calibrate_voltage_meter() 79 | 80 | def check_calibration(self): 81 | # Add your calibration check logic here 82 | print('Checking Calibration...') 83 | self.cal_class.check_voltage_calibration() 84 | 85 | if __name__ == '__main__': 86 | app = QApplication(sys.argv) 87 | window = MultimeterCalibrationApp() 88 | window.show() 89 | sys.exit(app.exec()) -------------------------------------------------------------------------------- /lab_equipment/Eload_KEL10X.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Korad KEL10X series eloads 2 | #Links helpful for finding commands: https://lygte-info.dk/project/TestControllerIntro 3 | 4 | import pyvisa 5 | import time 6 | from .PyVisaDeviceTemplate import EloadDevice 7 | 8 | # E-Load 9 | class KEL10X(EloadDevice): 10 | 11 | has_remote_sense = True 12 | connection_settings = { 13 | 'baud_rate': 115200, 14 | 'read_termination': '\n', 15 | 'query_delay': 0.05, 16 | 'pyvisa_backend': '@ivi', 17 | 'time_wait_after_open': 0, 18 | 'idn_available': True 19 | } 20 | 21 | # Specific initialization for the KEL10X E-Load 22 | def initialize(self): 23 | split_string = self.inst_idn.split(" ") 24 | self.model_number = split_string[0] 25 | self.version_number = split_string[1] 26 | self.serial_number = split_string[2] 27 | 28 | if 'KEL103' in self.model_number: 29 | self.max_current = 30 30 | self.max_power = 300 31 | elif 'KEL102' in self.model_number: 32 | self.max_current = 30 33 | self.max_power = 150 34 | 35 | self.mode = "CURR" 36 | 37 | #unit does not have reset command 38 | #self.inst.write("*RST") 39 | self.set_mode_current() 40 | self.set_current(0) 41 | 42 | #set to remote mode (disable front panel) 43 | self.lock_front_panel(True) 44 | 45 | # To Set E-Load in Amps 46 | def set_current(self, current_setpoint_A): 47 | if self.mode != "CURR": 48 | print("ERROR - E-load not in correct mode") 49 | return 50 | if current_setpoint_A < 0: 51 | current_setpoint_A = -current_setpoint_A 52 | self.inst.write(":CURR {}A".format(current_setpoint_A)) 53 | 54 | def set_mode_current(self): 55 | self.inst.write(":FUNC CC") 56 | self.mode = "CURR" 57 | 58 | ##COMMANDS FOR CV MODE 59 | def set_mode_voltage(self): 60 | self.inst.write(":FUNC CV") 61 | self.mode = "VOLT" 62 | 63 | def set_cv_voltage(self, voltage_setpoint_V): 64 | if self.mode != "VOLT": 65 | print("ERROR - E-load not in correct mode") 66 | return 67 | self.inst.write(":VOLT {}".format(voltage_setpoint_V)) 68 | 69 | ##END OF COMMANDS FOR CV MODE 70 | 71 | def toggle_output(self, state): 72 | if state: 73 | self.inst.write(":INP 1") 74 | else: 75 | self.inst.write(":INP 0") 76 | 77 | def remote_sense(self, state): 78 | if state: 79 | self.inst.write(":SYST:COMP 1") 80 | else: 81 | self.inst.write(":SYST:COMP 0") 82 | 83 | def lock_front_panel(self, state): 84 | pass 85 | if state: 86 | self.inst.write(":SYST:LOCK 1") 87 | else: 88 | self.inst.write(":SYST:LOCK 0") 89 | 90 | def measure_voltage(self): 91 | return float(self.inst.query(":MEAS:VOLT?").strip('V\n')) 92 | 93 | def measure_current(self): 94 | return (float(self.inst.query(":MEAS:CURR?").strip('A\n')) * (-1)) 95 | 96 | def measure_power(self): 97 | return float(self.inst.query(":MEAS:POW?").strip('W\n')) 98 | 99 | def __del__(self): 100 | try: 101 | self.toggle_output(False) 102 | self.lock_front_panel(False) 103 | self.inst.close() 104 | except (AttributeError, pyvisa.errors.InvalidSession): 105 | pass 106 | -------------------------------------------------------------------------------- /lab_equipment/SMU_A2D_POWER_BOARD.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling an A2D SENSE BOARD with I2C 2 | 3 | import pyvisa 4 | from .PyVisaDeviceTemplate import SourceMeasureDevice 5 | 6 | # DMM 7 | class A2D_POWER_BOARD(SourceMeasureDevice): 8 | 9 | has_remote_sense = False 10 | connection_settings = { 11 | 'read_termination': '\r\n', 12 | 'write_termination': '\n', 13 | 'baud_rate': 57600, 14 | 'query_delay': 0.02, 15 | 'chunk_size': 102400, 16 | 'pyvisa_backend': '@py', 17 | 'time_wait_after_open': 2, 18 | 'idn_available': True 19 | } 20 | 21 | def initialize(self): 22 | self.current_target_a = 0.0 23 | self.output_is_on = False 24 | self.inst.write("*RST") 25 | self.i2c_dac_addr = None 26 | self.i2c_adc_addr = None 27 | 28 | def reset_calibration(self): 29 | self.inst.write('POWER:CAL:RESET') 30 | self.inst.write('SENSE:CAL:RESET') 31 | 32 | def calibrate_voltage(self, v1a, v1m, v2a, v2m): #2 points, actual (a) (dmm) and measured (m) (sense board) 33 | self.inst.write('SENSE:CAL:VOLT {},{},{},{}'.format(v1a, v1m, v2a, v2m)) 34 | 35 | def calibrate_current(self, i1a, i1m, i2a, i2m): #2 points, actual (a) (dmm) and measured (m) (sense board) 36 | self.inst.write('SENSE:CAL:CURR {},{},{},{}'.format(v1a, v1m, v2a, v2m)) 37 | 38 | def measure_voltage(self): 39 | return float(self.inst.query("MEAS:VOLT:DC?")) 40 | 41 | def measure_current(self): 42 | return float(self.inst.query("MEAS:CURR:DC?")) 43 | 44 | def measure_temperature(self): 45 | return float(self.inst.query("MEAS:TEMP_C?")) 46 | 47 | def set_current(self, current_setpoint_A): 48 | #Turn output off and back on when switching current directions. Send new target before turning back on 49 | if self.output_is_on and (self.current_target_a <= 0.0 and current_setpoint_A > 0.0) or (self.current_target_a >= 0.0 and current_setpoint_A < 0.0): 50 | self.toggle_output(False) 51 | self.current_target_a = current_setpoint_A 52 | 53 | self.inst.write("CURR {}".format(abs(current_setpoint_A))) 54 | self.toggle_output(True) 55 | else: 56 | self.current_target_a = current_setpoint_A 57 | self.inst.write("CURR {}".format(abs(current_setpoint_A))) 58 | 59 | def set_voltage(self, voltage_setpoint_V): 60 | self.inst.write("VOLT {}".format(voltage_setpoint_V)) 61 | 62 | def toggle_output(self, state): 63 | if state: 64 | if self.current_target_a >= 0.0: 65 | self.inst.write("ELOAD:OFF") 66 | self.inst.write("PSU:ON") 67 | elif self.current_target_a < 0.0: 68 | self.inst.write("PSU:OFF") 69 | self.inst.write("ELOAD:ON") 70 | self.output_is_on = True 71 | else: 72 | self.inst.write("PSU:OFF") 73 | self.inst.write("ELOAD:OFF") 74 | self.output_is_on = False 75 | 76 | def set_i2c_dac_addr(self, addr): 77 | self.i2c_dac_addr = addr 78 | self.inst.write('INSTR:DAC:ADDR x {address}'.format(address = self.i2c_dac_addr)) 79 | 80 | def set_i2c_adc_addr(self, addr): 81 | self.i2c_adc_addr = addr 82 | self.inst.write('INSTR:ADC:ADDR x {address}'.format(address = self.i2c_adc_addr)) 83 | 84 | def set_led(self, state): #state is a bool 85 | if state: 86 | self.inst.write('INSTR:SET:LED x {}'.format(True)) 87 | else: 88 | self.inst.write('INSTR:SET:LED x {}'.format(False)) 89 | 90 | def __del__(self): 91 | try: 92 | self.inst.close() 93 | except (AttributeError, pyvisa.errors.InvalidSession): 94 | pass 95 | -------------------------------------------------------------------------------- /lab_equipment/PSU_SPD1000.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Siglent SPD1000 series power supplies 2 | 3 | import pyvisa 4 | import time 5 | from .PyVisaDeviceTemplate import PowerSupplyDevice 6 | from retry import retry 7 | 8 | class SetpointException(Exception): 9 | #Raised when a command is not passed to the instrument correctly. 10 | pass 11 | 12 | # Power Supply 13 | class SPD1000(PowerSupplyDevice): 14 | # Initialize the SPD1000 Power Supply 15 | has_remote_sense = True 16 | can_measure_v_while_off = False 17 | 18 | connection_settings = { 19 | 'read_termination': '\n', 20 | 'write_termination': '\n', 21 | 'query_delay': 0.15, 22 | 'pyvisa_backend': '@ivi', 23 | 'time_wait_after_open': 0, 24 | 'idn_available': True 25 | } 26 | 27 | setpoint_compare_tolerance = 0.0001 28 | 29 | def initialize(self): 30 | self.inst.write("*RST") 31 | time.sleep(0.1) 32 | 33 | #Choose channel 1 34 | self.inst.write("INST CH1") 35 | time.sleep(0.1) 36 | self.lock_commands(False) 37 | time.sleep(0.1) 38 | self.toggle_output(False) #Apparently RST does not turn off the output? 39 | time.sleep(0.1) 40 | self.set_current(0) 41 | time.sleep(0.1) 42 | self.set_voltage(0) 43 | time.sleep(0.1) 44 | 45 | # To Set power supply current limit in Amps 46 | @retry(SetpointException, delay=0.1, tries=10) 47 | def set_current(self, current_setpoint_A): 48 | self.inst.write("CURR {}".format(current_setpoint_A)) 49 | if abs(self.get_current() - current_setpoint_A) > SPD1000.setpoint_compare_tolerance: 50 | raise SetpointException 51 | 52 | def get_current(self): 53 | return float(self.inst.query("CURR?")) 54 | 55 | @retry(SetpointException, delay=0.1, tries=10) 56 | def set_voltage(self, voltage_setpoint_V): 57 | self.inst.write("VOLT {}".format(voltage_setpoint_V)) 58 | if abs(self.get_voltage() - voltage_setpoint_V) > SPD1000.setpoint_compare_tolerance: 59 | raise SetpointException 60 | 61 | def get_voltage(self): 62 | return float(self.inst.query("VOLT?")) 63 | 64 | @retry(SetpointException, delay=0.1, tries=10) 65 | def toggle_output(self, state, ch = 1): 66 | if state: 67 | self.inst.write("OUTP CH{},ON".format(ch)) 68 | else: 69 | self.inst.write("OUTP CH{},OFF".format(ch)) 70 | if self.get_output() != state: 71 | raise SetpointException 72 | 73 | def get_output(self): 74 | val = int(self.inst.query("SYST:STAT?"),16) 75 | return bool(val & (1<<4)) #Bit number 4 is Output 76 | 77 | @retry(SetpointException, delay=0.1, tries=10) 78 | def remote_sense(self, state): 79 | if state: 80 | self.inst.write("MODE:SET 4W") 81 | else: 82 | self.inst.write("MODE:SET 2W") 83 | if self.get_remote_sense() != state: 84 | raise SetpointException 85 | 86 | def get_remote_sense(self): 87 | val = int(self.inst.query("SYST:STAT?"),16) 88 | return bool(val & (1<<5)) #Bit number 5 is remote sense 89 | 90 | def lock_commands(self, state): 91 | if state: 92 | self.inst.write("*LOCK") 93 | else: 94 | self.inst.write("*UNLOCK") 95 | 96 | def measure_voltage(self): 97 | return float(self.inst.query("MEAS:VOLT?")) 98 | 99 | def measure_current(self): 100 | return float(self.inst.query("MEAS:CURR?")) 101 | 102 | def measure_power(self): 103 | return float(self.inst.query("MEAS:POWE?")) 104 | 105 | def __del__(self): 106 | try: 107 | self.toggle_output(False) 108 | self.lock_commands(False) 109 | self.inst.close() 110 | except (AttributeError, pyvisa.errors.InvalidSession): 111 | pass 112 | -------------------------------------------------------------------------------- /lab_equipment/Eload_DL3000.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Rigol DL3000 series eloads 2 | 3 | import pyvisa 4 | import time 5 | from .PyVisaDeviceTemplate import EloadDevice 6 | 7 | # E-Load 8 | class DL3000(EloadDevice): 9 | 10 | has_remote_sense = True 11 | connection_settings = { 12 | 'pyvisa_backend': '@ivi', 13 | 'time_wait_after_open': 0, 14 | 'idn_available': True 15 | } 16 | 17 | # Initialize the DL3000 E-Load 18 | def initialize(self): 19 | idn_split = self.inst_idn.split(',') 20 | model_number = idn_split[1] 21 | self.inst.write("*RST") 22 | 23 | if 'DL3021' in model_number: 24 | #values specific to the DL3021 & DL3021A 25 | self.ranges = {"low":4,"high":40} 26 | self.max_current = 40 27 | self.max_power = 200 28 | elif 'DL3031' in model_number: 29 | self.ranges = {"low":6,"high":60} 30 | self.max_current = 60 31 | self.max_power = 350 32 | 33 | self.range = "low" 34 | self.mode = "CURR" 35 | self.set_mode_current() 36 | self.set_current(0) 37 | self.set_range("high") 38 | 39 | #set to remote mode (disable front panel) 40 | #self.lock_front_panel(True) 41 | 42 | # To Set E-Load in Amps 43 | def set_current(self, current_setpoint_A): 44 | if self.mode != "CURR": 45 | print("ERROR - E-load not in correct mode") 46 | return 47 | 48 | if current_setpoint_A < 0: 49 | current_setpoint_A = -current_setpoint_A 50 | 51 | #4A range 52 | #if(current_setpoint_A <= self.ranges["low"]): 53 | # if(self.range != "low"): 54 | # self.set_range("low") 55 | 56 | #40A range 57 | #elif(current_setpoint_A <= self.ranges["high"]): 58 | # if(self.range != "high"): 59 | # self.set_range("high") 60 | 61 | self.inst.write(":CURR:LEV {}".format(current_setpoint_A)) 62 | 63 | def set_range(self, set_range): 64 | #set_range is either "high" or "low" 65 | write_range = "MIN" 66 | if(set_range == "high"): 67 | write_range = "MAX" 68 | self.inst.write(":CURR:RANG {}".format(write_range)) 69 | self.range = set_range 70 | 71 | def set_mode_current(self): 72 | self.inst.write(":FUNC CURR") 73 | self.mode = "CURR" 74 | 75 | 76 | ##COMMANDS FOR CV MODE 77 | def set_mode_voltage(self): 78 | self.inst.write(":FUNC VOLT") 79 | time.sleep(0.05) 80 | self.inst.write(":VOLT:RANG MAX") 81 | self.mode = "VOLT" 82 | 83 | def set_cv_voltage(self, voltage_setpoint_V): 84 | if self.mode != "VOLT": 85 | print("ERROR - E-load not in correct mode") 86 | return 87 | self.inst.write(":VOLT {}".format(voltage_setpoint_V)) 88 | 89 | #also have :VOLT:RANG :VOLT:VLIM :VOLT:ILIM 90 | 91 | ##END OF COMMANDS FOR CV MODE 92 | 93 | def toggle_output(self, state): 94 | if state: 95 | self.inst.write(":INP ON") 96 | else: 97 | self.inst.write(":INP OFF") 98 | 99 | def remote_sense(self, state): 100 | if state: 101 | self.inst.write(":SENS ON") 102 | else: 103 | self.inst.write(":SENS OFF") 104 | 105 | def lock_front_panel(self, state): 106 | pass 107 | # if state: 108 | # self.inst.write("SYST:REM") 109 | # else: 110 | # self.inst.write("SYST:LOC") 111 | 112 | def measure_voltage(self): 113 | return float(self.inst.query(":MEAS:VOLT:DC?")) 114 | 115 | def measure_current(self): 116 | return (float(self.inst.query(":MEAS:CURR:DC?")) * (-1)) 117 | 118 | def __del__(self): 119 | try: 120 | self.toggle_output(False) 121 | #self.lock_front_panel(False) 122 | self.inst.close() 123 | except (AttributeError, pyvisa.errors.InvalidSession): 124 | pass 125 | -------------------------------------------------------------------------------- /lab_equipment/Eload_BK8600.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling BK8600 series eloads 2 | 3 | import pyvisa 4 | import time 5 | from .PyVisaDeviceTemplate import EloadDevice 6 | 7 | # E-Load 8 | class BK8600(EloadDevice): 9 | 10 | has_remote_sense = True 11 | connection_settings = { 12 | 'pyvisa_backend': '@ivi', 13 | 'time_wait_after_open': 0, 14 | 'idn_available': True 15 | } 16 | 17 | 18 | def initialize(self): 19 | idn_split = self.inst_idn.split(',') 20 | model_number = idn_split[1] 21 | 22 | #resets to Constant Current Mode 23 | self.inst.write("*RST") 24 | 25 | if '8600' in model_number: 26 | self.max_current = 30 27 | self.max_power = 150 28 | elif '8601' in model_number: 29 | self.max_current = 60 30 | self.max_power = 250 31 | elif '8602' in model_number: 32 | self.max_current = 15 33 | self.max_power = 200 34 | elif '8610' in model_number: 35 | self.max_current = 120 36 | self.max_power = 750 37 | elif '8612' in model_number: 38 | self.max_current = 30 39 | self.max_power = 750 40 | elif '8614' in model_number: 41 | self.max_current = 240 42 | self.max_power = 1500 43 | elif '8616' in model_number: 44 | self.max_current = 60 45 | self.max_power = 1200 46 | elif '8620' in model_number: 47 | self.max_current = 480 48 | self.max_power = 3000 49 | elif '8622' in model_number: 50 | self.max_current = 100 51 | self.max_power = 2500 52 | elif '8624' in model_number: 53 | self.max_current = 600 54 | self.max_power = 4500 55 | elif '8610' in model_number: 56 | self.max_current = 720 57 | self.max_power = 6000 58 | 59 | self.mode = "CURR" 60 | 61 | self.set_current(0) 62 | #set to remote mode (disable front panel) 63 | self.lock_front_panel(True) 64 | 65 | # To Set E-Load in Amps 66 | def set_current(self, current_setpoint_A): 67 | if self.mode != "CURR": 68 | print("ERROR - E-load not in correct mode") 69 | return 70 | if current_setpoint_A < 0: 71 | current_setpoint_A = -current_setpoint_A 72 | self.inst.write("CURR:LEV {}".format(current_setpoint_A)) 73 | 74 | def set_mode_current(self): 75 | self.inst.write("FUNC CURR") 76 | self.mode = "CURR" 77 | 78 | ##COMMANDS FOR CV MODE 79 | def set_mode_voltage(self): 80 | self.inst.write("FUNC VOLT") 81 | self.mode = "VOLT" 82 | #Only 1 voltage range on this eload 83 | 84 | def set_cv_voltage(self, voltage_setpoint_V): 85 | if self.mode != "VOLT": 86 | print("ERROR - E-load not in correct mode") 87 | return 88 | self.inst.write("VOLT {}".format(voltage_setpoint_V)) 89 | 90 | ##END OF COMMANDS FOR CV MODE 91 | 92 | def toggle_output(self, state): 93 | if state: 94 | self.inst.write("INP ON") 95 | else: 96 | self.inst.write("INP OFF") 97 | 98 | def remote_sense(self, state): 99 | if state: 100 | self.inst.write("REM:SENS ON") 101 | else: 102 | self.inst.write("REM:SENS OFF") 103 | 104 | def lock_front_panel(self, state): 105 | if state: 106 | self.inst.write("SYST:REM") 107 | else: 108 | self.inst.write("SYST:LOC") 109 | 110 | def measure_voltage(self): 111 | return float(self.inst.query("MEAS:VOLT:DC?")) 112 | 113 | def measure_current(self): 114 | return (float(self.inst.query("MEAS:CURR:DC?")) * (-1)) 115 | 116 | def __del__(self): 117 | try: 118 | self.toggle_output(False) 119 | self.lock_front_panel(False) 120 | self.inst.close() 121 | except (AttributeError, pyvisa.errors.InvalidSession): 122 | pass 123 | -------------------------------------------------------------------------------- /lab_equipment/Eload_PARALLEL.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands to emulate a combination of eloads as a single eload 2 | 3 | import pyvisa 4 | import time 5 | import easygui as eg 6 | import equipment as eq 7 | 8 | # E-Load 9 | class PARALLEL: 10 | # Initialize the Parallel E-Load 11 | 12 | has_remote_sense = False 13 | 14 | def __init__(self, resource_id = None, resources_list = None): 15 | 16 | #combine 2 or more eloads in parallel and split the current between them 17 | #need to create a 'virtual eload' here that emulates the other eload classes. 18 | 19 | self.class_name_1 = None 20 | self.class_name_2 = None 21 | self.res_id_1 = None 22 | self.res_id_2 = None 23 | self.setup_dict_1 = {'remote_sense': None} 24 | self.setup_dict_2 = {'remote_sense': None} 25 | 26 | print(resource_id) 27 | if resource_id != None: 28 | self.class_name_1 = resource_id['class_name_1'] 29 | self.class_name_2 = resource_id['class_name_2'] 30 | self.res_id_1 = resource_id['res_id_1'] 31 | self.res_id_2 = resource_id['res_id_2'] 32 | self.setup_dict_1 = {'remote_sense': resource_id['remote_sense_1']} 33 | self.setup_dict_2 = {'remote_sense': resource_id['remote_sense_2']} 34 | #self.use_remote_sense_1 = resource_id['use_remote_sense_1'] 35 | #self.use_remote_sense_2 = resource_id['use_remote_sense_2'] 36 | 37 | 38 | #choose_eload(self, class_name = None, resource_id = None, setup_dict = None, resources_list = None) 39 | eload_1_list = eq.eLoads.choose_eload(self.class_name_1, self.res_id_1, self.setup_dict_1) 40 | eload_2_list = eq.eLoads.choose_eload(self.class_name_2, self.res_id_2, self.setup_dict_2) 41 | 42 | self.class_name_1 = eload_1_list[0] 43 | self.class_name_2 = eload_2_list[0] 44 | self.eload1 = eload_1_list[1] 45 | self.eload2 = eload_2_list[1] 46 | self.use_remote_sense_1 = eload_1_list[2] 47 | self.use_remote_sense_2 = eload_2_list[2] 48 | 49 | #We want to load each eload proportionally to its max load. 50 | #We will start by sharing power proportionally, but should account in the future for uneven current ranges. 51 | #e.g. a high power load with 20A 500V 1000W and a lower power load wit 60A 120V 250W 52 | self.max_current = self.eload1.max_current + self.eload2.max_current 53 | self.max_power = self.eload1.max_power + self.eload2.max_power 54 | 55 | self.eload1_current_share = self.eload1.max_power / self.max_power 56 | self.eload2_current_share = self.eload2.max_power / self.max_power 57 | 58 | self.mode = "CURR" 59 | self.set_mode_current() 60 | 61 | def set_mode_current(self): 62 | self.eload1.set_mode_current() 63 | self.eload2.set_mode_current() 64 | self.mode = "CURR" 65 | 66 | def set_mode_voltage(self): 67 | print("ERROR - CV mode with parallel eloads is not allowed") 68 | 69 | # To Set E-Load in Amps 70 | def set_current(self, current_setpoint_A): 71 | if self.mode != "CURR": 72 | print("ERROR - E-load not in correct mode") 73 | return 74 | self.eload1.set_current(current_setpoint_A * self.eload1_current_share) 75 | self.eload2.set_current(current_setpoint_A * self.eload2_current_share) 76 | 77 | def toggle_output(self, state): 78 | self.eload1.toggle_output(state) 79 | self.eload2.toggle_output(state) 80 | 81 | def remote_sense(self, state): 82 | #only do remote sense on a single eload, we only need a single measurement and it doesn't compensate for anything. 83 | self.eload1.remote_sense(state) 84 | 85 | def lock_front_panel(self, state): 86 | self.eload1.lock_front_panel(state) 87 | self.eload2.lock_front_panel(state) 88 | 89 | def measure_voltage(self): 90 | #use only the voltage measurement from the first eload 91 | return self.eload1.measure_voltage() 92 | 93 | def measure_current(self): 94 | return (self.eload1.measure_current() + self.eload2.measure_current()) 95 | 96 | #def __del__(self): 97 | # pass -------------------------------------------------------------------------------- /lab_equipment/A2D_DAQ_Config_All_Thermistor_NXRT15XV.csv: -------------------------------------------------------------------------------- 1 | Channel,Input_Type,Voltage_Scaling,Temp_A,Temp_B,Temp_C 2 | 0,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 3 | 1,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 4 | 2,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 5 | 3,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 6 | 4,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 7 | 5,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 8 | 6,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 9 | 7,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 10 | 8,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 11 | 9,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 12 | 10,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 13 | 11,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 14 | 12,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 15 | 13,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 16 | 14,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 17 | 15,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 18 | 16,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 19 | 17,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 20 | 18,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 21 | 19,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 22 | 20,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 23 | 21,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 24 | 22,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 25 | 23,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 26 | 24,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 27 | 25,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 28 | 26,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 29 | 27,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 30 | 28,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 31 | 29,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 32 | 30,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 33 | 31,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 34 | 32,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 35 | 33,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 36 | 34,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 37 | 35,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 38 | 36,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 39 | 37,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 40 | 38,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 41 | 39,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 42 | 40,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 43 | 41,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 44 | 42,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 45 | 43,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 46 | 44,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 47 | 45,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 48 | 46,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 49 | 47,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 50 | 48,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 51 | 49,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 52 | 50,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 53 | 51,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 54 | 52,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 55 | 53,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 56 | 54,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 57 | 55,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 58 | 56,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 59 | 57,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 60 | 58,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 61 | 59,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 62 | 60,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 63 | 61,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 64 | 62,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 65 | 63,temperature,1,1.119349044E-03,2.359019498E-04,7.926382169E-08 66 | -------------------------------------------------------------------------------- /lab_equipment/Eload_IT8500.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Itech IT8500 series eloads 2 | 3 | import pyvisa 4 | import time 5 | from .PyVisaDeviceTemplate import EloadDevice 6 | 7 | # E-Load 8 | class IT8500(EloadDevice): 9 | # Initialize the IT8500 E-Load 10 | has_remote_sense = True 11 | 12 | connection_settings = { 13 | 'baud_rate': 115200, 14 | 'write_termination': '\n', 15 | 'read_termination': '\n', 16 | 'pyvisa_backend': '@py', 17 | 'time_wait_after_open': 0, 18 | 'idn_available': True 19 | } 20 | 21 | def initialize(self): 22 | idn_split = self.inst_idn.split(',') 23 | model_number = idn_split[1] 24 | 25 | if 'IT8511A' in model_number: 26 | self.max_current = 30 27 | self.max_power = 150 28 | elif 'IT8511B' in model_number: 29 | self.max_current = 10 30 | self.max_power = 150 31 | elif 'IT8512A' in model_number: 32 | self.max_current = 30 33 | self.max_power = 300 34 | elif 'IT8512B' in model_number: 35 | self.max_current = 15 36 | self.max_power = 300 37 | elif 'IT8512C' in model_number: 38 | self.max_current = 60 39 | self.max_power = 300 40 | elif 'IT8512H' in model_number: 41 | self.max_current = 5 42 | self.max_power = 300 43 | elif 'IT8513A' in model_number: 44 | self.max_current = 60 45 | self.max_power = 400 46 | elif 'IT8513B' in model_number: 47 | self.max_current = 30 48 | self.max_power = 600 49 | elif 'IT8513C' in model_number: 50 | self.max_current = 120 51 | self.max_power = 600 52 | elif 'IT8514B' in model_number: 53 | self.max_current = 60 54 | self.max_power = 1500 55 | elif 'IT8514C' in model_number: 56 | self.max_current = 240 57 | self.max_power = 1500 58 | elif 'IT8516C' in model_number: 59 | self.max_current = 240 60 | self.max_power = 3000 61 | 62 | #resets to Constant Current Mode 63 | self.mode = "CURR" 64 | self.inst.write("*RST") 65 | self.set_current(0) 66 | #set to remote mode (disable front panel) 67 | self.lock_front_panel(True) 68 | 69 | # To Set E-Load in Amps 70 | def set_current(self, current_setpoint_A): 71 | if self.mode != "CURR": 72 | print("ERROR - E-load not in correct mode") 73 | return 74 | if current_setpoint_A < 0: 75 | current_setpoint_A = -current_setpoint_A 76 | self.inst.write("CURR {}".format(current_setpoint_A)) 77 | 78 | def set_mode_current(self): 79 | self.inst.write("FUNC CURR") 80 | self.mode = "CURR" 81 | 82 | ##COMMANDS FOR CV MODE 83 | def set_mode_voltage(self): 84 | self.inst.write("FUNC VOLT") 85 | self.mode = "VOLT" 86 | #Only 1 voltage range on this eload 87 | 88 | def set_cv_voltage(self, voltage_setpoint_V): 89 | if self.mode != "VOLT": 90 | print("ERROR - E-load not in correct mode") 91 | return 92 | self.inst.write("VOLT {}".format(voltage_setpoint_V)) 93 | 94 | ##END OF COMMANDS FOR CV MODE 95 | 96 | def toggle_output(self, state): 97 | if state: 98 | self.inst.write("INP ON") 99 | else: 100 | self.inst.write("INP OFF") 101 | 102 | def remote_sense(self, state): 103 | if state: 104 | self.inst.write("SYST:SENS ON") 105 | else: 106 | self.inst.write("SYST:SENS OFF") 107 | 108 | def lock_front_panel(self, state): 109 | if state: 110 | self.inst.write("SYST:REM") 111 | else: 112 | self.inst.write("SYST:LOC") 113 | 114 | def measure_voltage(self): 115 | return float(self.inst.query("MEAS:VOLT:DC?")) 116 | 117 | def measure_current(self): 118 | return (float(self.inst.query("MEAS:CURR:DC?")) * (-1)) 119 | 120 | def __del__(self): 121 | try: 122 | self.toggle_output(False) 123 | self.lock_front_panel(False) 124 | self.inst.close() 125 | except (AttributeError, pyvisa.errors.InvalidSession): 126 | pass -------------------------------------------------------------------------------- /PlotTemps.py: -------------------------------------------------------------------------------- 1 | #Plotting temperatures for each channel 2 | 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | import easygui as eg 6 | import os 7 | import TempChannels 8 | 9 | tc = TempChannels.TempChannels() 10 | 11 | #returns a dataframe with all entried in the directory that 12 | #fall within the start and end times in the stats directory 13 | def find_timestamp(stats, prefix, dir, file = None, tolerance = 10): 14 | 15 | check_time_low = stats['{}_start_time'.format(prefix)] - tolerance 16 | check_time_high = stats['{}_end_time'.format(prefix)] + tolerance 17 | 18 | #better way - apply upper and lower masks, check size. 19 | 20 | if(file is None): 21 | num_data_files_used = 0 22 | for file_name in os.listdir(dir): 23 | data_added = False 24 | file_path = os.path.join(dir, file_name) 25 | if os.path.isdir(file_path): 26 | #ignore other subdirectories 27 | continue 28 | if(num_data_files_used == 0): 29 | valid, df = check_file(file_path, check_time_high, check_time_low) 30 | else: 31 | valid, df_file = check_file(file_path, check_time_high, check_time_low) 32 | 33 | #add to the dataframe if we have already started one 34 | if valid: 35 | data_added = True 36 | if(num_data_files_used > 0): 37 | df = df.append(df_file) 38 | num_data_files_used += 1 39 | 40 | if((valid == False) and (num_data_files_used > 0)): 41 | #break when no new data was added and we already have data 42 | break 43 | 44 | else: 45 | file_path = os.path.join(dir, file_name) 46 | df = check_file(file_path, check_time_high, check_time_low) 47 | 48 | if df.size == 0: 49 | print("No File Found") 50 | 51 | return df 52 | 53 | #check to see if the file has any measurements with valid timestamps in them 54 | def check_file(file_path, check_high, check_low): 55 | df = pd.read_csv(file_path) 56 | 57 | valid_data = False 58 | 59 | #apply mask 60 | df = df[(df['Timestamp'] >= check_low) & (df['Timestamp'] <= check_high)] 61 | 62 | if(df.size > 0): 63 | print('File Name: {}\tDataframe Size: {}'.format(os.path.split(file_path)[-1], df.size)) 64 | valid_data = True 65 | 66 | return valid_data, df 67 | 68 | #return a dataframe with the temperature log for the discharge log 69 | #uses start and end timestamps to get the data from the temp logs 70 | #channels for each device found with the TempChannels.py cell names 71 | def get_temps(stats, prefix, log_dir): 72 | 73 | #get the filtered data from while charging or discharging 74 | df = find_timestamp(stats, prefix, log_dir) 75 | 76 | #get the channels that were used for this test 77 | channels = tc.channels[stats['cell_name']] 78 | 79 | channel_list = list() 80 | #get each channel number 81 | for label in channels: 82 | for channel_num in channels[label]: 83 | channel_list.append('Channel_C_{}'.format(channel_num)) 84 | 85 | max_temp = df[channel_list].max().max() 86 | 87 | channel_list.append('Timestamp') 88 | df = df[channel_list] 89 | 90 | print("Max Temp: {}".format(max_temp)) 91 | 92 | #now we should have just the filtered temperatures for this cell 93 | return df, max_temp 94 | 95 | #plot a dataframe that has all the temperatures in it 96 | def plot_temps(df, cell_name, separate_temps, save_filepath = '', show_graph=True, suffix = ''): 97 | if df.size == 0: 98 | return 99 | 100 | fig, ax_temps = plt.subplots() 101 | fig.set_size_inches(12,10) 102 | 103 | num_colors = len(df.columns.values.tolist())-1 104 | cm = plt.get_cmap('tab20') #this colormap has 20 different colors in it 105 | ax_temps.set_prop_cycle('color', [cm(1.*i/num_colors) for i in range(num_colors)]) 106 | 107 | 108 | #plot all of the temps 109 | for temp_name in df.columns.values.tolist(): 110 | if temp_name == 'Data_Timestamp': 111 | continue 112 | if separate_temps: 113 | channel_num = temp_name.split('_')[-1] 114 | #find the correct location 115 | location = tc.find_location(cell_name, channel_num) 116 | else: 117 | location = temp_name.split('t')[-1] 118 | #plot 119 | ax_temps.plot('Data_Timestamp', temp_name, data = df, label = location) 120 | 121 | title = 'Temperature log' 122 | if suffix != '': 123 | title += ' {}'.format(suffix) 124 | 125 | fig.suptitle(title) 126 | ax_temps.set_ylabel('Temperature (Celsius)') 127 | ax_temps.set_xlabel('Seconds from Start of Test (S)') 128 | 129 | fig.legend(loc='upper right') 130 | ax_temps.grid(b=True, axis='both') 131 | 132 | #save the file if specified 133 | if(save_filepath != ''): 134 | plt.savefig(os.path.splitext(save_filepath)[0]) 135 | 136 | if(show_graph): 137 | plt.show() 138 | else: 139 | plt.close() 140 | -------------------------------------------------------------------------------- /MSXIV_Battery_Module_Wiring_Test.py: -------------------------------------------------------------------------------- 1 | import A2D_DAQ_control as AD_control 2 | import voltage_to_temp as V2T 3 | import tkinter as tk 4 | import os 5 | from datetime import datetime 6 | import pandas as pd 7 | import time 8 | 9 | 10 | #connect to the io module 11 | daq = AD_control.A2D_DAQ() 12 | 13 | 14 | ################ CONNECTIONS ON THE BOARD ################# 15 | 16 | #cell connections 17 | c1_vpin = 16 18 | c2_vpin = 17 19 | c1_bpin = 18 20 | c2_bpin = 19 21 | input_pins = [c1_vpin, c2_vpin, c1_bpin, c2_bpin] 22 | 23 | c1_div_ratio = 2 #cell 1 has a voltage divider for 3.3V max range 24 | c2_div_ratio = 3 #cell 2 has a voltage divider to get the 3.3V analog range 25 | 26 | 27 | #thermistor connections 28 | t1_pin = 20 29 | t2_pin = 21 30 | t3_pin = 22 31 | t4_pin = 23 32 | t5_pin = 24 33 | thermistor_pins = [t1_pin, t2_pin, t3_pin, t4_pin, t5_pin] 34 | 35 | pullup_r = 3300 36 | pullup_v = 3.3 37 | 38 | #wires used for thermistors 39 | wire_length_m = 0.75 40 | wire_awg = 22 41 | 42 | 43 | ############# PIN SETUP AND READING ###################### 44 | 45 | def setup_pins(): 46 | daq.calibrate_pullup_v() 47 | pullup_v = daq.get_pullup_v() 48 | 49 | for pin in input_pins: 50 | daq.conf_io(pin, 0) #0 is input 51 | 52 | for pin in thermistor_pins: 53 | daq.conf_io(pin, 1) #1 is output 54 | daq.set_dig(pin, 1) #pull high 55 | 56 | def read_voltages(): 57 | voltages = list() 58 | 59 | daq.set_read_delay_ms(50) 60 | 61 | voltages.append(daq.get_analog_v(c1_vpin)*c1_div_ratio) 62 | voltages.append(daq.get_analog_v(c2_vpin)*c2_div_ratio-voltages[-1]) 63 | voltages.append(daq.get_analog_v(c1_bpin)*c1_div_ratio) 64 | voltages.append(daq.get_analog_v(c2_bpin)*c2_div_ratio-voltages[-1]) 65 | 66 | return voltages 67 | 68 | def read_temperatures(): 69 | daq.set_read_delay_ms(5) 70 | 71 | daq.calibrate_pullup_v() 72 | pullup_v = daq.get_pullup_v() 73 | 74 | temperatures = list() 75 | t_pins = [t1_pin, t2_pin, t3_pin, t4_pin, t5_pin] 76 | 77 | for pin in t_pins: 78 | pin_v = daq.get_analog_v(pin) 79 | print("Pin: {} Voltage: {}".format(pin, pin_v)) 80 | temperature_c = V2T.voltage_to_C(pin_v, pullup_r, pullup_v, wire_length_m = wire_length_m, wire_awg = wire_awg) 81 | temperatures.append(temperature_c) 82 | 83 | return temperatures 84 | 85 | 86 | ####################### FILE IO ########################### 87 | 88 | def get_directory(name): 89 | root = tk.Tk() 90 | root.withdraw() 91 | dir = tk.filedialog.askdirectory( 92 | title='Select location to save {} data files'.format(name)) 93 | root.destroy() 94 | return dir 95 | 96 | def write_line(filepath, list_line): 97 | #read into pandas dataframe - works, in quick to code 98 | #and is likely easy to extend - but one line doesn't really need it 99 | df = pd.DataFrame(list_line).T 100 | 101 | #save to csv - append, no index, no header 102 | df.to_csv(filepath, header=False, mode='a', index=False) 103 | 104 | def start_file(directory, name): 105 | dt = datetime.now().strftime("%Y-%m-%d %H-%M-%S") 106 | filename = '{test_name} {date}.csv'.format(\ 107 | test_name = name, date = dt) 108 | 109 | filepath = os.path.join(directory, filename) 110 | 111 | #Headers 112 | #create a list 113 | headers = list() 114 | headers.append('Timestamp') 115 | headers.append('Module_Number') 116 | headers.append('Cell_1_Voltage-V') 117 | headers.append('Cell_2_Voltage-V') 118 | headers.append('Cell_1_Balance-V') 119 | headers.append('Cell_2_Balance-V') 120 | headers.append('Temperature_1-degC') 121 | headers.append('Temperature_2-degC') 122 | headers.append('Temperature_3-degC') 123 | headers.append('Temperature_4-degC') 124 | headers.append('Temperature_5-degC') 125 | 126 | write_line(filepath, headers) 127 | 128 | return filepath 129 | 130 | def write_data(filepath, test_data, printout=False): 131 | data = list() 132 | 133 | #add timestamp 134 | data.append(time.time()) 135 | data.extend(test_data) 136 | 137 | if(printout): 138 | print(data) 139 | 140 | write_line(filepath, data) 141 | 142 | 143 | ######################## GATHER DATA ####################### 144 | 145 | def gather_data(): 146 | data = list() 147 | 148 | #get the module number 149 | module_number = input("Enter the module number: ") 150 | data.append(module_number) 151 | 152 | #read the data 153 | data.extend(read_voltages()) 154 | data.extend(read_temperatures()) 155 | 156 | return data 157 | 158 | 159 | ######################### MAIN PROGRAM ####################### 160 | 161 | dir = get_directory("MSXIV Battery Module Wiring Test") 162 | filepath = start_file(dir, "Module_Manufacturing_Test") 163 | setup_pins() 164 | 165 | response = "Y" 166 | 167 | while response == "Y": 168 | data = gather_data() 169 | write_data(filepath, data, printout=True) 170 | response = input("\n\nDo you want to test another module (Y or N)?\n").upper() 171 | 172 | 173 | -------------------------------------------------------------------------------- /lab_equipment/A2D_DAQ_control.py: -------------------------------------------------------------------------------- 1 | #python library for controlling the A2D DAQ 2 | 3 | import pyvisa 4 | import easygui as eg 5 | import voltage_to_temp as V2T 6 | from . import A2D_DAQ_config 7 | from .PyVisaDeviceTemplate import PyVisaDevice 8 | 9 | #Data Acquisition Unit 10 | class A2D_DAQ(PyVisaDevice): 11 | num_channels = 64 12 | start_channel = 0 13 | connection_settings = { 14 | 'read_termination': '\r\n', 15 | 'write_termination': '\n', 16 | 'baud_rate': 57600, 17 | 'query_delay': 0.02, 18 | 'chunk_size': 102400, 19 | 'pyvisa_backend': '@py', 20 | 'time_wait_after_open': 2, 21 | 'idn_available': True 22 | } 23 | 24 | #initialization specific to this instrument 25 | def initialize(self): 26 | self.pull_up_r = 3300 27 | self.pullup_voltage = 3.3 28 | self.pull_up_cal_ch = 63 29 | self.config_dict = {} 30 | 31 | def __del__(self): 32 | try: 33 | self.inst.close() 34 | except AttributeError: 35 | pass 36 | 37 | def configure_from_dict(self): 38 | #Go through each channel and set it up according to the dict 39 | for ch in list(self.config_dict.keys()): 40 | if self.config_dict[ch]['Input_Type'] == 'voltage': 41 | self.conf_io(ch, 0) #input 42 | elif self.config_dict[ch]['Input_Type'] == 'temperature': 43 | self.conf_io(ch, 1) #output 44 | self.set_dig(ch, 1) #pull high 45 | elif self.config_dict[ch]['Input_Type'] == 'dig_in': 46 | self.conf_io(ch, 0) #input 47 | elif self.config_dict[ch]['Input_Type'] == 'dig_out': 48 | self.conf_io(ch, 1) #output 49 | self.set_dig(ch, 0) #pull low 50 | 51 | 52 | def reset(self): 53 | self.inst.write('*RST') 54 | 55 | def conf_io(self, channel = 0, dir = 1): 56 | #1 means output - pin is being driven 57 | if dir: 58 | self.inst.write('CONF:DAQ:OUTP (@{ch})'.format(ch = channel)) 59 | #0 means input - pin high impedance 60 | elif not dir: 61 | self.inst.write('CONF:DAQ:INP (@{ch})'.format(ch = channel)) 62 | 63 | def get_pullup_v(self): 64 | return self.pullup_voltage 65 | 66 | def calibrate_pullup_v(self, cal_ch = None): 67 | if cal_ch == None: 68 | cal_ch = self.pull_up_cal_ch 69 | 70 | #choose a channel to read from - this channel should have nothing on it 71 | if cal_ch < self.num_channels: 72 | #ensure channel is set to output and pullued high 73 | self.conf_io(channel = cal_ch, dir = 1) 74 | self.set_dig(channel = cal_ch, value = 1) 75 | 76 | self.pullup_voltage = float(self.measure_voltage(channel = cal_ch)) 77 | 78 | def get_analog_mv(self, channel = 0): 79 | scaling = 1 80 | 81 | #print(f"Measuring channel {channel}") 82 | 83 | if self.config_dict[channel]['Input_Type'] == 'voltage': 84 | scaling = self.config_dict[channel]['Voltage_Scaling'] 85 | 86 | raw_value = self.inst.query('INSTR:READ:ANA? (@{ch})'.format(ch = channel)) 87 | #print(f"Query raw value: {raw_value}") 88 | return_value = float(raw_value)*scaling 89 | #print(f"Query return value: {return_value}") 90 | return return_value 91 | 92 | def get_analog_v(self, channel = 0): 93 | return float(self.get_analog_mv(channel))/1000.0 94 | 95 | def measure_voltage(self, channel = 0): 96 | return float(self.get_analog_v(channel)) 97 | 98 | def measure_temperature(self, channel = 0): 99 | sh_consts = {'SH_A': self.config_dict[channel]['Temp_A'], 100 | 'SH_B': self.config_dict[channel]['Temp_B'], 101 | 'SH_C': self.config_dict[channel]['Temp_C']} 102 | return V2T.voltage_to_C(self.measure_voltage(channel), self.pull_up_r, self.pullup_voltage, sh_constants = sh_consts) 103 | 104 | def get_dig_in(self, channel = 0): 105 | return self.inst.query('INSTR:READ:DIG? (@{ch})'.format(ch = channel)) 106 | 107 | def set_dig(self, channel = 0, value = 0): 108 | if(value > 1): 109 | value = 1 110 | self.inst.write('INSTR:DAQ:SET:OUTP (@{ch}),{val}'.format(ch = channel, val = value)) 111 | 112 | def set_led(self, value = 0): 113 | if(value > 1): 114 | value = 1 115 | #x is a character that we parse but do nothing with (channel must be first) 116 | self.inst.write('INSTR:DAQ:SET:LED x {val}'.format(val = value)) 117 | 118 | def set_read_delay_ms(self, delay_ms): 119 | #x is a character that we parse but do nothing with (channel must be first) 120 | self.inst.write('CONF:DAQ:READDEL x {val}'.format(val = delay_ms)) 121 | 122 | def __del__(self): 123 | try: 124 | self.inst.close() 125 | except (AttributeError, pyvisa.errors.InvalidSession): 126 | pass 127 | -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/ads1219.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # Copyright (c) 2019 Mike Teachman 3 | # https://opensource.org/licenses/MIT 4 | 5 | # MicroPython driver for the Texas Instruments ADS1219 ADC 6 | 7 | from micropython import const 8 | import adafruit_bus_device.i2c_device as i2cdevice 9 | from adafruit_register.i2c_struct import ROUnaryStruct, UnaryStruct 10 | from adafruit_register.i2c_bits import ROBits, RWBits 11 | from adafruit_register.i2c_bit import ROBit, RWBit 12 | 13 | try: 14 | import struct 15 | except ImportError: 16 | import ustruct as struct 17 | 18 | import time 19 | 20 | _CHANNEL_MASK = const(0b11100000) 21 | _GAIN_MASK = const(0b00010000) 22 | _DR_MASK = const(0b00001100) 23 | _CM_MASK = const(0b00000010) 24 | _VREF_MASK = const(0b00000001) 25 | 26 | _COMMAND_RESET = const(0b00000110) 27 | _COMMAND_START_SYNC = const(0b00001000) 28 | _COMMAND_POWERDOWN = const(0b00000010) 29 | _COMMAND_RDATA = const(0b00010000) 30 | 31 | _COMMAND_RREG_CONFIG = const(0b00100000) 32 | _COMMAND_RREG_STATUS = const(0b00100100) 33 | _COMMAND_WREG_CONFIG = const(0b01000000) 34 | 35 | _DRDY_MASK = const(0b10000000) 36 | _DRDY_NO_NEW_RESULT = const(0b00000000) # No new conversion result available 37 | _DRDY_NEW_RESULT_READY = const(0b10000000) # New conversion result ready 38 | 39 | class ADS1219_MUX: 40 | P_AIN0_N_AIN1 = 0 41 | P_AIN2_N_AIN3 = 1 42 | P_AIN1_N_AIN2 = 2 43 | AIN0 = 3 44 | AIN1 = 4 45 | AIN2 = 5 46 | AIN3 = 6 47 | P_N_AVDD_2 = 7 48 | 49 | class ADS1219_GAIN: 50 | GAIN_1 = 0 51 | GAIN_4 = 1 52 | 53 | class ADS1219_DATA_RATE: 54 | DR_20_SPS = 0 55 | DR_90_SPS = 1 56 | DR_330_SPS = 2 57 | DR_1000_SPS = 3 58 | 59 | class ADS1219_CONV_MODE: 60 | CM_SINGLE = 0 61 | CM_CONTINUOUS = 1 62 | 63 | class ADS1219_VREF: 64 | VREF_INTERNAL = 0 65 | VREF_EXTERNAL = 1 66 | 67 | 68 | 69 | class ADS1219: 70 | 71 | VREF_INTERNAL_MV = 2048 # Internal reference voltage = 2048 mV 72 | POSITIVE_CODE_RANGE = 0x7FFFFF # 23 bits of positive range 73 | 74 | def __init__(self, i2c, address=0x40): 75 | self.i2c_device = i2cdevice.I2CDevice(i2c, address) 76 | 77 | self.MUX = ADS1219_MUX.P_AIN0_N_AIN1 78 | self.GAIN = ADS1219_GAIN.GAIN_1 79 | self.DR = ADS1219_DATA_RATE.DR_20_SPS 80 | self.CM = ADS1219_CONV_MODE.CM_SINGLE 81 | self.VREF = ADS1219_VREF.VREF_INTERNAL 82 | self.config_byte = self._calc_config_byte() 83 | 84 | self.reset() 85 | 86 | def _calc_config_byte(self): 87 | #cfg_byte = bytearray(1) 88 | return ((self.MUX << 5) | (self.GAIN << 4) | (self.DR << 2) | (self.CM << 1) | (self.VREF)) 89 | 90 | #print("cfg_byte: {}\tMUX: {}\tGAIN: {}\tDR: {}\tCM: {}\tVREF: {}".format(bin(cfg_byte), bin(self.MUX), bin(self.GAIN), bin(self.DR), bin(self.CM), bin(self.VREF))) 91 | 92 | def _update_config(self): 93 | new_config_byte = self._calc_config_byte() 94 | 95 | if new_config_byte != self.config_byte: 96 | self.config_byte = new_config_byte 97 | data = bytearray([_COMMAND_WREG_CONFIG, self.config_byte]) 98 | with self.i2c_device: 99 | self.i2c_device.write(data) 100 | 101 | def read_config(self): 102 | rreg = struct.pack('B', _COMMAND_RREG_CONFIG) 103 | #self._i2c.writeto(self._address, rreg) 104 | with self.i2c_device: 105 | self.i2c_device.write(rreg) 106 | config = bytearray(1) 107 | #self._i2c.readfrom_into(self._address, config) 108 | self.i2c_device.readinto(config) 109 | return config[0] 110 | 111 | def read_status(self): 112 | rreg = struct.pack('B', _COMMAND_RREG_STATUS) 113 | #self._i2c.writeto(self._address, rreg) 114 | with self.i2c_device: 115 | self.i2c_device.write(rreg) 116 | status = bytearray(1) 117 | #self._i2c.readfrom_into(self._address, status) 118 | self.i2c_device.readinto(status) 119 | return status[0] 120 | 121 | def set_channel(self, channel): 122 | self.MUX = channel 123 | self._update_config() 124 | 125 | def set_gain(self, gain): 126 | self.GAIN = gain 127 | self._update_config() 128 | 129 | def set_data_rate(self, dr): 130 | self.DR = dr 131 | self._update_config() 132 | 133 | def set_conversion_mode(self, cm): 134 | self.CM = cm 135 | self._update_config() 136 | 137 | def set_vref(self, vref): 138 | self.VREF = vref 139 | self._update_config() 140 | 141 | def read_data(self): 142 | if self.CM == ADS1219_CONV_MODE.CM_SINGLE: 143 | self.start_sync() 144 | # loop until conversion is completed 145 | while((self.read_status() & _DRDY_MASK) == _DRDY_NO_NEW_RESULT): 146 | time.sleep(100.0/1000/1000) 147 | 148 | rreg = struct.pack('B', _COMMAND_RDATA) 149 | #self._i2c.writeto(self._address, rreg) 150 | with self.i2c_device: 151 | self.i2c_device.write(rreg) 152 | data = bytearray(3) 153 | #self._i2c.readfrom_into(self._address, data) 154 | self.i2c_device.readinto(data) 155 | return struct.unpack('>I', b'\x00' + data)[0] 156 | 157 | def read_data_irq(self): 158 | rreg = struct.pack('B', _COMMAND_RDATA) 159 | #self._i2c.writeto(self._address, rreg) 160 | with self.i2c_device: 161 | self.i2c_device.write(rreg) 162 | data = bytearray(3) 163 | #self._i2c.readfrom_into(self._address, data) 164 | self.i2c_device.readinto(data) 165 | return struct.unpack('>I', b'\x00' + data)[0] 166 | 167 | def reset(self): 168 | data = struct.pack('B', _COMMAND_RESET) 169 | #self._i2c.writeto(self._address, data) 170 | with self.i2c_device: 171 | self.i2c_device.write(data) 172 | self.reset_local_conf_copies() 173 | 174 | def start_sync(self): 175 | data = struct.pack('B', _COMMAND_START_SYNC) 176 | #self._i2c.writeto(self._address, data) 177 | with self.i2c_device: 178 | self.i2c_device.write(data) 179 | 180 | def powerdown(self): 181 | data = struct.pack('B', _COMMAND_POWERDOWN) 182 | #self._i2c.writeto(self._address, data) 183 | with self.i2c_device: 184 | self._i2c.writeto(self._address, data) 185 | 186 | def reset_local_conf_copies(self): 187 | self.MUX = ADS1219_MUX.P_AIN0_N_AIN1 188 | self.GAIN = ADS1219_GAIN.GAIN_1 189 | self.DR = ADS1219_DATA_RATE.DR_20_SPS 190 | self.CM = ADS1219_CONV_MODE.CM_SINGLE 191 | self.VREF = ADS1219_VREF.VREF_INTERNAL 192 | self.config_byte = self._calc_config_byte() -------------------------------------------------------------------------------- /BATT_HIL/ADS1219/README.md: -------------------------------------------------------------------------------- 1 | # MicroPython Driver for Texas Instruments ADS1219 Analog to Digital Converter (ADC) 2 | The ADS1219 is a precision, 4-channel, 24-bit, analog-to-digital converter (ADC) with I2C interface 3 | *** 4 | **Example usage: single-shot conversion** 5 | 6 | ``` 7 | from machine import Pin 8 | from machine import I2C 9 | from ads1219 import ADS1219 10 | import utime 11 | 12 | i2c = I2C(scl=Pin(26), sda=Pin(27)) 13 | adc = ADS1219(i2c) 14 | 15 | adc.set_channel(ADS1219.CHANNEL_AIN0) 16 | adc.set_conversion_mode(ADS1219.CM_SINGLE) 17 | adc.set_gain(ADS1219.GAIN_1X) 18 | adc.set_data_rate(ADS1219.DR_20_SPS) # 20 SPS is the most accurate 19 | adc.set_vref(ADS1219.VREF_INTERNAL) 20 | 21 | while True: 22 | result = adc.read_data() 23 | print('result = {}, mV = {}'.format(result, 24 | result * ADS1219.VREF_INTERNAL_MV / ADS1219.POSITIVE_CODE_RANGE)) 25 | utime.sleep(0.5) 26 | ``` 27 | 28 | **Example usage: continuous conversion with interrupt** 29 | 30 | ``` 31 | from machine import Pin 32 | from machine import I2C 33 | from ads1219 import ADS1219 34 | import utime 35 | 36 | def isr_callback(arg): 37 | global irq_count 38 | result = adc.read_data_irq() 39 | print('result = {}, mV = {:.2f}'.format( 40 | result, result * ADS1219.VREF_INTERNAL_MV / ADS1219.POSITIVE_CODE_RANGE)) 41 | irq_count += 1 42 | 43 | i2c = I2C(scl=Pin(26), sda=Pin(27)) 44 | adc = ADS1219(i2c) 45 | adc.set_channel(ADS1219.CHANNEL_AIN1) 46 | adc.set_conversion_mode(ADS1219.CM_CONTINUOUS) 47 | adc.set_gain(ADS1219.GAIN_1X) 48 | adc.set_data_rate(ADS1219.DR_20_SPS) 49 | adc.set_vref(ADS1219.VREF_INTERNAL) 50 | drdy_pin = Pin(34, mode=Pin.IN) 51 | adc.start_sync() # starts continuous sampling 52 | irq_count = 0 53 | 54 | # enable interrupts 55 | print("enabling DRDY interrupt") 56 | irq = drdy_pin.irq(trigger=Pin.IRQ_FALLING, handler=isr_callback) 57 | 58 | # from this point onwards the ADS1219 will pull the DRDY pin 59 | # low whenever an ADC conversion has completed. The ESP32 60 | # will detect this falling edge on the GPIO pin (pin 34 in this 61 | # example) which will cause the isr_callback() routine to run. 62 | 63 | # The ESP32 will continue to process interrupts and call 64 | # isr_callback() during the following one second of sleep time. 65 | # The ADS1219 is configured for 20 conversions every second, so 66 | # the ISR will be called 20x during this second of sleep time. 67 | utime.sleep(1) 68 | # disable interrupt by specifying handler=None 69 | irq = drdy_pin.irq(handler=None) 70 | print('irq_count =', irq_count) 71 | ``` 72 | *** 73 | # class ADS1219 74 | ## Constructor 75 | ``` 76 | class ads1219.ADS1219(i2c, [address = 0x040]), 77 | ``` 78 | Construct and return a new ADS1219 object with the given arguments: 79 | * **i2c** specifies I2C bus instance 80 | * **address** device address (default: 0x40) 81 | 82 | Defaults after initialization: 83 | * channel = CHANNEL\_AIN0\_AIN1 84 | * gain = 1 85 | * data rate = 20 SPS 86 | * conversion mode = single-shot 87 | * voltage reference = internal 2.048V 88 | 89 | ## Methods 90 | ``` 91 | ADS1219.read_config() 92 | ``` 93 | Read the contents of the 8-bit Configuration Register 94 | 95 | *** 96 | ``` 97 | ADS1219.read_status() 98 | ``` 99 | Read the contents of the 8-bit Status Register 100 | 101 | *** 102 | ``` 103 | ADS1219.set_channel(channel) 104 | ``` 105 | *** 106 | ``` 107 | ADS1219.set_gain(gain) 108 | ``` 109 | *** 110 | ``` 111 | ADS1219.set_data_rate(data_rate) 112 | ``` 113 | *** 114 | ``` 115 | ADS1219.set_conversion_mode(conversion_mode) 116 | ``` 117 | *** 118 | ``` 119 | ADS1219.set_vref(voltage_reference) 120 | ``` 121 | *** 122 | ``` 123 | ADS1219.read_data() 124 | ``` 125 | Read the most recent conversion result 126 | *** 127 | ``` 128 | ADS1219.reset() 129 | ``` 130 | Resets the device to the default states 131 | *** 132 | ``` 133 | ADS1219.start_sync() 134 | ``` 135 | Starts a conversion. start\_sync() must be called to start continuous conversion mode. Not needed for single-shot conversion 136 | (the read\_data() method includes a start\_sync() call for single-shot mode) 137 | *** 138 | ``` 139 | ADS1219.powerdown() 140 | ``` 141 | Places the device into power-down mode 142 | *** 143 | 144 | ## Constants 145 | 146 | **channel(s)** being sampled 147 | 148 | ``` 149 | CHANNEL_AIN0_AIN1 150 | CHANNEL_AIN2_AIN3 151 | CHANNEL_AIN1_AIN2 152 | CHANNEL_AIN0 153 | CHANNEL_AIN1 154 | CHANNEL_AIN2 155 | CHANNEL_AIN3 156 | CHANNEL_MID_AVDD 157 | ``` 158 | *** 159 | 160 | **gain** 161 | 162 | ``` 163 | GAIN_1X, GAIN_4X 164 | ``` 165 | *** 166 | 167 | **data_rate** 168 | 169 | ``` 170 | DR_20_SPS, DR_90_SPS, DR_330_SPS, DR_1000_SPS 171 | ``` 172 | *** 173 | 174 | **conversion_mode** 175 | 176 | ``` 177 | CM_SINGLE, CM_CONTINUOUS 178 | ``` 179 | *** 180 | 181 | **voltage_reference** 182 | 183 | ``` 184 | VREF_INTERNAL, VREF_EXTERNAL 185 | ``` 186 | ## Making a breakout board 187 | The ADS1219 device is available in a TSSOP-16 package which can be soldered onto a compatible breakout board. Here is a photo showing the device soldered into an Adafruit TSSOP-16 breakout board. 188 | 189 | ![ADS1219 Breakout Board](images/ads1219-breakout.jpg) 190 | 191 | [Adafruit TSSOP-16 breakout board](https://www.adafruit.com/product/1207) 192 | 193 | The ADS1219 device can be purchased from a supplier such as Digikey. In single quantities each part costs around USD $6.50. Make sure to purchase the TSSOP-16 package and not the WQFN-16 package (which is more difficult to hand solder). 194 | 195 | [ADS1219 in TSSOP-16 package](https://www.digikey.com/product-detail/en/texas-instruments/ADS1219IPWR/296-50884-1-ND/9743261) 196 | 197 | ## How to achieve optimum performance 198 | Using this ADC with a breakout board offers a quick way to start code development. But, a simple breakout board does not allow the device to realize its specified performance. Optimum ADC performance is obtained by following the manufacturer's recommended practises for layout and circuit design. For example, bypass capacitors should be located as close as possible the analog and digital power supply pins. Achieving a high level of performance involves creating a custom circuit board that follows best practises for mixed analog/digital designs. 199 | -------------------------------------------------------------------------------- /jsonIO.py: -------------------------------------------------------------------------------- 1 | #importing and exporting settings to json files 2 | import easygui as eg 3 | from os import path 4 | import json 5 | import pandas as pd 6 | 7 | ##################### Checking User Input ############## 8 | def check_user_entry(keys, entries, valid_strings): 9 | if(entries == None): 10 | return False 11 | 12 | entry_dict = dict(zip(keys, entries)) 13 | 14 | for key in entry_dict: 15 | if not is_entry_valid(key, entry_dict[key], valid_strings): 16 | return False 17 | 18 | return True 19 | 20 | def is_entry_valid(key, value, valid_strings): 21 | try: 22 | float(value) 23 | return True 24 | except ValueError: 25 | try: 26 | if value in valid_strings[key]: 27 | return True 28 | except (AttributeError, KeyError): 29 | return False 30 | return False 31 | return False 32 | 33 | def convert_to_float(settings): 34 | #if possible, convert items to floats 35 | for key in settings: 36 | try: 37 | settings[key] = float(settings[key]) 38 | except ValueError: 39 | pass 40 | return settings 41 | 42 | def convert_keys_to_int(settings): 43 | settings = {int(k):v for k,v in settings.items()} 44 | return settings 45 | 46 | def force_extension(filename, extension): 47 | #Checking the file type 48 | file_root, file_extension = path.splitext(filename) 49 | if(file_extension != extension): 50 | file_extension = extension 51 | file_name = file_root + file_extension 52 | return file_name 53 | 54 | def get_extension(filename): 55 | name, ext = path.splitext(filename) 56 | return ext 57 | 58 | 59 | ####################### Getting Input from User ####################### 60 | 61 | def update_settings(settings, new_value_list): 62 | #assuming an ordered response list 63 | #a bit hacky but it should work 64 | index = 0 65 | for key in settings.keys(): 66 | settings.update({key: new_value_list[index]}) 67 | index += 1 68 | 69 | return settings 70 | 71 | def export_cycle_settings(settings, cycle_name = "", file_name = None): 72 | #add extra space to get formatting correct 73 | if (cycle_name != ""): 74 | cycle_name += " " 75 | 76 | write_mode = "a" 77 | if file_name == None: 78 | write_mode = "w" 79 | #get the file to export to 80 | file_name = eg.filesavebox(msg = "Choose a File to export {}settings to".format(cycle_name), 81 | title = "Settings", filetypes = ['*.json', 'JSON files']) 82 | 83 | if file_name == None: 84 | return None 85 | 86 | #force file name extension 87 | file_name = force_extension(file_name, '.json') 88 | 89 | #export the file 90 | with open(file_name, write_mode) as write_file: 91 | json.dump(settings, write_file, indent = 4) 92 | 93 | return file_name 94 | 95 | def import_multi_step_from_csv(): 96 | file_name = eg.fileopenbox(msg = "Choose a File to import step settings from", 97 | title = "Settings", filetypes = ['*.csv', 'CSV files']) 98 | 99 | if file_name == None: 100 | return None 101 | 102 | df = pd.read_csv(file_name) 103 | settings_list = df.to_dict(orient = 'records') 104 | return settings_list 105 | 106 | def import_cycle_settings(cycle_name = "", queue = None, ch_num = None): 107 | #add extra space to get formatting correct 108 | if (cycle_name != ""): 109 | cycle_name += " " 110 | 111 | #get the file to import from 112 | file_name = eg.fileopenbox(msg = "Choose a File to import {}settings from".format(cycle_name), 113 | title = "Settings", filetypes = [['*.json', 'JSON files'],['*.csv', 'CSV files']]) 114 | 115 | if file_name == None: 116 | return None 117 | 118 | settings = None 119 | 120 | #import the file 121 | if(file_name != None): 122 | #determine the file type - JSON or CSV 123 | extension = get_extension(file_name) 124 | if extension == '.json': 125 | with open(file_name, "r") as read_file: 126 | settings = json.load(read_file) 127 | elif extension == '.csv': 128 | df = pd.read_csv(file_name) 129 | settings = df.to_dict(orient = 'records')[0] 130 | 131 | if queue != None: 132 | settings = {'ch_num': ch_num, 'cdc_input_dict': settings} 133 | queue.put_nowait(settings) 134 | else: 135 | return settings 136 | 137 | def get_cycle_settings(settings, valid_strings = None, cycle_name = ""): 138 | 139 | if(cycle_name != ""): 140 | cycle_name += " " 141 | 142 | response = eg.buttonbox(msg = "Would you like to import settings for {}cycle or create new settings?".format(cycle_name), 143 | title = "Settings for {}cycle".format(cycle_name), choices = ("New Settings", "Import Settings")) 144 | if response == None: 145 | return None 146 | 147 | elif response == "New Settings": 148 | valid_entries = False 149 | while not valid_entries: 150 | response_list = eg.multenterbox(msg = "Enter Info for {}cycle".format(cycle_name), title = response, 151 | fields = list(settings.keys()), values = list(settings.values())) 152 | if response_list == None: 153 | return None 154 | valid_entries = check_user_entry(list(settings.keys()), response_list, valid_strings) 155 | 156 | #update dict entries with the response - can't use the dict.update since we only have a list here. 157 | settings = update_settings(settings, response_list) 158 | 159 | if (eg.ynbox(msg = "Would you like to save these settings for future use?", title = "Save Settings")): 160 | export_cycle_settings(settings, cycle_name) 161 | elif response == "Import Settings": 162 | settings = import_cycle_settings(cycle_name) 163 | 164 | settings = convert_to_float(settings) 165 | 166 | return settings 167 | -------------------------------------------------------------------------------- /dc_dc_test.py: -------------------------------------------------------------------------------- 1 | #Purpose: Automated Testing of DC-DC Converter Efficiency curves 2 | # at different load currents and input voltages 3 | #Written By: Micah Black 4 | #Date 5 | 6 | import equipment as eq 7 | import time 8 | import easygui as eg 9 | import numpy as np 10 | import FileIO 11 | import Templates 12 | import jsonIO 13 | 14 | 15 | 16 | #sweep the load current of an E-load from X to Y A in increments and log to CSV 17 | 18 | res_ids_dict = {} 19 | psu = eq.powerSupplies.choose_psu() 20 | res_ids_dict['psu'] = eq.get_res_id_dict_and_disconnect(psu) 21 | eload = eq.eLoads.choose_eload() 22 | res_ids_dict['eload'] = eq.get_res_id_dict_and_disconnect(eload) 23 | #Input Voltage Measurement 24 | msg = "Do you want to use a separate device to measure input voltage?" 25 | title = "Input Voltage Measurement Device" 26 | use_dmm_vin = False 27 | if eg.ynbox(msg, title): 28 | dmm_v_in = eq.dmms.choose_dmm() 29 | res_ids_dict['dmm_v_in'] = eq.get_res_id_dict_and_disconnect(dmm_v_in) 30 | use_dmm_vin = True 31 | #Output Voltage Measurement 32 | msg = "Do you want to use a separate device to measure output voltage?" 33 | title = "Output Voltage Measurement Device" 34 | use_dmm_vout = False 35 | if eg.ynbox(msg, title): 36 | dmm_v_out = eq.dmms.choose_dmm() 37 | res_ids_dict['dmm_v_out'] = eq.get_res_id_dict_and_disconnect(dmm_v_out) 38 | use_dmm_vout = True 39 | 40 | eq_dict = eq.get_equipment_dict(res_ids_dict) 41 | psu = eq_dict['psu'] 42 | eload = eq_dict['eload'] 43 | 44 | vmeas_in_eq = psu 45 | if use_dmm_vin: 46 | vmeas_in_eq = eq_dict['dmm_v_in'] 47 | vmeas_out_eq = eload 48 | if use_dmm_vout: 49 | vmeas_out_eq = eq_dict['dmm_v_out'] 50 | 51 | def init_instruments(): 52 | test1_v = vmeas_in_eq.measure_voltage() 53 | test2_v = vmeas_out_eq.measure_voltage() 54 | 55 | def remove_extreme_values(list_to_remove, num_to_remove): 56 | for i in range(int(num_to_remove)): 57 | list_to_remove.remove(max(list_to_remove)) 58 | list_to_remove.remove(min(list_to_remove)) 59 | return list_to_remove 60 | 61 | def gather_data(samples_to_avg): 62 | data = dict() 63 | 64 | input_voltage = list() 65 | input_current = list() 66 | output_voltage = list() 67 | output_current = list() 68 | 69 | for i in range(int(samples_to_avg)): 70 | #The current and voltage measurements are not simultaneous 71 | #so we technically are not measuring true power 72 | #we will average each term individually as the load conditions are not 73 | #changing and any switching noise, mains noise, etc. will be averaged out (hopefully) 74 | time.sleep(0.01) 75 | input_voltage.append(vmeas_in_eq.measure_voltage()) 76 | input_current.append(psu.measure_current()) 77 | output_voltage.append(vmeas_out_eq.measure_voltage()) 78 | output_current.append(eload.measure_current()) 79 | 80 | #discard top and bottom measurements 81 | #average and save the rest of the measurements 82 | #need to be careful of number of samples taken 83 | 84 | #top and bottom amount to remove: 85 | remove_num = int(np.log(samples_to_avg)) 86 | 87 | input_voltage = remove_extreme_values(input_voltage, remove_num) 88 | input_current = remove_extreme_values(input_current, remove_num) 89 | output_voltage = remove_extreme_values(output_voltage, remove_num) 90 | output_current = remove_extreme_values(output_current, remove_num) 91 | 92 | #compute average of what's left (non-outliers) 93 | iv = sum(input_voltage) / len(input_voltage) 94 | ic = sum(input_current) / len(input_current) 95 | ov = sum(output_voltage) / len(output_voltage) 96 | oc = sum(output_current) / len(output_current) 97 | 98 | data['v_in']=(iv) 99 | data['i_in']=(ic) 100 | data['v_out']=(ov) 101 | data['i_out']=(oc) 102 | 103 | return data 104 | 105 | def sweep_load_current(filepath, test_name, settings): 106 | 107 | psu.set_voltage(settings["psu_voltage"]) 108 | time.sleep(0.1) 109 | psu.set_current(settings["psu_current_limit_a"]) 110 | 111 | for current in np.linspace(settings["load_current_min"], 112 | settings["load_current_max"], 113 | int(settings["num_current_steps"])): 114 | eload.set_current(current) 115 | time.sleep(settings["step_delay_s"]) 116 | data = gather_data(settings["measurement_samples_for_avg"]) 117 | data["v_in_set"] = settings["psu_voltage"] 118 | FileIO.write_data(filepath, data, printout = True) 119 | 120 | 121 | ################# MAIN PROGRAM ################## 122 | if __name__ == '__main__': 123 | directory = FileIO.get_directory("DC-DC-Test") 124 | test_name = eg.enterbox(title = "Test Setup", msg = "Enter the Test Name\n(Spaces will be replaced with underscores)", 125 | default = "TEST_NAME", strip = True) 126 | test_name = test_name.replace(" ", "_") 127 | 128 | test_settings = Templates.DcdcTestSettings() 129 | test_settings = test_settings.settings 130 | test_settings = jsonIO.get_cycle_settings(test_settings, cycle_name = test_name) 131 | 132 | #generate a list of sweep settings - changing voltage for each sweep 133 | voltage_list = np.linspace(test_settings["psu_voltage_min"], 134 | test_settings["psu_voltage_max"], 135 | int(test_settings["num_voltage_steps"])) 136 | 137 | sweep_settings_list = list() 138 | for voltage in voltage_list: 139 | sweep_settings = Templates.DcdcSweepSettings() 140 | sweep_settings = sweep_settings.settings 141 | sweep_settings["psu_voltage"] = voltage 142 | sweep_settings["psu_current_limit_a"] = test_settings["psu_current_limit_a"] 143 | sweep_settings["load_current_min"] = test_settings["load_current_min"] 144 | sweep_settings["load_current_max"] = test_settings["load_current_max"] 145 | sweep_settings["num_current_steps"] = test_settings["num_current_steps"] 146 | sweep_settings["step_delay_s"] = test_settings["step_delay_s"] 147 | sweep_settings["measurement_samples_for_avg"] = test_settings["measurement_samples_for_avg"] 148 | sweep_settings_list.append(sweep_settings) 149 | 150 | filepath = FileIO.start_file(directory, test_name) 151 | 152 | #Turn on power supply and eload to get the converter started up 153 | init_instruments() 154 | psu.set_voltage(test_settings["psu_voltage_min"]) 155 | time.sleep(0.05) 156 | psu.set_current(test_settings["psu_current_limit_a"]) 157 | eload.set_current(0) 158 | time.sleep(0.05) 159 | psu.toggle_output(True) 160 | eload.toggle_output(True) 161 | 162 | #run through each of the generated settings 163 | for sweep_settings in sweep_settings_list: 164 | sweep_load_current(filepath, test_name, sweep_settings) 165 | 166 | #Turn off power supply and eload 167 | eload.set_current(0) 168 | eload.toggle_output(False) 169 | psu.toggle_output(False) 170 | -------------------------------------------------------------------------------- /lab_equipment/Eload_A2D_Eload.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling A2D Eload 2 | 3 | import pyvisa 4 | import time 5 | from .PyVisaDeviceTemplate import EloadDevice 6 | 7 | # E-Load 8 | class A2D_Eload(EloadDevice): 9 | max_channels = 32 #max 32 eloads connected on the RS485 bus 10 | has_remote_sense = False 11 | connection_settings = { 12 | 'read_termination': '\r\n', 13 | 'write_termination': '\n', 14 | 'baud_rate': 115200, 15 | 'query_delay': 0.005, 16 | 'command_delay': 0.005, #wait between commands sent to instrument 17 | 'time_wait_after_open': 0, 18 | 'chunk_size': 102400, 19 | 'pyvisa_backend': '@py', 20 | 'idn_available': True 21 | } 22 | 23 | # Initialize the A2D ELoad 24 | def initialize(self): 25 | idn_split = self.inst_idn.split(',') 26 | self.manufacturer = idn_split[0] 27 | self.model_number = idn_split[1] 28 | self.serial_number = idn_split[2] 29 | self.firmware_version = idn_split[3] 30 | 31 | if 'Eload' in self.model_number: 32 | self.max_current = 10.0 33 | self.current_setpoint = 0.0 34 | 35 | self._last_command_time = time.perf_counter() 36 | 37 | self.reset() 38 | self.set_current(0) 39 | 40 | def reset(self): 41 | self._inst_write("*RST") 42 | self.current_setpoint = 0.0 43 | time.sleep(2.0) 44 | 45 | def kick(self, channel = 1): 46 | self._inst_write(f"INSTR:KICK {channel}") 47 | 48 | # To Set E-Load in Amps 49 | def set_current(self, current_setpoint_A, channel = 1): 50 | if current_setpoint_A < 0: 51 | current_setpoint_A = abs(current_setpoint_A) 52 | self._inst_write(f"CURR {channel},{current_setpoint_A}") 53 | self.current_setpoint = current_setpoint_A 54 | 55 | def toggle_output(self, state, channel = 1): 56 | if state: 57 | self._inst_write(f"INSTR:RELAY {channel},1") 58 | #This device sets the current to 0 before turning the relay ON to avoid a current spike and maintain relay health 59 | #So we need to set the current again after turning the relay ON. 60 | self.set_current(self.current_setpoint) 61 | else: 62 | self._inst_write(f"INSTR:RELAY {channel},0") 63 | self.current_setpoint = 0.0 64 | 65 | def get_output(self, channel = 1): 66 | return bool(int(self._inst_query(f"INSTR:RELAY {channel}?"))) 67 | 68 | def measure_voltage_supply(self, channel = 1): 69 | return float(self._inst_query(f"MEAS:VOLT {channel}?")) 70 | 71 | def measure_voltage_adc_supply(self, channel = 1): 72 | return float(self._inst_query(f"MEAS:VOLT:ADC {channel}?")) 73 | 74 | def measure_current(self, channel = 1): 75 | return (float(self._inst_query(f"CURR {channel}?")) * (-1)) #just returns the target current 76 | 77 | def measure_current_control(self, channel = 1): 78 | return (float(self._inst_query(f"CURR:CTRL {channel}?")) * (-1)) #returns the applied control signal for the current (use for calibration) 79 | 80 | def measure_temperature(self, channel = 1): 81 | return (float(self._inst_query(f"MEAS:TEMP {channel}?"))) 82 | 83 | def set_led(self, state, channel = 1): 84 | if state: 85 | self._inst_write(f"INSTR:LED {channel},1") 86 | else: 87 | self._inst_write(f"INSTR:LED {channel},0") 88 | 89 | def get_led(self, channel = 1): 90 | return bool(int(self._inst_query(f"INSTR:LED {channel}?"))) 91 | 92 | def set_fan(self, state, channel = 1): 93 | if state: 94 | self._inst_write(f"INSTR:FAN {channel},1") 95 | else: 96 | self._inst_write(f"INSTR:FAN {channel},0") 97 | 98 | def get_fan(self, channel = 1): 99 | return bool(int(self._inst_query(f"INSTR:FAN {channel}?"))) 100 | 101 | def cal_v_reset(self, channel = 1): 102 | self._inst_write(f"CAL:V:RST {channel}") 103 | 104 | def cal_v_save(self, channel = 1): 105 | self._inst_write(f"CAL:V:SAV {channel}") 106 | 107 | def cal_i_reset(self, channel = 1): 108 | self._inst_write(f"CAL:I:RST {channel}") 109 | 110 | def cal_i_save(self, channel = 1): 111 | self._inst_write(f"CAL:I:SAV {channel}") 112 | 113 | def get_cal_v(self, channel = 1): #returns [offset,gain] 114 | return [float(val) for val in self._inst_query_ascii(f'CAL:V {channel}?')] 115 | 116 | def get_cal_i(self, channel = 1): #returns [offset,gain] 117 | return [float(val) for val in self._inst_query_ascii(f'CAL:I {channel}?')] 118 | 119 | def calibrate_voltage(self, v1a, v1m, v2a, v2m, channel = 1): #2 points, actual (a - dmm) and measured (m - dut) 120 | self._inst_write(f'CAL:V {channel},{v1a},{v1m},{v2a},{v2m}') 121 | 122 | def calibrate_current(self, i1a, i1m, i2a, i2m, channel = 1): #2 points, actual (a - dmm) and measured (m - dut) 123 | self._inst_write(f'CAL:I {channel},{i1a},{i1m},{i2a},{i2m}') 124 | 125 | def get_rs485_addr(self): 126 | return int(self._inst_query("INSTR:RS485?")) 127 | 128 | def set_rs485_addr(self, address): 129 | self._inst_write(f"INSTR:RS485 {address}") 130 | 131 | def save_rs485_addr(self): 132 | self._inst_write("INSTR:RS485:SAV") 133 | 134 | def _inst_write(self, string_to_write): 135 | while (time.perf_counter() - self._last_command_time) < self.connection_settings['command_delay']: 136 | time.sleep(self.connection_settings['command_delay'] / 10.0) 137 | self.inst.write(string_to_write) 138 | self._last_command_time = time.perf_counter() 139 | 140 | def _inst_query(self, string_to_write): 141 | while (time.perf_counter() - self._last_command_time) < self.connection_settings['command_delay']: 142 | time.sleep(self.connection_settings['command_delay'] / 10.0) 143 | response = self.inst.query(string_to_write) 144 | self._last_command_time = time.perf_counter() 145 | return response 146 | 147 | def _inst_query_ascii(self, string_to_write): 148 | while (time.perf_counter() - self._last_command_time) < self.connection_settings['command_delay']: 149 | time.sleep(self.connection_settings['command_delay'] / 10.0) 150 | response = self.inst.query_ascii_values(string_to_write) 151 | self._last_command_time = time.perf_counter() 152 | return response 153 | 154 | def __del__(self): 155 | try: 156 | self.toggle_output(False) 157 | self.inst.close() 158 | except (AttributeError, pyvisa.errors.InvalidSession): 159 | pass 160 | -------------------------------------------------------------------------------- /lab_equipment/OTHER_Arduino_IO_Module.py: -------------------------------------------------------------------------------- 1 | #python library for controlling an Arduino's analog and digital pins 2 | 3 | import pyvisa 4 | import time 5 | from .PyVisaDeviceTemplate import PyVisaDevice 6 | import keyboard 7 | from functools import partial 8 | 9 | #Data Acquisition Unit 10 | class Arduino_IO(PyVisaDevice): 11 | connection_settings = { 12 | 'baud_rate': 57600, 13 | 'read_termination': '\r\n', 14 | 'write_termination': '\n', 15 | 'query_delay': 0.02, 16 | 'chunk_size': 102400, 17 | 'pyvisa_backend': '@py', 18 | 'time_wait_after_open': 2, 19 | 'idn_available': True 20 | } 21 | 22 | def initialize(self): 23 | self.num_channels = 13 24 | 25 | def __del__(self): 26 | try: 27 | self.inst.close() 28 | except AttributeError: 29 | pass 30 | 31 | def reset(self): 32 | self.inst.write('*RST') 33 | 34 | def conf_io(self, channel = 2, dir = 1): 35 | #1 means output - pin is being driven 36 | if dir: 37 | self.inst.write('CONF:IO:OUTP (@{ch})'.format(ch = channel)) 38 | #0 means input - pin high impedance 39 | elif not dir: 40 | self.inst.write('CONF:IO:INP (@{ch})'.format(ch = channel)) 41 | 42 | def conf_servo(self, channel): 43 | self.inst.write('CONF:SERVO (@{ch})'.format(ch = channel)) 44 | 45 | def set_servo_us(self, channel, value): 46 | self.inst.write('IO:SERVO:MICRO (@{ch}),{val}'.format(ch = channel, val = value)) 47 | 48 | def get_analog_mv(self, channel = 2): 49 | return self.inst.query('INSTR:IO:READ:ANA? (@{ch})'.format(ch = channel)) 50 | 51 | def get_analog_v(self, channel = 2): 52 | return self.get_analog_mv/1000.0 53 | 54 | def get_dig_in(self, channel = 2): 55 | return self.inst.query('INSTR:IO:READ:DIG? (@{ch})'.format(ch = channel)) 56 | 57 | def set_dig(self, channel = 2, value = 0): 58 | if(value > 1): 59 | value = 1 60 | self.inst.write('INSTR:IO:SET:DIG:OUTP (@{ch}),{val}'.format(ch = channel, val = value)) 61 | 62 | def set_pwm(self, channel = 3, pwm = 0): 63 | if(pwm > 255): #max arduino 8-bit PWM 64 | pwm = 255 65 | self.inst.write('INSTR:IO:SET:PWM:OUTP (@{ch}),{val}'.format(ch = channel, val = pwm)) 66 | 67 | def fade_pwm(self, channel = 3, pwm_start = 0, pwm_end = 255, time_s = 1): 68 | pwm_high = max(pwm_start, pwm_end) 69 | pwm_low = min(pwm_start, pwm_end) 70 | 71 | increment = 1 72 | if pwm_end < pwm_start: 73 | increment = -1 74 | 75 | num_steps = pwm_high - pwm_low + 1 76 | s_per_step = time_s / num_steps 77 | 78 | for pwm in range(pwm_start, pwm_end + 1*increment, increment): 79 | self.set_pwm(channel, pwm) 80 | time.sleep(s_per_step) #not fully accurate, but works for now 81 | 82 | #make a pin give a high or low pulse 83 | def pulse_pin(self, channel = 2, pulse_val = 1): 84 | self.inst.write('INSTR:IO:PULSE (@{ch}),{val}'.format(ch = channel, val = pulse_val)) 85 | 86 | def set_led(self, value = 0): 87 | if(value > 1): 88 | value = 1 89 | #x is a character that we parse but do nothing with (channel must be first) 90 | self.inst.write('INSTR:IO:SET:LED x {val}'.format(val = value)) 91 | 92 | 93 | 94 | if __name__ == "__main__": 95 | #connect to the io module 96 | io = Arduino_IO() 97 | 98 | ###################### Controlling some PWM pins 99 | ''' 100 | # setup testing for 6 pwm signals 101 | out_pins = [2,3,5,6,9,10,11] 102 | en_pin = 2 103 | pwm_pins = [3,5,6,9,10,10] 104 | 105 | for pin in out_pins: 106 | io.conf_io(pin, 1) 107 | 108 | #enable 109 | io.set_dig(en_pin, 1) 110 | 111 | #fade PWMs in and out 112 | for pin in pwm_pins: 113 | io.fade_pwm(pin, 0, 255, 0.5) 114 | io.fade_pwm(pin, 255, 0, 0.5) 115 | 116 | #disable 117 | io.set_dig(en_pin, 0) 118 | ''' 119 | 120 | ##################### Controlling some servos with arrow keys 121 | # WASD control for moving a 2 wheel robot around with tank steering. 122 | io.conf_servo(9) 123 | io.conf_servo(10) 124 | 125 | up_pressed = False 126 | down_pressed = False 127 | left_pressed = False 128 | right_pressed = False 129 | 130 | def key_pr(test, key, pr): 131 | #print('KEY: {}, PR: {}'.format(key, pr)) 132 | pressed = False 133 | if pr == 'p': 134 | pressed = True 135 | if key == 'w': 136 | #print('W {}'.format(pressed)) 137 | global up_pressed 138 | up_pressed = pressed 139 | elif key == 'a': 140 | global left_pressed 141 | left_pressed = pressed 142 | elif key == 's': 143 | global down_pressed 144 | down_pressed = pressed 145 | elif key == 'd': 146 | global right_pressed 147 | right_pressed = pressed 148 | 149 | keyboard.on_press_key('w', partial(key_pr,key = 'w',pr = 'p')) 150 | keyboard.on_press_key('a', partial(key_pr,key = 'a',pr = 'p')) 151 | keyboard.on_press_key('s', partial(key_pr,key = 's',pr = 'p')) 152 | keyboard.on_press_key('d', partial(key_pr,key = 'd',pr = 'p')) 153 | 154 | keyboard.on_release_key('w', partial(key_pr,key = 'w',pr = 'r')) 155 | keyboard.on_release_key('a', partial(key_pr,key = 'a',pr = 'r')) 156 | keyboard.on_release_key('s', partial(key_pr,key = 's',pr = 'r')) 157 | keyboard.on_release_key('d', partial(key_pr,key = 'd',pr = 'r')) 158 | 159 | io.set_servo_us(9,1500) 160 | io.set_servo_us(10, 1500) 161 | 162 | while True: 163 | if up_pressed: 164 | #both forward 165 | print('Forward') 166 | io.set_servo_us(9, 1800) 167 | io.set_servo_us(10, 1800) 168 | elif down_pressed: 169 | #both backward 170 | print('Backward') 171 | io.set_servo_us(9, 1200) 172 | io.set_servo_us(10, 1200) 173 | elif left_pressed: 174 | #left only forward 175 | print('Left') 176 | io.set_servo_us(9, 1800) 177 | io.set_servo_us(10, 1500) 178 | elif right_pressed: 179 | #right only forward 180 | print('Right') 181 | io.set_servo_us(9, 1500) 182 | io.set_servo_us(10, 1800) 183 | else: 184 | print('Stop') 185 | io.set_servo_us(9, 1500) 186 | io.set_servo_us(10, 1500) 187 | #print('Loop {} {} {} {}'.format(up_pressed, down_pressed, left_pressed, right_pressed)) 188 | time.sleep(0.5) 189 | -------------------------------------------------------------------------------- /BATT_HIL/Eload_DL3000.py: -------------------------------------------------------------------------------- 1 | #python pyvisa commands for controlling Rigol DL3000 series eloads 2 | #Written by: Micah Black 3 | # 4 | #Updated by: Micah Black Oct 4, 2021 5 | #Added Battery Test Mode 6 | #Changed local variable 'range' to '_range' to avoid confusion with python's range() function 7 | 8 | import pyvisa 9 | import time 10 | import easygui as eg 11 | 12 | # E-Load 13 | class DL3000: 14 | # Initialize the DL3000 E-Load 15 | def __init__(self, resource_id = ""): 16 | rm = pyvisa.ResourceManager('@ivi') 17 | 18 | if(resource_id == ""): 19 | resources = rm.list_resources('@ivi') 20 | 21 | ################# IDN VERSION ################# 22 | #Attempt to connect to each Visa Resource and get the IDN response 23 | title = "Eload Selection" 24 | if(len(resources) == 0): 25 | resource_id = 0 26 | print("No PyVisa Resources Available. Connection attempt will exit with errors") 27 | idns_dict = {} 28 | for resource in resources: 29 | try: 30 | instrument = rm.open_resource(resource) 31 | instrument_idn = instrument.query("*IDN?") 32 | idns_dict[resource] = instrument_idn 33 | instrument.close() 34 | except pyvisa.errors.VisaIOError: 35 | pass 36 | 37 | #Now we have all the available resources that we can connect to, with their IDNs. 38 | resource_id = 0 39 | if(len(idns_dict.values()) == 0): 40 | print("No Equipment Available. Connection attempt will exit with errors") 41 | elif(len(idns_dict.values()) == 1): 42 | msg = "There is only 1 Visa Equipment available.\nWould you like to use it?\n{}".format(list(idns_dict.values())[0]) 43 | if(eg.ynbox(msg, title)): 44 | idn = list(idns_dict.values())[0] 45 | else: 46 | msg = "Select the Eload Supply Model:" 47 | idn = eg.choicebox(msg, title, idns_dict.values()) 48 | #Now we know which IDN we want to connect to 49 | #swap keys and values and then connect 50 | resources_dict = dict((v,k) for k,v in idns_dict.items()) 51 | resource_id = resources_dict[idn] 52 | 53 | 54 | 55 | self.inst = rm.open_resource(resource_id) 56 | 57 | self.inst_idn = self.inst.query("*IDN?") 58 | print("Connected to {}\n".format(self.inst_idn)) 59 | 60 | split_string = self.inst_idn.split(",") 61 | self.manufacturer = split_string[0] 62 | self.model_number = split_string[1] 63 | self.serial_number = split_string[2] 64 | self.version_number = split_string[3] 65 | 66 | self._range = "low" 67 | 68 | if self.model_number == "DL3021" or self.model_number == "DL3021A": 69 | #values specific to the DL3000 - will break out to another file later 70 | self.ranges = {"low":4,"high":40} 71 | elif self.model_number == "DL3031" or self.model_number == "DL3031A": 72 | #values specific to the DL3000 - will break out to another file later 73 | self.ranges = {"low":6,"high":60} 74 | 75 | self.inst.write("*RST") 76 | self.set_mode_current() 77 | self.set_current(0) 78 | 79 | #set to remote mode (disable front panel) 80 | self.lock_front_panel(True) 81 | 82 | # To Set E-Load in Amps 83 | def set_current(self, current_setpoint_A): 84 | #4 or 6A range 85 | if(current_setpoint_A <= self.ranges["low"]): 86 | if(self._range != "low"): 87 | self.set_range("low") 88 | 89 | #40 or 60A range 90 | elif(current_setpoint_A <= self.ranges["high"]): 91 | if(self._range != "high"): 92 | self.set_range("high") 93 | 94 | self.inst.write(":CURR:LEV %s" % current_setpoint_A) 95 | 96 | def set_range(self, set_range): 97 | #set_range is either "high" or "low" 98 | write_range = "MIN" 99 | if(set_range == "high"): 100 | write_range = "MAX" 101 | self.inst.write(":CURR:RANG {}".format(write_range)) 102 | self._range = set_range 103 | 104 | def set_mode_current(self): 105 | self.inst.write(":FUNC CURR") 106 | 107 | def toggle_output(self, state): 108 | if state: 109 | self.inst.write(":INP ON") 110 | else: 111 | self.inst.write(":INP OFF") 112 | 113 | def remote_sense(self, state): 114 | if state: 115 | self.inst.write(":SENS ON") 116 | else: 117 | self.inst.write(":SENS OFF") 118 | 119 | def lock_front_panel(self, state): 120 | pass 121 | # if state: 122 | # self.inst.write("SYST:REM") 123 | # else: 124 | # self.inst.write("SYST:LOC") 125 | 126 | def measure_voltage(self): 127 | return float(self.inst.query(":MEAS:VOLT:DC?")) 128 | 129 | def measure_current(self): 130 | return float(self.inst.query(":MEAS:CURR:DC?")) 131 | 132 | def battery_test(self, current_a, end_condition = "v", end_value = 2.5, start_v = 2.5) 133 | #end_condition is "c", "v", or "t" for voltage, current, or time. 134 | #end_v is end voltage in V 135 | #end_c is end capacity in Ah 136 | #end_t is end time in seconds 137 | 138 | if(end_condition not in ("c", "v", "t"): 139 | print("Incompatible end condition for battery test") 140 | return 141 | 142 | #ensure output is off before switching modes 143 | self.toggle_output(False) 144 | #Go into battery mode 145 | self.inst.write(":FUNC:MODE:BATT") 146 | 147 | #Set current range for the battery 148 | self.inst.write(":BATT:RANG {}".format(current_a)) 149 | #Set battery discharge current 150 | self.inst.write(":BATT {}".format(current_a)) 151 | 152 | vstop = "MIN" 153 | cstop = "MIN" 154 | tstop = "MIN" 155 | 156 | if end_condition == "c": 157 | vstop = "MAX" 158 | tstop = "MAX" 159 | cstop = end_value * 1000 160 | elif end_condition == "v": 161 | vstop = end_value 162 | tstop = "MAX" 163 | cstop = "MAX" 164 | elif end_condition == "t": 165 | vstop = "MAX" 166 | tstop = end_value 167 | cstop = "MAX" 168 | 169 | self.inst.write(":BATT:VST {}".format(vstop)) 170 | self.inst.write(":BATT:CST {}".format(cstop)) 171 | self.inst.write(":BATT:TIM {}".format(tstop)) 172 | self.inst.write("BATT:VON {}".format(start_v)) 173 | 174 | #TODO - do VST, CST, TIM turn off the output when they trigger? 175 | 176 | #TODO - what are CEN, VEN, and TEN? 177 | 178 | #TODO - can we measure voltage and current when in battery mode? 179 | 180 | #TODO - how do we know when the discharge is complete? 181 | 182 | def get_battery_capacity_ah(self): 183 | #I believe the load measures capacity in mAh, so divide by 1000 for Ah. 184 | return self.inst.query(":FETCH:CAP?")/1000.0 185 | 186 | def get_battery_energy_wh(self): 187 | return self.inst.query(":FETCH:WATT?") 188 | 189 | def get_battery_time_s(self): 190 | #returns the time it took to discharge the battery_test 191 | return self.inst.query(":FETCH:DISCT?") 192 | 193 | def __del__(self): 194 | self.toggle_output(False) 195 | #self.lock_front_panel(False) 196 | try: 197 | self.inst.close() 198 | except AttributeError: 199 | pass -------------------------------------------------------------------------------- /BATT_HIL/LTC2944/LTC2944.py: -------------------------------------------------------------------------------- 1 | #Driver for the LTC2944 Coulomb Counter 2 | #Written By: Micah Black 3 | #Date: Oct 1, 2021 4 | #For use with Adafruit's CircuitPython Libraries 5 | 6 | 7 | from micropython import const 8 | import adafruit_bus_device.i2c_device as i2cdevice 9 | 10 | from adafruit_register.i2c_struct import ROUnaryStruct, UnaryStruct 11 | from adafruit_register.i2c_bits import ROBits, RWBits 12 | from adafruit_register.i2c_bit import ROBit, RWBit 13 | 14 | 15 | # I2C Address and Register Locations 16 | _I2C_ADDRESS = const(0x64) 17 | 18 | _REG_STATUS = const(0x00) 19 | _REG_CONTROL = const(0x01) 20 | _REG_ACCUMULATED_CHARGE_MSB = const(0x02) 21 | #_REG_ACCUMULATED_CHARGE_LSB = const(0x03) 22 | _REG_CHARGE_THRESHOLD_HIGH_MSB = const(0x04) 23 | #_REG_CHARGE_THRESHOLD_HIGH_LSB = const(0x05) 24 | _REG_CHARGE_THRESHOLD_LOW_MSB = const(0x06) 25 | #_REG_CHARGE_THRESHOLD_LOW_LSB = const(0x07) 26 | _REG_VOLTAGE_MSB = const(0x08) 27 | #_REG_VOLTAGE_LSB = const(0x09) 28 | _REG_VOLTAGE_THRESHOLD_HIGH_MSB = const(0x0A) 29 | #_REG_VOLTAGE_THRESHOLD_HIGH_LSB = const(0x0B) 30 | _REG_VOLTAGE_THRESHOLD_LOW_MSB = const(0x0C) 31 | #_REG_VOLTAGE_THRESHOLD_LOW_LSB = const(0x0D) 32 | _REG_CURRENT_MSB = const(0x0E) 33 | #_REG_CURRENT_LSB = const(0x0F) 34 | _REG_CURRENT_THRESHOLD_HIGH_MSB = const(0x10) 35 | #_REG_CURRENT_THRESHOLD_HIGH_LSB = const(0x11) 36 | _REG_CURRENT_THRESHOLD_LOW_MSB = const(0x12) 37 | #_REG_CURRENT_THRESHOLD_LOW_LSB = const(0x13) 38 | _REG_TEMPERATURE_MSB = const(0x14) 39 | #_REG_TEMPERATURE_LSB = const(0x15) 40 | _REG_TEMPERATURE_THRESHOLD_HIGH = const(0x16) 41 | _REG_TEMPERATURE_THRESHOLD_LOW = const(0x17) 42 | 43 | 44 | class LTC2944_ADCMode: 45 | AUTO = const(0x03) #Continuous conversions of voltage, current, and temp 46 | SCAN = const(0x02) #Conversions every 10s 47 | MANUAL = const(0x01) #Single conversion then sleep 48 | SLEEP = const(0x00) #Sleep 49 | 50 | class LTC2944_Prescaler: 51 | PRESCALER_1 = const(0x00) 52 | PRESCALER_4 = const(0x01) 53 | PRESCALER_16 = const(0x02) 54 | PRESCALER_64 = const(0x03) 55 | PRESCALER_256 = const(0x04) 56 | PRESCALER_1024 = const(0x05) 57 | PRESCALER_4096 = const(0x06) 58 | PRESCALER_4096_2 = const(0x07) 59 | 60 | def get_prescaler(prescaler_enum): 61 | conv_dict = { 62 | 0: 1, 63 | 1: 4, 64 | 2: 16, 65 | 3: 64, 66 | 4: 256, 67 | 5: 1024, 68 | 6: 4096, 69 | 7: 4096 70 | } 71 | return conv_dict[prescaler_enum] 72 | 73 | class LTC2944_ALCCConf: 74 | ALERT = const(0x02) #Pin is a logic output for alert functionality 75 | CC = const(0x01) #Pin is Logic input, accepts active low charge complete signal 76 | DISABLED = const(0x00) #pin disabled 77 | #0x03 is not allowed 78 | 79 | 80 | 81 | class LTC2944: 82 | """Driver for the LTC2944 power and current sensor. 83 | :param ~busio.I2C i2c_bus: The I2C bus the LTC2944 is connected to. 84 | """ 85 | 86 | def __init__(self, i2c_bus): 87 | self.i2c_device = i2cdevice.I2CDevice(i2c_bus, _I2C_ADDRESS) 88 | 89 | 90 | RSHUNT = 0.1 #Set the value of the shunt resistor in Ohms. 91 | 92 | #Status Register A 93 | current_alert = ROBit(_REG_STATUS, 6) 94 | accum_charge_over_under_flow = ROBit(_REG_STATUS, 5) 95 | temp_alert = ROBit(_REG_STATUS, 4) 96 | charge_high_alert = ROBit(_REG_STATUS, 3) 97 | charge_low_alert = ROBit(_REG_STATUS, 2) 98 | voltage_alert = ROBit(_REG_STATUS, 1) 99 | uvlo_alert = ROBit(_REG_STATUS, 0) 100 | 101 | #Control Register B 102 | adc_mode = RWBits(2, _REG_CONTROL, 6) 103 | prescaler = RWBits(3, _REG_CONTROL, 3) 104 | alcc_config = RWBits(2, _REG_CONTROL, 1) 105 | shutdown = RWBit(_REG_CONTROL, 0) 106 | 107 | #Accumulated Charge 108 | charge = UnaryStruct(_REG_ACCUMULATED_CHARGE_MSB, ">H") #Unsigned Short 109 | charge_thresh_high = UnaryStruct(_REG_CHARGE_THRESHOLD_HIGH_MSB, ">H") 110 | charge_thresh_low = UnaryStruct(_REG_CHARGE_THRESHOLD_LOW_MSB, ">H") 111 | 112 | #Voltage 113 | _raw_voltage = ROUnaryStruct(_REG_VOLTAGE_MSB, ">H") #Unsigned Short 114 | volt_thresh_high = UnaryStruct(_REG_VOLTAGE_THRESHOLD_HIGH_MSB, ">H") 115 | volt_thresh_low = UnaryStruct(_REG_VOLTAGE_THRESHOLD_LOW_MSB, ">H") 116 | 117 | #Current 118 | _raw_current = ROUnaryStruct(_REG_CURRENT_MSB, ">H") #Unsigned Short 119 | current_thresh_high = UnaryStruct(_REG_CURRENT_THRESHOLD_HIGH_MSB, ">H") 120 | current_thresh_low = UnaryStruct(_REG_CURRENT_THRESHOLD_LOW_MSB, ">H") 121 | 122 | #Temperature 123 | _raw_temp = ROUnaryStruct(_REG_TEMPERATURE_MSB, ">H") #Unsigned Short 124 | temp_thresh_high = UnaryStruct(_REG_TEMPERATURE_THRESHOLD_HIGH, ">B") #unsigned Char 125 | temp_thresh_low = UnaryStruct(_REG_TEMPERATURE_THRESHOLD_LOW, ">B") #unsigned Char 126 | 127 | 128 | #Measurements 129 | 130 | def get_current_a(self): 131 | return self.convert_raw_current(self._raw_current) 132 | 133 | def get_voltage_v(self): 134 | return self.convert_raw_voltage(self._raw_voltage) 135 | 136 | def get_temp_c(self): 137 | return self.convert_raw_temp(self._raw_temp) 138 | 139 | 140 | #Thresholds 141 | 142 | def set_current_thresh_high(self, current_a): 143 | current_thresh_high = self.convert_current(current_a) 144 | 145 | def set_current_thresh_low(self, current_a): 146 | current_thresh_low = self.convert_current(current_a) 147 | 148 | def set_voltage_thresh_high(self, voltage_v): 149 | volt_thresh_high = self.convert_voltage(voltage_v) 150 | 151 | def set_voltage_thresh_low(self, voltage_v): 152 | volt_thresh_low = self.convert_voltage(voltage_v) 153 | 154 | def set_temp_thresh_high(self, temp_c): 155 | temp_thresh_high = self.convert_temp(temp_c) 156 | 157 | def set_temp_thresh_low(self, temp_c): 158 | temp_thresh_low = self.convert_temp(temp_c) 159 | 160 | 161 | #Measurement Conversions to get values 162 | 163 | def convert_raw_voltage(self, raw_voltage): 164 | return 70.8 * raw_voltage / 0xFFFF 165 | 166 | def convert_raw_current(self, raw_current): 167 | return 0.064 / self.RSHUNT * ((raw_current - 0x7FFF)/0x7FFF) 168 | 169 | def convert_raw_temp(self, raw_temp): 170 | return (510*raw_temp/0xFFFF) -273.15 171 | 172 | 173 | #Setting Conversions to set registers 174 | 175 | def convert_voltage(self, voltage): 176 | return voltage/70.8*0xFFFF 177 | 178 | def convert_current(self, current): 179 | return current * self.RSHUNT/0.064*32767 + 32767 180 | 181 | def convert_temp(self, temp): 182 | return ((temp + 273.15)/510*0xFFFF) & 0b11111111 #convert to 8 bits only since regs are 8-bit 183 | 184 | 185 | #Setting Charge Register 186 | def set_charge(self, set_charge): 187 | #check analog section power 188 | analog_off = self.shutdown 189 | 190 | if not analog_off: 191 | #disable analog section 192 | self.shutdown = True 193 | 194 | #set charge 195 | if 0 <= set_charge <= 0xFFFF: 196 | self.charge = set_charge 197 | else: 198 | print("Charge set is out of bounds") 199 | 200 | if not analog_off: 201 | #re-enable analog section to same state as before call to this function 202 | self.shutdown = False 203 | -------------------------------------------------------------------------------- /lab_equipment/PyVisaDeviceTemplate.py: -------------------------------------------------------------------------------- 1 | #Class to have all device selection have a common area instead of repeated in each device class. 2 | 3 | import pyvisa 4 | import easygui as eg 5 | import serial 6 | import time 7 | 8 | class PyVisaDevice: 9 | selection_window_title = "PyVisa Device Selection" 10 | 11 | connection_settings = { 12 | 'pyvisa_backend': '@py', 13 | 'time_wait_after_open': 0, 14 | 'idn_available': True, 15 | 'timeout': 1000 16 | } 17 | 18 | def __init__(self, resource_id = None, resources_list = None): 19 | rm = pyvisa.ResourceManager(self.connection_settings['pyvisa_backend']) 20 | 21 | self._last_command_time = time.perf_counter() 22 | 23 | if(resource_id == None): 24 | resources = None 25 | if resources_list == None: 26 | resources = rm.list_resources() 27 | else: 28 | #resources should be all the resources that have the same backend as this. 29 | resources = [resource['resource'] for resource in resources_list if resource['backend'] == self.connection_settings['pyvisa_backend']] 30 | 31 | ################# IDN VERSION ################# 32 | #Attempt to connect to each Visa Resource using the connection settings for this instrument and get the IDN response 33 | if(len(resources) == 0): 34 | resource_id = 0 35 | print("No PyVisa Resources Available. Connection attempt will exit with errors") 36 | idns_dict = {} 37 | for resource in resources: 38 | try: 39 | instrument = rm.open_resource(resource) 40 | time.sleep(self.connection_settings['time_wait_after_open']) 41 | 42 | #Instrument settings 43 | try: 44 | instrument.baud_rate = self.connection_settings['baud_rate'] 45 | except KeyError: 46 | pass 47 | 48 | try: 49 | instrument.read_termination = self.connection_settings['read_termination'] 50 | except KeyError: 51 | pass 52 | 53 | try: 54 | instrument.query_delay = self.connection_settings['query_delay'] 55 | except KeyError: 56 | pass 57 | 58 | try: 59 | instrument.write_termination = self.connection_settings['write_termination'] 60 | except KeyError: 61 | pass 62 | 63 | try: 64 | instrument.chunk_size = self.connection_settings['chunk_size'] 65 | except KeyError: 66 | pass 67 | 68 | try: 69 | instrument.timeout = self.connection_settings['timeout'] 70 | except KeyError: 71 | pass 72 | 73 | #Get IDN and add to dict 74 | if self.connection_settings['idn_available']: 75 | instrument_idn = instrument.query("*IDN?") 76 | idns_dict[resource] = instrument_idn 77 | else: 78 | try: 79 | if self.class_name == 'BK9100': 80 | try: 81 | instrument.query("GOUT") #try to get something from the instrument 82 | instrument.query("") #clear output buffer 83 | idns_dict[resource] = resource 84 | except (pyvisa.errors.VisaIOError, PermissionError, serial.serialutil.SerialException): 85 | print("Failed to query BK9100") 86 | else: 87 | idns_dict[resource] = resource 88 | except AttributeError: 89 | pass 90 | 91 | #Close connection 92 | instrument.close() 93 | except (pyvisa.errors.VisaIOError, PermissionError, serial.serialutil.SerialException): 94 | pass 95 | 96 | #Now we have all the available resources that we can connect to, with their IDNs if they were able to communicate with the same settings as the chosen device. 97 | resource_id = 0 98 | idn = None 99 | if(len(idns_dict.values()) == 0): 100 | print("No Equipment Available. Connection attempt will exit with errors") 101 | elif(len(idns_dict.values()) == 1): 102 | msg = "There is only 1 Visa Equipment available.\nWould you like to use it?\n{}".format(list(idns_dict.values())[0]) 103 | if(eg.ynbox(msg, self.selection_window_title)): 104 | idn = list(idns_dict.values())[0] 105 | else: 106 | msg = "Select the PyVisaDevice Resource:" 107 | idn = eg.choicebox(msg, self.selection_window_title, idns_dict.values()) 108 | #Now we know which IDN we want to connect to 109 | #swap keys and values and then connect 110 | if idn != None: 111 | resources_dict = dict((v,k) for k,v in idns_dict.items()) 112 | resource_id = resources_dict[idn] 113 | else: 114 | print("No Device Selected. Exiting.") 115 | return 116 | 117 | 118 | #Now we know the resource ID (no matter what type of device) 119 | self.inst = rm.open_resource(resource_id) 120 | time.sleep(self.connection_settings['time_wait_after_open']) 121 | 122 | try: 123 | self.inst.baud_rate = self.connection_settings['baud_rate'] 124 | except KeyError: 125 | pass 126 | 127 | try: 128 | self.inst.read_termination = self.connection_settings['read_termination'] 129 | except KeyError: 130 | pass 131 | 132 | try: 133 | self.inst.query_delay = self.connection_settings['query_delay'] 134 | except KeyError: 135 | pass 136 | 137 | try: 138 | self.inst.write_termination = self.connection_settings['write_termination'] 139 | except KeyError: 140 | pass 141 | 142 | try: 143 | self.inst.chunk_size = self.connection_settings['chunk_size'] 144 | except KeyError: 145 | pass 146 | 147 | try: 148 | self.inst.timeout = self.connection_settings['timeout'] 149 | except KeyError: 150 | pass 151 | 152 | if self.connection_settings['idn_available']: 153 | try: 154 | self.inst_idn = self._inst_query("*IDN?") 155 | except AttributeError: 156 | self.inst_idn = self.inst.query("*IDN?") 157 | print("Connected to {}\n".format(self.inst_idn)) 158 | else: 159 | self.inst_idn = resource_id 160 | 161 | self.initialize() #Specific initialization for each device 162 | 163 | def initialize(self): 164 | pass 165 | 166 | 167 | class PowerSupplyDevice(PyVisaDevice): 168 | selection_window_title = "Power Supply Selection" 169 | 170 | class EloadDevice(PyVisaDevice): 171 | selection_window_title = "Eload Selection" 172 | 173 | class SourceMeasureDevice(PyVisaDevice): 174 | selection_window_title = "Source Measure Selection" 175 | 176 | class DMMDevice(PyVisaDevice): 177 | selection_window_title = "DMM Selection" -------------------------------------------------------------------------------- /Calibration_Script.py: -------------------------------------------------------------------------------- 1 | #Calibrate a voltage meter with interface like the A2D_4CH_Isolated_ADC 2 | #3 instruments required 3 | #A power supply to generate voltage 4 | #A calibrated DMM for the reference meter 5 | #A Devide-Under-Test to generate calibration data for 6 | 7 | #Confirmed working with: 8 | #A2D_4CH_Isolated_ADC 9 | 10 | import equipment as eq 11 | import time 12 | import easygui as eg 13 | import numpy as np 14 | import pandas as pd 15 | import matplotlib.pyplot as plt 16 | 17 | class CalibrationClass: 18 | def __init__(self): 19 | self.dmm = None 20 | self.dut = None 21 | self.psu = None 22 | 23 | self.dmm_idn = "None" 24 | self.dut_idn = "None" 25 | self.psu_idn = "None" 26 | 27 | self.dut_channel = None 28 | 29 | def connect_psu(self): 30 | if self.psu is not None: 31 | self.psu.inst.close() 32 | 33 | self.psu = eq.powerSupplies.choose_psu()[1] 34 | self.psu_idn = self.psu.inst_idn 35 | 36 | def connect_dmm(self): 37 | if self.dmm is not None: 38 | self.dmm.inst.close() 39 | 40 | self.dmm = eq.dmms.choose_dmm()[1] 41 | self.dmm_idn = self.dmm.inst_idn 42 | 43 | def connect_dut(self): 44 | if self.dut is not None: 45 | self.dut.inst.close() 46 | 47 | self.dut = eq.dmms.choose_dmm()[1] 48 | self.dut.reset() 49 | self.dut_idn = self.dut.inst_idn 50 | 51 | try: 52 | self.dut_channel = eq.choose_channel(self.dut.num_channels, self.dut.start_channel) 53 | self.dut_idn += f' CH {self.dut_channel}' 54 | except AttributeError: 55 | pass 56 | 57 | def get_lin_cal_points(self): 58 | values = eg.multenterbox(msg = 'Enter the 2 points for linear calibration', 59 | title = 'Calibration Settings', 60 | fields = ['point_1', 'point_2'], 61 | values = [2,4]) 62 | 63 | return [float(val) for val in values] 64 | 65 | def calibrate_voltage_meter(self): 66 | 67 | points = self.get_lin_cal_points() 68 | 69 | #CAL POINT 1 70 | self.psu.set_voltage(points[0]) 71 | self.psu.set_current(0.1) 72 | self.psu.toggle_output(True) 73 | 74 | time.sleep(1) 75 | voltage_1 = self.measure_average(num_to_average = 10, at_adc = True) 76 | print(f"voltage_1: {voltage_1}") 77 | 78 | #CAL POINT 2 79 | self.psu.set_voltage(points[1]) 80 | time.sleep(1) 81 | voltage_2 = self.measure_average(num_to_average = 10, at_adc = True) 82 | print(f"voltage_2: {voltage_2}") 83 | 84 | self.psu.toggle_output(False) 85 | 86 | if self.dut_channel is not None: 87 | old_calibration = self.dut.get_calibration(self.dut_channel) 88 | else: 89 | old_calibration = self.dut.get_calibration() 90 | 91 | #CALIBRATE 92 | if self.dut_channel is not None: 93 | self.dut.calibrate_voltage(voltage_1['dmm'], voltage_1['dut'], voltage_2['dmm'], voltage_2['dut'], self.dut_channel) 94 | else: 95 | self.dut.calibrate_voltage(voltage_1['dmm'], voltage_1['dut'], voltage_2['dmm'], voltage_2['dut']) 96 | 97 | if self.dut_channel is not None: 98 | new_calibration = self.dut.get_calibration(self.dut_channel) 99 | else: 100 | new_calibration = self.dut.get_calibration() 101 | 102 | title = 'Use New Calibration?' 103 | message = f'Would you like to save the new calibration?\nOld Calibration: {old_calibration}\nNew Calibration: {new_calibration}' 104 | 105 | if eg.ynbox(title = title,msg = message): 106 | if self.dut_channel is not None: 107 | self.dut.save_calibration(self.dut_channel) 108 | else: 109 | self.dut.save_calibration() 110 | else: 111 | self.dut.reset() #resets calibration values back to original values 112 | 113 | def get_cal_check_points(self): 114 | values = eg.multenterbox(msg = 'Enter the voltage range and number of steps', 115 | title = 'Calibration Check Settings', 116 | fields = ['min_voltage', 'max_voltage','num_steps'], 117 | values = [0,5,10]) 118 | 119 | min_voltage = float(values[0]) 120 | max_voltage = float(values[1]) 121 | steps = int(values[2]) 122 | 123 | return np.linspace(min_voltage, max_voltage, steps) 124 | 125 | def check_voltage_calibration(self): 126 | voltages = self.get_cal_check_points() 127 | 128 | self.psu.set_voltage(0) 129 | self.psu.set_current(0.1) 130 | self.psu.toggle_output(True) 131 | 132 | measurements_list = [] 133 | 134 | for voltage in voltages: 135 | self.psu.set_voltage(voltage) 136 | time.sleep(1) 137 | measurements = {'psu': voltage} 138 | measurements.update(self.measure_average(at_adc = False)) 139 | measurements_list.append(measurements) 140 | 141 | self.psu.toggle_output(False) 142 | 143 | df = pd.DataFrame.from_records(measurements_list) 144 | df['dut-dmm'] = df['dut'] - df['dmm'] 145 | df['dut-dmm_percent'] = df['dut-dmm']/df['psu']*100 146 | 147 | fig, ax1 = plt.subplots() 148 | ax1.set_xlabel('Input Voltage (V)') 149 | ax1.plot(df['psu'], df['dut-dmm'], color='r') 150 | ax1.set_ylabel('Error (V)', color='r') 151 | 152 | ax2 = ax1.twinx() 153 | 154 | ax2.plot(df['psu'], df['dut-dmm_percent'], color='b') 155 | ax2.set_ylabel('Error (%)', color = 'b') 156 | 157 | fig.suptitle('Calibration Check') 158 | fig.tight_layout() 159 | plt.show() 160 | 161 | def measure_average(self, num_to_average = 10, at_adc = False): 162 | #get measurements from each source 163 | measurements = {'dut': [],'dmm': []} 164 | for i in range(num_to_average): 165 | if self.dut_channel is not None: 166 | if at_adc: 167 | measurements['dut'].append(self.dut.measure_voltage_at_adc(self.dut_channel)) 168 | else: 169 | measurements['dut'].append(self.dut.measure_voltage(self.dut_channel)) 170 | else: 171 | if at_adc: 172 | measurements['dut'].append(self.dut.measure_voltage_at_adc()) 173 | else: 174 | measurements['dut'].append(self.dut.measure_voltage()) 175 | measurements['dmm'].append(self.dmm.measure_voltage()) 176 | time.sleep(0.1) 177 | 178 | #remove high and low readings if there are enough 179 | if num_to_average >= 5: 180 | measurements['dut'].remove(max(measurements['dut'])) 181 | measurements['dut'].remove(min(measurements['dut'])) 182 | measurements['dmm'].remove(max(measurements['dmm'])) 183 | measurements['dmm'].remove(min(measurements['dmm'])) 184 | 185 | measurements['dut'] = np.mean(measurements['dut']) 186 | measurements['dmm'] = np.mean(measurements['dmm']) 187 | 188 | return measurements 189 | 190 | if __name__ == '__main__': 191 | #CONNECT EQUIPMENT 192 | cal_class = CalibrationClass() 193 | cal_class.connect_dmm() 194 | cal_class.connect_dut() 195 | cal_class.connect_psu() 196 | 197 | cal_choices = ["Calibrate", "Check Calibration"] 198 | 199 | cal_type = eg.indexbox(msg = "What would you like to do?", 200 | title = "Calibration Options", 201 | choices = cal_choices, 202 | default_choice = cal_choices[0]) 203 | 204 | cal_class.psu.set_voltage(0) 205 | cal_class.psu.set_current(0.1) 206 | 207 | if cal_type == 0: 208 | cal_class.calibrate_voltage_meter() 209 | elif cal_type == 1: 210 | cal_class.check_voltage_calibration() 211 | -------------------------------------------------------------------------------- /Templates.py: -------------------------------------------------------------------------------- 1 | #class to hold the templates for input/outputs settings 2 | 3 | import easygui as eg 4 | import jsonIO 5 | import json 6 | import os 7 | 8 | 9 | ###################### Statistics to gather for each cycle ############### 10 | class CycleStats: 11 | 12 | def __init__(self): 13 | self.stats = { 14 | "cell_name": 0, 15 | "charge_capacity_ah": 0, 16 | "charge_capacity_wh": 0, 17 | "charge_time_h": 0, 18 | "charge_current_a": 0, 19 | "charge_max_temp_c": 0, 20 | "charge_start_time": 0, 21 | "charge_end_time": 0, 22 | "charge_end_v": 0, 23 | "discharge_capacity_ah": 0, 24 | "discharge_capacity_wh": 0, 25 | "discharge_time_h": 0, 26 | "discharge_current_a": 0, 27 | "discharge_max_temp_c": 0, 28 | "discharge_start_time": 0, 29 | "discharge_end_time": 0, 30 | "discharge_end_v": 0 31 | } 32 | 33 | ##################### AVAILABLE CYCLE TYPES #################### 34 | class CycleTypes: 35 | 36 | cycle_types = { 37 | "Single_CC_Cycle": {'func_call': '', 'str_chg_opt': True}, 38 | "One_Setting_Continuous_CC_Cycles_With_Rest": {'func_call': '', 'str_chg_opt': True}, 39 | "Two_Setting_Continuous_CC_Cycles_With_Rest": {'func_call': '', 'str_chg_opt': True}, 40 | "CC_Charge_Only": {'func_call': '', 'str_chg_opt': False}, 41 | "CC_Discharge_Only": {'func_call': '', 'str_chg_opt': False}, 42 | "Step_Cycle": {'func_call': '', 'str_chg_opt': False}, 43 | "Continuous_Step_Cycles": {'func_call': '', 'str_chg_opt': True}, 44 | "Single_IR_Test": {'func_call': '', 'str_chg_opt': False}, 45 | "Repeated_IR_Discharge_Test": {'func_call': '', 'str_chg_opt': True}, 46 | "Rest": {'func_call': '', 'str_chg_opt': False} 47 | } 48 | 49 | cycle_requirements = { 50 | "charge": {'load_req': False, 'supply_req': True}, 51 | "discharge": {'load_req': True, 'supply_req': False}, 52 | "step": {'load_req': False, 'supply_req': False}, 53 | "rest": {'load_req': False, 'supply_req': False}, 54 | "cycle": {'load_req': True, 'supply_req': True} 55 | } 56 | 57 | ############### CYCLE ####################### 58 | class CycleSettings: 59 | 60 | def __init__(self): 61 | self.settings = { 62 | "cycle_type": 'cycle', 63 | "cycle_display": 'Cycle', 64 | "charge_end_v": 4.2, 65 | "charge_a": 1, 66 | "charge_end_a": 0.3, 67 | "rest_after_charge_min": 20, 68 | "discharge_end_v": 2.5, 69 | "discharge_a": -1, 70 | "rest_after_discharge_min": 20, 71 | "meas_log_int_s": 1, 72 | "safety_min_voltage_v": 2.45, 73 | "safety_max_voltage_v": 4.25, 74 | "safety_min_current_a": -10, 75 | "safety_max_current_a": 10, 76 | "safety_max_time_s": 100 #positive values give a value in seconds, negative values and 0 will disable the time safety check 77 | } 78 | self.valid_strings = { 79 | "cycle_type": ('cycle',), 80 | "cycle_display": ('Cycle',) 81 | } 82 | 83 | def get_cycle_settings(self, cycle_name = ""): 84 | self.settings = jsonIO.get_cycle_settings(self.settings, self.valid_strings, cycle_name) 85 | 86 | def export_cycle_settings(self, cycle_name = ""): 87 | jsonIO.export_cycle_settings(self.settings, cycle_name) 88 | 89 | def import_cycle_settings(self, cycle_name = ""): 90 | self.settings = jsonIO.import_cycle_settings(cycle_name) 91 | 92 | ############### CHARGE ##################### 93 | 94 | class ChargeSettings(CycleSettings): 95 | 96 | def __init__(self): 97 | self.settings = { 98 | "cycle_type": 'charge', 99 | "cycle_display": 'Charge', 100 | "charge_end_v": 4.2, 101 | "charge_a": 1, 102 | "charge_end_a": 0.1, 103 | "meas_log_int_s": 1, 104 | "safety_min_voltage_v": 2.45, 105 | "safety_max_voltage_v": 4.25, 106 | "safety_min_current_a": -10, 107 | "safety_max_current_a": 10, 108 | "safety_max_time_s": 100 #positive values give a value in seconds, negative values and 0 will disable the time safety check 109 | } 110 | self.valid_strings = { 111 | "cycle_type": ('charge',), 112 | "cycle_display": ('Charge',) 113 | } 114 | 115 | ############### DISCHARGE ##################### 116 | 117 | class DischargeSettings(CycleSettings): 118 | 119 | def __init__(self): 120 | self.settings = { 121 | "cycle_type": 'discharge', 122 | "cycle_display": 'Discharge', 123 | "discharge_end_v": 2.5, 124 | "discharge_a": -1, 125 | "meas_log_int_s": 1, 126 | "safety_min_voltage_v": 2.45, 127 | "safety_max_voltage_v": 4.25, 128 | "safety_min_current_a": -10, 129 | "safety_max_current_a": 10, 130 | "safety_max_time_s": 100 #positive values give a value in seconds, negative values and 0 will disable the time safety check 131 | } 132 | self.valid_strings = { 133 | "cycle_type": ('discharge',), 134 | "cycle_display": ('Discharge',) 135 | } 136 | 137 | ############### REST ##################### 138 | 139 | class RestSettings(CycleSettings): 140 | 141 | def __init__(self): 142 | self.settings = { 143 | "cycle_type": 'rest', 144 | "cycle_display": 'Rest', 145 | "rest_time_min": 5, 146 | "meas_log_int_s": 1, 147 | "safety_min_voltage_v": 2.45, 148 | "safety_max_voltage_v": 4.25, 149 | "safety_min_current_a": -10, 150 | "safety_max_current_a": 10, 151 | "safety_max_time_s": 100 #positive values give a value in seconds, negative values and 0 will disable the time safety check 152 | } 153 | self.valid_strings = { 154 | "cycle_type": ('rest',), 155 | "cycle_display": ('Rest',) 156 | } 157 | 158 | ################# STEPS ############ 159 | 160 | class StepSettings(CycleSettings): 161 | def __init__(self): 162 | self.settings = { 163 | "cycle_type": 'step', 164 | "cycle_display": 'Step', 165 | "drive_style": 'none', #'current_a', 'voltage_v', 'none' 166 | "drive_value": 0, 167 | "drive_value_other": 0, 168 | "end_style": 'time_s', #'time_s', 'current_a', 'voltage_v' 169 | "end_condition": 'greater', #'greater', 'lesser' 170 | "end_value": 10, 171 | "meas_log_int_s": 1, 172 | "safety_min_voltage_v": 2.45, 173 | "safety_max_voltage_v": 4.25, 174 | "safety_min_current_a": -10, 175 | "safety_max_current_a": 10, 176 | "safety_max_time_s": 100 #positive values give a value in seconds, negative values and 0 will disable the time safety check 177 | } 178 | self.valid_strings = { 179 | "cycle_type": ('step',), 180 | "cycle_display": ('Step',), 181 | "drive_style": ('current_a', 'voltage_v', 'none'), 182 | "end_style": ('time_s', 'current_a', 'voltage_v'), 183 | "end_condition": ('greater', 'lesser') 184 | } 185 | 186 | ################ INTERNAL RESISTANCE BATTERY TEST ############## 187 | 188 | class SingleIRSettings(CycleSettings): 189 | def __init__(self): 190 | self.settings = { 191 | "cycle_type": 'single_ir_test', 192 | "cycle_display": 'Single_IR_Test', 193 | "current_1_a": -1, 194 | "time_1_s": 5, 195 | "current_2_a": -5, 196 | "time_2_s": 5, 197 | "psu_voltage_if_pos_i": 0, 198 | "meas_log_int_s": 1, 199 | "safety_min_voltage_v": 2.45, 200 | "safety_max_voltage_v": 4.25, 201 | "safety_min_current_a": -10, 202 | "safety_max_current_a": 10, 203 | "safety_max_time_s": 100 #positive values give a value in seconds, negative values and 0 will disable the time safety check 204 | } 205 | self.valid_strings = { 206 | "cycle_type": ('single_ir_test',), 207 | "cycle_display": ('Single_IR_Test',) 208 | } 209 | 210 | class RepeatedIRDischargeSettings(CycleSettings): 211 | def __init__(self): 212 | self.settings = { 213 | "cycle_type": 'repeated_ir_discharge_test', 214 | "cycle_display": 'Repeated_IR_Discharge_Test', 215 | "charge_end_v": 4.2, 216 | "charge_a": 1, 217 | "charge_end_a": 0.1, 218 | "rest_after_charge_min": 20, 219 | "current_1_a": -1, 220 | "time_1_s": 5, 221 | "current_2_a": -5, 222 | "time_2_s": 5, 223 | "psu_voltage_if_pos_i": 0, 224 | "meas_log_int_s": 1, 225 | "cycle_end_voltage_v": 2.5, #if less than cycle_end_voltage_v, then end the cycle. Can also have cycle_end_time_s 226 | "rest_after_discharge_min": 20, 227 | "safety_min_voltage_v": 2.45, 228 | "safety_max_voltage_v": 4.25, 229 | "safety_min_current_a": -10, 230 | "safety_max_current_a": 10, 231 | "estimated_capacity_ah": 2.5 232 | } 233 | self.valid_strings = { 234 | "cycle_type": ('repeated_ir_discharge_test',), 235 | "cycle_display": ('Repeated_IR_Discharge_Test',) 236 | } 237 | 238 | #################### DC DC TESTING ############ 239 | 240 | class DcdcTestSettings(): 241 | 242 | def __init__(self): 243 | self.settings = { 244 | "psu_voltage_min": 4, 245 | "psu_voltage_max": 7, 246 | "num_voltage_steps": 4, 247 | "psu_current_limit_a": 2, 248 | "load_current_min": 0.1, 249 | "load_current_max": 1, 250 | "num_current_steps": 10, 251 | "step_delay_s": 2, 252 | "measurement_samples_for_avg": 10 253 | } 254 | self.valid_strings = {} 255 | 256 | class DcdcSweepSettings(): 257 | 258 | def __init__(self): 259 | self.settings = { 260 | "psu_voltage": 4, 261 | "psu_current_limit_a": 2, 262 | "load_current_min": 0.1, 263 | "load_current_max": 1, 264 | "num_current_steps": 10, 265 | "step_delay_s": 2, 266 | "measurement_samples_for_avg": 10 267 | } 268 | self.valid_strings = {} 269 | 270 | ######################### MPPT TESTING ################ 271 | 272 | class EloadCVSweepSettings(): 273 | 274 | def __init__(self): 275 | self.settings = { 276 | "min_cv_voltage": 0, 277 | "max_cv_voltage": 30, 278 | "num_voltage_steps": 10, 279 | "step_delay_s": 1, 280 | "measurement_samples_for_avg": 10 281 | } 282 | self.valid_strings = {} 283 | --------------------------------------------------------------------------------