hitl_tester.test_cases.bms.test_2590_cell_sims
Modified version of the tests in test_2590_live.py, for use by cell simulators.
Used in these test plans:
- new_2590_test ⠀⠀⠀(bms/new_2590_test.plan)
- milprf_dev_b ⠀⠀⠀(bms/milprf_dev_b.plan)
- milprf_dev_a ⠀⠀⠀(bms/milprf_dev_a.plan)
Example Command (warning: test plan may run other test cases):
./hitl_tester.py new_2590_test -DMAX_TIME=30 -DCELL_VOLTAGE=1.375 -DDEFAULT_SOC_PERCENT=0.8 -DDEFAULT_TEMPERATURE_C=15 -DREST_TIME=10800 -DREST_INTERVAL=10 -DDISCHARGE_VOLTAGE=10 -DDISCHARGE_CURRENT=2 -DMINIMUM_READINGS=3 -DCHARGE_INC_START=0 -DCHARGE_INC_STOP=3 -DCHARGE_INC_STEP=0.02 -DCHARGE_INC_TIME=20 -DSAMPLE_INTERVAL=10 -DWAKEUP_CURRENT=0.01 -DCYCLE_COUNT=6 -DCYCLE_CAPACITY=0.6
1""" 2Modified version of the tests in test_2590_live.py, for use by cell simulators. 3""" 4 5from __future__ import annotations 6 7import time 8 9import pytest 10 11from hitl_tester.modules.bms.bms_hw import BMSHardware 12from hitl_tester.modules.bms.event_watcher import SerialWatcher 13from hitl_tester.modules.bms.plateset import Plateset 14from hitl_tester.modules.bms_types import ( 15 OverVoltageError, 16 TimeoutExceededError, 17 UnderVoltageError, 18 DischargeType, 19 StopWatch, 20) 21from hitl_tester.modules.logger import logger 22 23MAX_TIME = 30 24CELL_VOLTAGE = 1.375 25 26DEFAULT_SOC_PERCENT = 0.80 27DEFAULT_TEMPERATURE_C = 15 28 29REST_TIME = 3 * 3600 30REST_INTERVAL = 10 31 32DISCHARGE_VOLTAGE = 10 33DISCHARGE_CURRENT = 2 34 35MINIMUM_READINGS = 3 36CHARGE_INC_START = 0 37CHARGE_INC_STOP = 3 38CHARGE_INC_STEP = 0.020 39CHARGE_INC_TIME = 20 40SAMPLE_INTERVAL = 10 41 42WAKEUP_CURRENT = 0.010 43 44CYCLE_COUNT = 6 45CYCLE_CAPACITY = 0.6 46 47bms_hardware = BMSHardware(pytest.flags) # type: ignore[arg-type] 48bms_hardware.init() 49 50serial_watcher = SerialWatcher() 51plateset = Plateset() 52 53 54def test_reset_cell_sims(): 55 """Activate cell sims and set appropriate temperatures.""" 56 logger.write_info_to_report("Powering down cell sims") 57 for cell in bms_hardware.cells.values(): 58 cell.disengage_safety_protocols = True 59 cell.volts = 0.0001 60 time.sleep(5) 61 logger.write_info_to_report("Powering up cell sims") 62 for cell in bms_hardware.cells.values(): 63 cell.state_of_charge = DEFAULT_SOC_PERCENT 64 cell.disengage_safety_protocols = False 65 logger.write_info_to_report(f"Setting temperature to {DEFAULT_TEMPERATURE_C}°C") 66 plateset.thermistor1 = DEFAULT_TEMPERATURE_C 67 plateset.thermistor2 = DEFAULT_TEMPERATURE_C 68 69 70def check_cell_imbalance_conditions(high_cell_id: int): 71 """Set one cell 100mV above the rest, confirm its flag is set, and that it has a higher current.""" 72 serial_watcher.assert_true(f"flags.output_c{high_cell_id}_bal", False) 73 old_cell_current = bms_hardware.cells[high_cell_id].amps # NOTE: Do I need to be (dis)charging? 74 for i, cell in bms_hardware.cells.items(): 75 logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A") 76 77 logger.write_info_to_report(f"Cell {high_cell_id} = 4.0 V") 78 bms_hardware.cells[high_cell_id].exact_volts = 4.0 79 for cell_id in bms_hardware.cells: 80 serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", cell_id == high_cell_id) 81 82 # We should see the current increase when a flag is set. The current will be cell voltage / 100 83 cell_current = bms_hardware.cells[high_cell_id].amps 84 for i, cell in bms_hardware.cells.items(): 85 logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A") 86 logger.write_info_to_report(f"{cell_current} A > {old_cell_current} A?") 87 assert cell_current > old_cell_current 88 new_cell_current = cell_current - old_cell_current 89 expected_current_a = bms_hardware.cells[high_cell_id].volts / 100 90 logger.write_info_to_report(f"{new_cell_current} A ≈ {expected_current_a} A") 91 current_error_a = 0.02 92 assert expected_current_a - current_error_a <= new_cell_current <= expected_current_a + current_error_a 93 94 # Reset the higher voltage 95 logger.write_info_to_report(f"Cell {high_cell_id} = 3.9 V") 96 bms_hardware.cells[high_cell_id].exact_volts = 3.9 97 for i, cell in bms_hardware.cells.items(): 98 logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V (target: {cell.volts}), {cell.amps} A") 99 for cell_id in bms_hardware.cells: 100 serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False) 101 102 103def test_cell_imbalance(): 104 """ 105 The flag is set when a cell is 4.2V+ or 20mv above the lowest cell. 106 107 What that is actually doing though is connecting a resistor across the cell that is too high in voltage. 108 If you monitor the cell simulator current you should actually see the current increase in that cell if 109 the flag get set. The current would be Vcell / 100 since they use 100 Ohm resistors. 110 """ 111 logger.write_info_to_report("Testing Cell Imbalance") 112 serial_watcher.start() 113 for cell in bms_hardware.cells.values(): 114 cell.exact_volts = 3.9 115 for cell_id in bms_hardware.cells: 116 serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False) 117 118 # Resting 119 logger.write_info_to_report("Resting") 120 for cell_id in bms_hardware.cells: 121 check_cell_imbalance_conditions(cell_id) 122 123 # Discharging 124 logger.write_info_to_report("Discharging at 100 mA") 125 bms_hardware.load.mode_cc() 126 bms_hardware.load.amps = 0.100 127 bms_hardware.load.enable() 128 for cell_id in bms_hardware.cells: 129 check_cell_imbalance_conditions(cell_id) 130 bms_hardware.load.disable() 131 132 # Charging 133 logger.write_info_to_report("Charging at 100 mA") 134 bms_hardware.charger.set_profile(3.9 * 4, 0.1) 135 plateset.ce_switch = True 136 bms_hardware.charger.enable() 137 for cell_id in bms_hardware.cells: 138 check_cell_imbalance_conditions(cell_id) 139 bms_hardware.charger.disable() 140 plateset.ce_switch = False 141 142 serial_watcher.stop() 143 144 145def test_charge_enable(): 146 """ 147 Confirm we can charge in excess of 400 mA when charge enable is on. 148 If battery is near full charge, you may not be able to go past 400 mA when charging. 149 150 We want to disable the charging current if charge enable is off if the current goes over 400 mA per 151 the spec. In reality, we want to disable if current is over 20 mA since that's what BT does and it's a safer way 152 to charge. Other OTS may have different cutoffs, and the BT one was measured by experiment. 153 """ 154 logger.write_info_to_report("Testing Charge Enable") 155 serial_watcher.start() 156 157 logger.write_info_to_report("Charge enable = 1") 158 plateset.ce_switch = True # Allows current charging in excess of 400 mA 159 serial_watcher.assert_true("ncharge_en_gpio", False, 1) 160 max_time = 60 161 elapsed_time = 0 162 charge_sample_interval = 5 163 termination_current = 0.100 164 current_limit = 0.400 + 0.100 # When to end the test 165 latest_i = termination_current + 0.1 166 ov_protection = 17 167 latest_v = 0 168 169 # Enable charging and timer 170 bms_hardware.charger.set_profile(volts=16.8, amps=3) 171 bms_hardware.charger.enable() # Enables the output of the charger 172 bms_hardware.timer.reset() # Keep track of runtime 173 174 # Charge until termination current, current limit, ov_protection, or max_time 175 while current_limit >= latest_i >= termination_current: 176 if latest_v >= ov_protection: 177 raise OverVoltageError( 178 f"Overvoltage protection triggered at {time.strftime('%x %X')}. " 179 f"Voltage {latest_v} is higher than {ov_protection}." 180 ) 181 if elapsed_time >= max_time: 182 raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds") 183 184 elapsed_time = bms_hardware.timer.elapsed_time 185 latest_v = bms_hardware.dmm.volts 186 latest_i = bms_hardware.charger.amps 187 logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}") 188 189 bms_hardware.csv.cycle.record(elapsed_time) 190 time.sleep(charge_sample_interval) 191 bms_hardware.charger.disable() # Charging is complete, turn off the charger 192 193 assert latest_i > current_limit 194 195 logger.write_info_to_report("Charge enable = 0") 196 plateset.ce_switch = False # Does not allow current charging in excess of 400 mA or less 197 serial_watcher.assert_true("ncharge_en_gpio", True, 2) 198 max_time = 60 199 elapsed_time = 0 200 charge_sample_interval = 1 201 termination_current = 0.0 202 current_limit = 0.400 + 0.100 # When to end the test 203 charge_current = 0.001 204 latest_i = termination_current + 0.1 205 ov_protection = 17 206 latest_v = 0 207 max_charge_current = 0 208 209 # Enable charging and timer 210 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 211 bms_hardware.charger.enable() # Enables the output of the charger 212 bms_hardware.timer.reset() # Keep track of runtime 213 214 # Charge until termination current, current limit, ov_protection, or max_time 215 while current_limit >= latest_i >= termination_current: 216 if latest_v >= ov_protection: 217 raise OverVoltageError( 218 f"Overvoltage protection triggered at {time.strftime('%x %X')}. " 219 f"Voltage {latest_v} is higher than {ov_protection}." 220 ) 221 if elapsed_time >= max_time: 222 raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds") 223 224 elapsed_time = bms_hardware.timer.elapsed_time 225 latest_v = bms_hardware.dmm.volts 226 latest_i = bms_hardware.charger.amps 227 max_charge_current = max(max_charge_current, latest_i) 228 logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}") 229 230 bms_hardware.csv.cycle.record(elapsed_time) 231 charge_current += 0.001 # Increase by 1mA per second 232 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 233 time.sleep(charge_sample_interval) 234 bms_hardware.charger.disable() # Charging is complete, turn off the charger 235 236 assert max_charge_current <= 0.400 237 238 serial_watcher.stop() 239 test_reset_cell_sims() 240 241 242def standard_charge( 243 charge_current: float = 2, 244 max_time: int = 3 * 3600, 245 sample_interval: int = 10, 246 minimum_readings: int = 3, 247 termination_current: float = 0.100, 248): 249 """ 250 Charge batteries in accordance with 4.3.1 for not greater than three hours. 251 4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge. 252 """ 253 bms_hardware.voltage = 16.8 254 bms_hardware.ov_protection = bms_hardware.voltage + 0.050 # 50mV above the charging voltage 255 bms_hardware.current = charge_current 256 bms_hardware.termination_current = termination_current # 100 mA 257 bms_hardware.max_time = max_time 258 bms_hardware.sample_interval = sample_interval 259 bms_hardware.minimum_readings = minimum_readings 260 261 # Run the Charge cycle 262 bms_hardware.run_li_charge_cycle() 263 264 265def standard_discharge( 266 discharge_current: float = DISCHARGE_CURRENT, 267 max_time: int = 8 * 3600, 268 sample_interval: int = 10, 269 discharge_voltage: float = DISCHARGE_VOLTAGE, 270): 271 """Discharge at 2A until 10V.""" 272 bms_hardware.voltage = discharge_voltage 273 bms_hardware.uv_protection = bms_hardware.voltage - 0.500 # 500mV below voltage cutoff 274 bms_hardware.current = discharge_current 275 bms_hardware.discharge_type = DischargeType.CONSTANT_CURRENT 276 bms_hardware.max_time = max_time 277 bms_hardware.sample_interval = sample_interval 278 279 # Run the discharge cycle, returning the capacity 280 capacity = bms_hardware.run_discharge_cycle() 281 logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh") 282 return capacity 283 284 285def standard_rest(seconds: int = 0, sample_interval: int = 0): 286 """Stabilize the batteries for 2+ hours.""" 287 bms_hardware.max_time = seconds or REST_TIME 288 bms_hardware.sample_interval = sample_interval or SAMPLE_INTERVAL 289 bms_hardware.run_resting_cycle() 290 291 292def test_full_discharge(): 293 """Perform a discharge from full.""" 294 for cell in bms_hardware.cells.values(): 295 cell.state_of_charge = 1.00 296 old_cycle_function = bms_hardware.csv.cycle 297 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 298 bms_hardware.csv.cycle.create_file() 299 standard_discharge(sample_interval=10) 300 bms_hardware.csv.cycle = old_cycle_function 301 302 303def test_capacity_discharge(): 304 """ 305 Discharge at 2A (per Mil-prf/5) and capture voltage, coulomb, temp, (normal full test) as well as SMBus. 306 Ping SMBus a total of 1800 times over the 5 hour test (i.e. every 10 seconds). 307 """ 308 old_cycle_function = bms_hardware.csv.cycle 309 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 310 bms_hardware.csv.cycle.create_file() 311 plateset.ce_switch = True # Allows current charging in excess of 400 mA 312 standard_charge(max_time=6 * 3600, sample_interval=10, charge_current=2) 313 plateset.ce_switch = False 314 standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE) 315 bms_hardware.csv.cycle = old_cycle_function 316 317 318def test_low_voltage_charge(): 319 """ 320 | Requirement | Charge from any starting voltage | 321 | :------------------- | :-------------------------------- | 322 | GitHub Issue | #161 (HITL), #250 (BMS) | 323 | Instructions | 1. Power down the cells to 0V to shut down the BMS </br>\ 324 2. Power up the cells to our starting voltage </br>\ 325 3. Start a 2A charge | 326 | Pass / Fail Criteria | Fail if we are unable to charge | 327 | Configuration | ⦁ `CELL_VOLTAGE` = Starting voltage </br>\ 328 ⦁ `MAX_TIME` = Charge time </br>\ 329 ⦁ `MINIMUM_READINGS` = Minimum low current readings to take before ending early | 330 """ 331 old_cycle_function = bms_hardware.csv.cycle 332 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 333 bms_hardware.csv.cycle.create_file() 334 335 logger.write_info_to_report("Powering down cell sims") 336 for cell in bms_hardware.cells.values(): 337 cell.disengage_safety_protocols = True 338 cell.volts = 0.0001 339 time.sleep(5) 340 logger.write_info_to_report("Powering up cell sims") 341 for cell in bms_hardware.cells.values(): 342 cell.volts = CELL_VOLTAGE 343 cell.disengage_safety_protocols = False 344 time.sleep(5) 345 346 plateset.ce_switch = True # Allows current charging in excess of 400 mA 347 try: 348 standard_charge(max_time=MAX_TIME, sample_interval=1, charge_current=2, minimum_readings=MINIMUM_READINGS) 349 except TimeoutExceededError: 350 bms_hardware.charger.disable() 351 plateset.ce_switch = False 352 353 bms_hardware.csv.cycle = old_cycle_function 354 355 356def test_0v_works(): 357 """ 358 | Requirement | Test charging/discharging with 0V | 359 | :------------------- | :-------------------------------- | 360 | GitHub Issue | #161 (HITL), #250 (BMS) | 361 | Instructions | 1. Power up BMS </br>\ 362 ⠀⠀⦁ Power cells to a terminal voltage in the range 5.25V-5.75V </br>\ 363 ⠀⠀⦁ Power up to target voltage without causing an imbalance </br>\ 364 3. Discharge for 400 seconds at 2A (down to 0V) </br>\ 365 4. Reset BMS (simulating long discharge at rest) </br>\ 366 ⠀⠀⦁ Power down cell sims to 0V </br>\ 367 ⠀⠀⦁ Power up cell sims to 0.1V </br>\ 368 5. Charge for 200 seconds at 2A </br>\ 369 6. Discharge for 260 seconds at 2A (down to 0V) | 370 | Pass / Fail Criteria | Fail if we are unable to charge or discharge | 371 """ 372 old_cycle_function = bms_hardware.csv.cycle 373 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 374 bms_hardware.csv.cycle.create_file() 375 for cell in bms_hardware.cells.values(): # Total voltage = 5.5V 376 cell.exact_volts = 1.375 377 bms_hardware.resting_time = 4 378 bms_hardware.resting_sample_interval = 1 379 bms_hardware.run_resting_cycle() 380 381 for cell in bms_hardware.cells.values(): # Total voltage = 8V 382 cell.exact_volts = 2.0 383 bms_hardware.resting_time = 4 384 bms_hardware.resting_sample_interval = 1 385 bms_hardware.run_resting_cycle() 386 387 for cell in bms_hardware.cells.values(): # Total voltage = 12V (we set it to 2V first to avoid imbalance) 388 cell.exact_volts = 3.0 389 390 try: 391 standard_discharge(max_time=400, discharge_voltage=-0.1, sample_interval=1) 392 except TimeoutExceededError: 393 bms_hardware.load.disable() 394 395 logger.write_info_to_report("Powering down cell sims") 396 for cell in bms_hardware.cells.values(): 397 cell.disengage_safety_protocols = True 398 cell.volts = 0.0001 399 time.sleep(5) 400 logger.write_info_to_report("Powering up cell sims") 401 for cell in bms_hardware.cells.values(): 402 cell.volts = 0.1 403 cell.disengage_safety_protocols = False 404 405 plateset.ce_switch = True # Allows current charging in excess of 400 mA 406 try: 407 standard_charge(max_time=200, sample_interval=1, charge_current=2) 408 except TimeoutExceededError: 409 bms_hardware.charger.disable() 410 plateset.ce_switch = False 411 412 try: 413 standard_discharge(max_time=260, discharge_voltage=-0.1, sample_interval=1) 414 except TimeoutExceededError: 415 bms_hardware.load.disable() 416 417 bms_hardware.csv.cycle = old_cycle_function 418 419 420def test_short_discharge(): 421 """30 min discharge.""" 422 for cell in bms_hardware.cells.values(): 423 cell.state_of_charge = 0.10 # 10% FIXME(JA): figure out the correct soc for 20-30 min 424 old_cycle_function = bms_hardware.csv.cycle 425 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 426 bms_hardware.csv.cycle.create_file() 427 standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE) 428 bms_hardware.csv.cycle = old_cycle_function 429 430 431def test_timed_discharge(): 432 """Timed discharge.""" 433 for cell in bms_hardware.cells.values(): 434 cell.state_of_charge = 0.80 435 old_cycle_function = bms_hardware.csv.cycle 436 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 437 bms_hardware.csv.cycle.create_file() 438 try: 439 standard_discharge(sample_interval=10, max_time=60 * 60) 440 except TimeoutExceededError: 441 bms_hardware.load.disable() 442 bms_hardware.csv.cycle = old_cycle_function 443 444 445def test_columb_count(): 446 """Short discharge, followed by a short rest below -113 mA.""" 447 for cell in bms_hardware.cells.values(): 448 cell.state_of_charge = 0.80 # 80% 449 old_cycle_function = bms_hardware.csv.cycle 450 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 451 bms_hardware.csv.cycle.create_file() 452 try: 453 standard_discharge(sample_interval=10, max_time=30 * 60) 454 except TimeoutExceededError: 455 pass 456 try: 457 standard_discharge(sample_interval=10, max_time=30 * 60, discharge_current=0.100) 458 except TimeoutExceededError: 459 bms_hardware.load.disable() 460 test_resting(max_time=2 * 60) 461 bms_hardware.csv.cycle = old_cycle_function 462 463 464def test_live_cell_undervoltage(): 465 """Discharge down to 2.5 volts and cause an undervoltage discharge fault, clear by CE and charging.""" 466 logger.write_info_to_report("Testing Live Cell Undervoltage") 467 serial_watcher.start() 468 cell = None 469 for cell in bms_hardware.cells.values(): 470 cell.volts = 3.5 471 if cell: 472 cell.disengage_safety_protocols = True 473 cell.volts = 2.5 474 old_cycle_function = bms_hardware.csv.cycle 475 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 476 bms_hardware.csv.cycle.create_file() 477 478 # Discharge until undervoltage 479 try: 480 standard_discharge(sample_interval=10, discharge_voltage=9) 481 except UnderVoltageError: 482 bms_hardware.load.disable() 483 serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True) 484 485 # Attempt to charge back up 486 plateset.ce_switch = True # Allows current charging in excess of 400 mA 487 serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False) 488 try: 489 standard_charge(max_time=2 * 60, sample_interval=10, charge_current=2) 490 except TimeoutExceededError: 491 bms_hardware.charger.disable() 492 plateset.ce_switch = False 493 for cell in bms_hardware.cells.values(): 494 assert cell.volts >= 2.6 495 cell.disengage_safety_protocols = False 496 497 bms_hardware.csv.cycle = old_cycle_function 498 serial_watcher.stop() 499 500 501def test_5v_charge(): 502 """Check if we can charge the cells back up from as low as 5V.""" 503 logger.write_info_to_report("Testing 5V Charging") 504 serial_watcher.start() 505 for cell in bms_hardware.cells.values(): 506 cell.disengage_safety_protocols = True 507 cell.volts = 1.4 508 old_cycle_function = bms_hardware.csv.cycle 509 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 510 bms_hardware.csv.cycle.create_file() 511 512 plateset.ce_switch = True # Allows current charging in excess of 400 mA 513 try: 514 standard_charge(max_time=5 * 60, sample_interval=10, charge_current=2) 515 except TimeoutExceededError: 516 bms_hardware.charger.disable() 517 plateset.ce_switch = False 518 519 for cell in bms_hardware.cells.values(): 520 assert cell.volts >= 2.6 # FIXME(JA): set to expected voltage after 5 minutes 521 cell.disengage_safety_protocols = False 522 523 bms_hardware.csv.cycle = old_cycle_function 524 serial_watcher.stop() 525 526 527def test_resting(max_time: int = 30, sample_interval=10): 528 """Record SMBus data while at rest.""" 529 old_cycle_function = bms_hardware.csv.cycle 530 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 531 bms_hardware.csv.cycle.create_file() 532 start_time = time.perf_counter() 533 while time.perf_counter() - start_time <= max_time: 534 bms_hardware.csv.cycle.record(time.perf_counter() - start_time, 0.0) 535 time.sleep(sample_interval) 536 bms_hardware.csv.cycle = old_cycle_function 537 538 539def test_standard_rest(): 540 """Stabilize the batteries for some time.""" 541 old_cycle_function = bms_hardware.csv.cycle 542 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 543 standard_rest() 544 bms_hardware.csv.cycle = old_cycle_function 545 546 547def test_soc_rest(): 548 """ 549 We can change cell voltages while the state machine is relaxed, does smbus and serial report the proper state of 550 charge? 551 """ 552 logger.write_info_to_report("Testing SOC Rest") 553 serial_watcher.start() 554 old_cycle_function = bms_hardware.csv.cycle 555 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 556 bms_hardware.csv.cycle.create_file() 557 for soc in range(10, 100, 10): # 10% to 90% 558 for cell in bms_hardware.cells.values(): 559 cell.state_of_charge = soc / 100 560 test_resting(max_time=30, sample_interval=10) 561 serial_watcher.assert_true("percent_charged", soc, error=(5, 5)) 562 563 bms_hardware.csv.cycle = old_cycle_function 564 serial_watcher.stop() 565 566 567def test_soc_discharge(): 568 """ 569 Discharging: exceed threshold of 113mamps, we discharge at 1 amp for ~30 minutes, have bms report back estimated 570 state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the 571 time between excited and fully relaxed. 572 """ 573 logger.write_info_to_report("Testing SOC Discharging") 574 serial_watcher.start() 575 old_cycle_function = bms_hardware.csv.cycle 576 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 577 bms_hardware.csv.cycle.create_file() 578 579 for cell in bms_hardware.cells.values(): 580 cell.state_of_charge = 0.80 581 try: 582 standard_discharge(max_time=30 * 60, sample_interval=10, discharge_current=1) 583 except TimeoutExceededError: 584 bms_hardware.load.disable() 585 test_resting(max_time=30 * 60) 586 587 bms_hardware.csv.cycle = old_cycle_function 588 serial_watcher.stop() 589 590 591def test_soc_charge(): 592 """ 593 Charging: exceed threshold of 143mamps, we charge at 1 amp for ~30 minutes, have bms report back estimated 594 state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the 595 time between excited and fully relaxed. 596 """ 597 logger.write_info_to_report("Testing SOC Charging") 598 serial_watcher.start() 599 old_cycle_function = bms_hardware.csv.cycle 600 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 601 bms_hardware.csv.cycle.create_file() 602 603 for cell in bms_hardware.cells.values(): 604 cell.state_of_charge = 0.20 605 plateset.ce_switch = True 606 try: 607 standard_charge(max_time=30 * 60, sample_interval=10, charge_current=1) 608 except TimeoutExceededError: 609 bms_hardware.charger.disable() 610 plateset.ce_switch = False 611 test_resting(max_time=30 * 60) 612 613 bms_hardware.csv.cycle = old_cycle_function 614 serial_watcher.stop() 615 616 617def test_wakeup_counter(): 618 """ 619 Charge at around 10mA to repeatedly trigger the wakeup counter. 620 This occurs as we are very close to the trigger point. 621 """ 622 logger.write_info_to_report("Testing Wakeup Counter") 623 old_cycle_function = bms_hardware.csv.cycle 624 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 625 bms_hardware.csv.cycle.create_file() 626 serial_watcher.start(csv=True) 627 628 for cell in bms_hardware.cells.values(): 629 cell.state_of_charge = 0.20 630 plateset.ce_switch = True 631 try: 632 standard_charge(max_time=5 * 60, sample_interval=1, charge_current=WAKEUP_CURRENT, termination_current=0.000001) 633 except TimeoutExceededError: 634 bms_hardware.charger.disable() 635 plateset.ce_switch = False 636 637 assert serial_watcher.events["Wakeup_Counter"][0].value == serial_watcher.events["Wakeup_Counter"][-1].value 638 639 serial_watcher.stop() 640 bms_hardware.csv.cycle = old_cycle_function 641 642 643def test_charge_increment(): 644 """Increment current.""" 645 logger.write_info_to_report("Charging Incrementally") 646 old_cycle_function = bms_hardware.csv.cycle 647 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 648 649 max_time = 3 * 60 * 60 650 elapsed_time = 0 651 charge_sample_interval = 1 652 termination_current = 0.0 653 current_limit = CHARGE_INC_STOP 654 charge_current = CHARGE_INC_START 655 latest_i = termination_current + 0.1 656 ov_protection = 17 657 latest_v = 0 658 max_charge_current = 0 659 660 current_time = StopWatch() 661 current_time.start() 662 663 # Enable charging and timer 664 plateset.ce_switch = True # Does not allow current charging in excess of 400 mA or less 665 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 666 bms_hardware.charger.enable() # Enables the output of the charger 667 bms_hardware.timer.reset() # Keep track of runtime 668 669 # Charge until termination current, current limit, ov_protection, or max_time 670 while current_limit >= latest_i >= termination_current: 671 if latest_v >= ov_protection: 672 raise OverVoltageError( 673 f"Overvoltage protection triggered at {time.strftime('%x %X')}. " 674 f"Voltage {latest_v} is higher than {ov_protection}." 675 ) 676 if elapsed_time >= max_time: 677 raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds") 678 679 elapsed_time = bms_hardware.timer.elapsed_time 680 latest_v = bms_hardware.dmm.volts 681 latest_i = bms_hardware.charger.amps 682 max_charge_current = max(max_charge_current, latest_i) 683 logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}") 684 685 bms_hardware.csv.cycle.record(elapsed_time) 686 if current_time.elapsed_time > CHARGE_INC_TIME: 687 current_time.reset() 688 charge_current += CHARGE_INC_STEP 689 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 690 time.sleep(charge_sample_interval) 691 bms_hardware.charger.disable() # Charging is complete, turn off the charger 692 plateset.ce_switch = False 693 bms_hardware.csv.cycle = old_cycle_function 694 695 696def test_cell_sim_voltage(): 697 """Set the cell sims to some voltage.""" 698 # old_cycle_function = bms_hardware.csv.cycle 699 # bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 700 # bms_hardware.csv.cycle.create_file() 701 702 logger.write_info_to_report("Powering down cell sims") 703 for cell in bms_hardware.cells.values(): 704 cell.disengage_safety_protocols = True 705 cell.volts = 0.0001 706 time.sleep(5) 707 logger.write_info_to_report("Powering up cell sims") 708 for cell in bms_hardware.cells.values(): 709 cell.volts = CELL_VOLTAGE 710 cell.disengage_safety_protocols = False 711 # for cell in bms_hardware.cells.values(): 712 # cell.exact_volts = CELL_VOLTAGE 713 logger.write_info_to_report("Setting temperature to 15°C") 714 plateset.thermistor1 = DEFAULT_TEMPERATURE_C 715 plateset.thermistor2 = DEFAULT_TEMPERATURE_C 716 717 time.sleep(5) 718 719 standard_discharge(sample_interval=10, discharge_current=DISCHARGE_CURRENT / 1000, discharge_voltage=-0.05) 720 721 bms_hardware.resting_time = REST_TIME 722 bms_hardware.max_time = REST_TIME 723 bms_hardware.resting_sample_interval = REST_INTERVAL 724 bms_hardware.sample_interval = REST_INTERVAL 725 bms_hardware.run_resting_cycle() 726 727 # bms_hardware.csv.cycle = old_cycle_function 728 729 730def test_cycle_count(): 731 """Repeat capacity test to confirm cycle count functionality.""" 732 test_reset_cell_sims() 733 734 old_cycle_function = bms_hardware.csv.cycle 735 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 736 bms_hardware.csv.cycle.create_file() 737 # serial_watcher.start(csv=True) 738 739 # Change cell sim capacity 740 for cell in bms_hardware.cells.values(): 741 cell.data.capacity = CYCLE_CAPACITY 742 743 # Run cycles 744 for i in range(CYCLE_COUNT): 745 logger.write_info_to_report(f"Running cycle {i}") 746 plateset.ce_switch = True # Allows current charging in excess of 400 mA 747 standard_charge(termination_current=1.0) 748 plateset.ce_switch = False 749 standard_discharge(discharge_voltage=2.9 * 4) 750 751 # serial_watcher.stop() 752 bms_hardware.csv.cycle = old_cycle_function 753 754 # FIXME(JA): merge NGI branch to allow threads to access the same hardware 755 # assert serial_watcher.events["charge_cycles"][-1].value == CYCLE_COUNT
55def test_reset_cell_sims(): 56 """Activate cell sims and set appropriate temperatures.""" 57 logger.write_info_to_report("Powering down cell sims") 58 for cell in bms_hardware.cells.values(): 59 cell.disengage_safety_protocols = True 60 cell.volts = 0.0001 61 time.sleep(5) 62 logger.write_info_to_report("Powering up cell sims") 63 for cell in bms_hardware.cells.values(): 64 cell.state_of_charge = DEFAULT_SOC_PERCENT 65 cell.disengage_safety_protocols = False 66 logger.write_info_to_report(f"Setting temperature to {DEFAULT_TEMPERATURE_C}°C") 67 plateset.thermistor1 = DEFAULT_TEMPERATURE_C 68 plateset.thermistor2 = DEFAULT_TEMPERATURE_C
Activate cell sims and set appropriate temperatures.
71def check_cell_imbalance_conditions(high_cell_id: int): 72 """Set one cell 100mV above the rest, confirm its flag is set, and that it has a higher current.""" 73 serial_watcher.assert_true(f"flags.output_c{high_cell_id}_bal", False) 74 old_cell_current = bms_hardware.cells[high_cell_id].amps # NOTE: Do I need to be (dis)charging? 75 for i, cell in bms_hardware.cells.items(): 76 logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A") 77 78 logger.write_info_to_report(f"Cell {high_cell_id} = 4.0 V") 79 bms_hardware.cells[high_cell_id].exact_volts = 4.0 80 for cell_id in bms_hardware.cells: 81 serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", cell_id == high_cell_id) 82 83 # We should see the current increase when a flag is set. The current will be cell voltage / 100 84 cell_current = bms_hardware.cells[high_cell_id].amps 85 for i, cell in bms_hardware.cells.items(): 86 logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V ({cell.volts}), {cell.amps} A") 87 logger.write_info_to_report(f"{cell_current} A > {old_cell_current} A?") 88 assert cell_current > old_cell_current 89 new_cell_current = cell_current - old_cell_current 90 expected_current_a = bms_hardware.cells[high_cell_id].volts / 100 91 logger.write_info_to_report(f"{new_cell_current} A ≈ {expected_current_a} A") 92 current_error_a = 0.02 93 assert expected_current_a - current_error_a <= new_cell_current <= expected_current_a + current_error_a 94 95 # Reset the higher voltage 96 logger.write_info_to_report(f"Cell {high_cell_id} = 3.9 V") 97 bms_hardware.cells[high_cell_id].exact_volts = 3.9 98 for i, cell in bms_hardware.cells.items(): 99 logger.write_debug_to_report(f"Cell {i}: {cell.measured_volts} V (target: {cell.volts}), {cell.amps} A") 100 for cell_id in bms_hardware.cells: 101 serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False)
Set one cell 100mV above the rest, confirm its flag is set, and that it has a higher current.
104def test_cell_imbalance(): 105 """ 106 The flag is set when a cell is 4.2V+ or 20mv above the lowest cell. 107 108 What that is actually doing though is connecting a resistor across the cell that is too high in voltage. 109 If you monitor the cell simulator current you should actually see the current increase in that cell if 110 the flag get set. The current would be Vcell / 100 since they use 100 Ohm resistors. 111 """ 112 logger.write_info_to_report("Testing Cell Imbalance") 113 serial_watcher.start() 114 for cell in bms_hardware.cells.values(): 115 cell.exact_volts = 3.9 116 for cell_id in bms_hardware.cells: 117 serial_watcher.assert_true(f"flags.output_c{cell_id}_bal", False) 118 119 # Resting 120 logger.write_info_to_report("Resting") 121 for cell_id in bms_hardware.cells: 122 check_cell_imbalance_conditions(cell_id) 123 124 # Discharging 125 logger.write_info_to_report("Discharging at 100 mA") 126 bms_hardware.load.mode_cc() 127 bms_hardware.load.amps = 0.100 128 bms_hardware.load.enable() 129 for cell_id in bms_hardware.cells: 130 check_cell_imbalance_conditions(cell_id) 131 bms_hardware.load.disable() 132 133 # Charging 134 logger.write_info_to_report("Charging at 100 mA") 135 bms_hardware.charger.set_profile(3.9 * 4, 0.1) 136 plateset.ce_switch = True 137 bms_hardware.charger.enable() 138 for cell_id in bms_hardware.cells: 139 check_cell_imbalance_conditions(cell_id) 140 bms_hardware.charger.disable() 141 plateset.ce_switch = False 142 143 serial_watcher.stop()
The flag is set when a cell is 4.2V+ or 20mv above the lowest cell.
What that is actually doing though is connecting a resistor across the cell that is too high in voltage. If you monitor the cell simulator current you should actually see the current increase in that cell if the flag get set. The current would be Vcell / 100 since they use 100 Ohm resistors.
146def test_charge_enable(): 147 """ 148 Confirm we can charge in excess of 400 mA when charge enable is on. 149 If battery is near full charge, you may not be able to go past 400 mA when charging. 150 151 We want to disable the charging current if charge enable is off if the current goes over 400 mA per 152 the spec. In reality, we want to disable if current is over 20 mA since that's what BT does and it's a safer way 153 to charge. Other OTS may have different cutoffs, and the BT one was measured by experiment. 154 """ 155 logger.write_info_to_report("Testing Charge Enable") 156 serial_watcher.start() 157 158 logger.write_info_to_report("Charge enable = 1") 159 plateset.ce_switch = True # Allows current charging in excess of 400 mA 160 serial_watcher.assert_true("ncharge_en_gpio", False, 1) 161 max_time = 60 162 elapsed_time = 0 163 charge_sample_interval = 5 164 termination_current = 0.100 165 current_limit = 0.400 + 0.100 # When to end the test 166 latest_i = termination_current + 0.1 167 ov_protection = 17 168 latest_v = 0 169 170 # Enable charging and timer 171 bms_hardware.charger.set_profile(volts=16.8, amps=3) 172 bms_hardware.charger.enable() # Enables the output of the charger 173 bms_hardware.timer.reset() # Keep track of runtime 174 175 # Charge until termination current, current limit, ov_protection, or max_time 176 while current_limit >= latest_i >= termination_current: 177 if latest_v >= ov_protection: 178 raise OverVoltageError( 179 f"Overvoltage protection triggered at {time.strftime('%x %X')}. " 180 f"Voltage {latest_v} is higher than {ov_protection}." 181 ) 182 if elapsed_time >= max_time: 183 raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds") 184 185 elapsed_time = bms_hardware.timer.elapsed_time 186 latest_v = bms_hardware.dmm.volts 187 latest_i = bms_hardware.charger.amps 188 logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}") 189 190 bms_hardware.csv.cycle.record(elapsed_time) 191 time.sleep(charge_sample_interval) 192 bms_hardware.charger.disable() # Charging is complete, turn off the charger 193 194 assert latest_i > current_limit 195 196 logger.write_info_to_report("Charge enable = 0") 197 plateset.ce_switch = False # Does not allow current charging in excess of 400 mA or less 198 serial_watcher.assert_true("ncharge_en_gpio", True, 2) 199 max_time = 60 200 elapsed_time = 0 201 charge_sample_interval = 1 202 termination_current = 0.0 203 current_limit = 0.400 + 0.100 # When to end the test 204 charge_current = 0.001 205 latest_i = termination_current + 0.1 206 ov_protection = 17 207 latest_v = 0 208 max_charge_current = 0 209 210 # Enable charging and timer 211 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 212 bms_hardware.charger.enable() # Enables the output of the charger 213 bms_hardware.timer.reset() # Keep track of runtime 214 215 # Charge until termination current, current limit, ov_protection, or max_time 216 while current_limit >= latest_i >= termination_current: 217 if latest_v >= ov_protection: 218 raise OverVoltageError( 219 f"Overvoltage protection triggered at {time.strftime('%x %X')}. " 220 f"Voltage {latest_v} is higher than {ov_protection}." 221 ) 222 if elapsed_time >= max_time: 223 raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds") 224 225 elapsed_time = bms_hardware.timer.elapsed_time 226 latest_v = bms_hardware.dmm.volts 227 latest_i = bms_hardware.charger.amps 228 max_charge_current = max(max_charge_current, latest_i) 229 logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}") 230 231 bms_hardware.csv.cycle.record(elapsed_time) 232 charge_current += 0.001 # Increase by 1mA per second 233 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 234 time.sleep(charge_sample_interval) 235 bms_hardware.charger.disable() # Charging is complete, turn off the charger 236 237 assert max_charge_current <= 0.400 238 239 serial_watcher.stop() 240 test_reset_cell_sims()
Confirm we can charge in excess of 400 mA when charge enable is on. If battery is near full charge, you may not be able to go past 400 mA when charging.
We want to disable the charging current if charge enable is off if the current goes over 400 mA per the spec. In reality, we want to disable if current is over 20 mA since that's what BT does and it's a safer way to charge. Other OTS may have different cutoffs, and the BT one was measured by experiment.
243def standard_charge( 244 charge_current: float = 2, 245 max_time: int = 3 * 3600, 246 sample_interval: int = 10, 247 minimum_readings: int = 3, 248 termination_current: float = 0.100, 249): 250 """ 251 Charge batteries in accordance with 4.3.1 for not greater than three hours. 252 4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge. 253 """ 254 bms_hardware.voltage = 16.8 255 bms_hardware.ov_protection = bms_hardware.voltage + 0.050 # 50mV above the charging voltage 256 bms_hardware.current = charge_current 257 bms_hardware.termination_current = termination_current # 100 mA 258 bms_hardware.max_time = max_time 259 bms_hardware.sample_interval = sample_interval 260 bms_hardware.minimum_readings = minimum_readings 261 262 # Run the Charge cycle 263 bms_hardware.run_li_charge_cycle()
Charge batteries in accordance with 4.3.1 for not greater than three hours. 4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge.
266def standard_discharge( 267 discharge_current: float = DISCHARGE_CURRENT, 268 max_time: int = 8 * 3600, 269 sample_interval: int = 10, 270 discharge_voltage: float = DISCHARGE_VOLTAGE, 271): 272 """Discharge at 2A until 10V.""" 273 bms_hardware.voltage = discharge_voltage 274 bms_hardware.uv_protection = bms_hardware.voltage - 0.500 # 500mV below voltage cutoff 275 bms_hardware.current = discharge_current 276 bms_hardware.discharge_type = DischargeType.CONSTANT_CURRENT 277 bms_hardware.max_time = max_time 278 bms_hardware.sample_interval = sample_interval 279 280 # Run the discharge cycle, returning the capacity 281 capacity = bms_hardware.run_discharge_cycle() 282 logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh") 283 return capacity
Discharge at 2A until 10V.
286def standard_rest(seconds: int = 0, sample_interval: int = 0): 287 """Stabilize the batteries for 2+ hours.""" 288 bms_hardware.max_time = seconds or REST_TIME 289 bms_hardware.sample_interval = sample_interval or SAMPLE_INTERVAL 290 bms_hardware.run_resting_cycle()
Stabilize the batteries for 2+ hours.
293def test_full_discharge(): 294 """Perform a discharge from full.""" 295 for cell in bms_hardware.cells.values(): 296 cell.state_of_charge = 1.00 297 old_cycle_function = bms_hardware.csv.cycle 298 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 299 bms_hardware.csv.cycle.create_file() 300 standard_discharge(sample_interval=10) 301 bms_hardware.csv.cycle = old_cycle_function
Perform a discharge from full.
304def test_capacity_discharge(): 305 """ 306 Discharge at 2A (per Mil-prf/5) and capture voltage, coulomb, temp, (normal full test) as well as SMBus. 307 Ping SMBus a total of 1800 times over the 5 hour test (i.e. every 10 seconds). 308 """ 309 old_cycle_function = bms_hardware.csv.cycle 310 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 311 bms_hardware.csv.cycle.create_file() 312 plateset.ce_switch = True # Allows current charging in excess of 400 mA 313 standard_charge(max_time=6 * 3600, sample_interval=10, charge_current=2) 314 plateset.ce_switch = False 315 standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE) 316 bms_hardware.csv.cycle = old_cycle_function
Discharge at 2A (per Mil-prf/5) and capture voltage, coulomb, temp, (normal full test) as well as SMBus. Ping SMBus a total of 1800 times over the 5 hour test (i.e. every 10 seconds).
319def test_low_voltage_charge(): 320 """ 321 | Requirement | Charge from any starting voltage | 322 | :------------------- | :-------------------------------- | 323 | GitHub Issue | #161 (HITL), #250 (BMS) | 324 | Instructions | 1. Power down the cells to 0V to shut down the BMS </br>\ 325 2. Power up the cells to our starting voltage </br>\ 326 3. Start a 2A charge | 327 | Pass / Fail Criteria | Fail if we are unable to charge | 328 | Configuration | ⦁ `CELL_VOLTAGE` = Starting voltage </br>\ 329 ⦁ `MAX_TIME` = Charge time </br>\ 330 ⦁ `MINIMUM_READINGS` = Minimum low current readings to take before ending early | 331 """ 332 old_cycle_function = bms_hardware.csv.cycle 333 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 334 bms_hardware.csv.cycle.create_file() 335 336 logger.write_info_to_report("Powering down cell sims") 337 for cell in bms_hardware.cells.values(): 338 cell.disengage_safety_protocols = True 339 cell.volts = 0.0001 340 time.sleep(5) 341 logger.write_info_to_report("Powering up cell sims") 342 for cell in bms_hardware.cells.values(): 343 cell.volts = CELL_VOLTAGE 344 cell.disengage_safety_protocols = False 345 time.sleep(5) 346 347 plateset.ce_switch = True # Allows current charging in excess of 400 mA 348 try: 349 standard_charge(max_time=MAX_TIME, sample_interval=1, charge_current=2, minimum_readings=MINIMUM_READINGS) 350 except TimeoutExceededError: 351 bms_hardware.charger.disable() 352 plateset.ce_switch = False 353 354 bms_hardware.csv.cycle = old_cycle_function
| Requirement | Charge from any starting voltage |
|---|---|
| GitHub Issue | #161 (HITL), #250 (BMS) |
| Instructions | 1. Power down the cells to 0V to shut down the BMS 2. Power up the cells to our starting voltage 3. Start a 2A charge |
| Pass / Fail Criteria | Fail if we are unable to charge |
| Configuration | ⦁ CELL_VOLTAGE = Starting voltage ⦁ MAX_TIME = Charge time ⦁ MINIMUM_READINGS = Minimum low current readings to take before ending early |
357def test_0v_works(): 358 """ 359 | Requirement | Test charging/discharging with 0V | 360 | :------------------- | :-------------------------------- | 361 | GitHub Issue | #161 (HITL), #250 (BMS) | 362 | Instructions | 1. Power up BMS </br>\ 363 ⠀⠀⦁ Power cells to a terminal voltage in the range 5.25V-5.75V </br>\ 364 ⠀⠀⦁ Power up to target voltage without causing an imbalance </br>\ 365 3. Discharge for 400 seconds at 2A (down to 0V) </br>\ 366 4. Reset BMS (simulating long discharge at rest) </br>\ 367 ⠀⠀⦁ Power down cell sims to 0V </br>\ 368 ⠀⠀⦁ Power up cell sims to 0.1V </br>\ 369 5. Charge for 200 seconds at 2A </br>\ 370 6. Discharge for 260 seconds at 2A (down to 0V) | 371 | Pass / Fail Criteria | Fail if we are unable to charge or discharge | 372 """ 373 old_cycle_function = bms_hardware.csv.cycle 374 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 375 bms_hardware.csv.cycle.create_file() 376 for cell in bms_hardware.cells.values(): # Total voltage = 5.5V 377 cell.exact_volts = 1.375 378 bms_hardware.resting_time = 4 379 bms_hardware.resting_sample_interval = 1 380 bms_hardware.run_resting_cycle() 381 382 for cell in bms_hardware.cells.values(): # Total voltage = 8V 383 cell.exact_volts = 2.0 384 bms_hardware.resting_time = 4 385 bms_hardware.resting_sample_interval = 1 386 bms_hardware.run_resting_cycle() 387 388 for cell in bms_hardware.cells.values(): # Total voltage = 12V (we set it to 2V first to avoid imbalance) 389 cell.exact_volts = 3.0 390 391 try: 392 standard_discharge(max_time=400, discharge_voltage=-0.1, sample_interval=1) 393 except TimeoutExceededError: 394 bms_hardware.load.disable() 395 396 logger.write_info_to_report("Powering down cell sims") 397 for cell in bms_hardware.cells.values(): 398 cell.disengage_safety_protocols = True 399 cell.volts = 0.0001 400 time.sleep(5) 401 logger.write_info_to_report("Powering up cell sims") 402 for cell in bms_hardware.cells.values(): 403 cell.volts = 0.1 404 cell.disengage_safety_protocols = False 405 406 plateset.ce_switch = True # Allows current charging in excess of 400 mA 407 try: 408 standard_charge(max_time=200, sample_interval=1, charge_current=2) 409 except TimeoutExceededError: 410 bms_hardware.charger.disable() 411 plateset.ce_switch = False 412 413 try: 414 standard_discharge(max_time=260, discharge_voltage=-0.1, sample_interval=1) 415 except TimeoutExceededError: 416 bms_hardware.load.disable() 417 418 bms_hardware.csv.cycle = old_cycle_function
| Requirement | Test charging/discharging with 0V |
|---|---|
| GitHub Issue | #161 (HITL), #250 (BMS) |
| Instructions | 1. Power up BMS ⠀⠀⦁ Power cells to a terminal voltage in the range 5.25V-5.75V ⠀⠀⦁ Power up to target voltage without causing an imbalance 3. Discharge for 400 seconds at 2A (down to 0V) 4. Reset BMS (simulating long discharge at rest) ⠀⠀⦁ Power down cell sims to 0V ⠀⠀⦁ Power up cell sims to 0.1V 5. Charge for 200 seconds at 2A 6. Discharge for 260 seconds at 2A (down to 0V) |
| Pass / Fail Criteria | Fail if we are unable to charge or discharge |
421def test_short_discharge(): 422 """30 min discharge.""" 423 for cell in bms_hardware.cells.values(): 424 cell.state_of_charge = 0.10 # 10% FIXME(JA): figure out the correct soc for 20-30 min 425 old_cycle_function = bms_hardware.csv.cycle 426 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 427 bms_hardware.csv.cycle.create_file() 428 standard_discharge(sample_interval=10, discharge_voltage=DISCHARGE_VOLTAGE) 429 bms_hardware.csv.cycle = old_cycle_function
30 min discharge.
432def test_timed_discharge(): 433 """Timed discharge.""" 434 for cell in bms_hardware.cells.values(): 435 cell.state_of_charge = 0.80 436 old_cycle_function = bms_hardware.csv.cycle 437 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 438 bms_hardware.csv.cycle.create_file() 439 try: 440 standard_discharge(sample_interval=10, max_time=60 * 60) 441 except TimeoutExceededError: 442 bms_hardware.load.disable() 443 bms_hardware.csv.cycle = old_cycle_function
Timed discharge.
446def test_columb_count(): 447 """Short discharge, followed by a short rest below -113 mA.""" 448 for cell in bms_hardware.cells.values(): 449 cell.state_of_charge = 0.80 # 80% 450 old_cycle_function = bms_hardware.csv.cycle 451 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 452 bms_hardware.csv.cycle.create_file() 453 try: 454 standard_discharge(sample_interval=10, max_time=30 * 60) 455 except TimeoutExceededError: 456 pass 457 try: 458 standard_discharge(sample_interval=10, max_time=30 * 60, discharge_current=0.100) 459 except TimeoutExceededError: 460 bms_hardware.load.disable() 461 test_resting(max_time=2 * 60) 462 bms_hardware.csv.cycle = old_cycle_function
Short discharge, followed by a short rest below -113 mA.
465def test_live_cell_undervoltage(): 466 """Discharge down to 2.5 volts and cause an undervoltage discharge fault, clear by CE and charging.""" 467 logger.write_info_to_report("Testing Live Cell Undervoltage") 468 serial_watcher.start() 469 cell = None 470 for cell in bms_hardware.cells.values(): 471 cell.volts = 3.5 472 if cell: 473 cell.disengage_safety_protocols = True 474 cell.volts = 2.5 475 old_cycle_function = bms_hardware.csv.cycle 476 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 477 bms_hardware.csv.cycle.create_file() 478 479 # Discharge until undervoltage 480 try: 481 standard_discharge(sample_interval=10, discharge_voltage=9) 482 except UnderVoltageError: 483 bms_hardware.load.disable() 484 serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", True) 485 486 # Attempt to charge back up 487 plateset.ce_switch = True # Allows current charging in excess of 400 mA 488 serial_watcher.assert_true("flags.faultslumber_undervoltage_discharge", False) 489 try: 490 standard_charge(max_time=2 * 60, sample_interval=10, charge_current=2) 491 except TimeoutExceededError: 492 bms_hardware.charger.disable() 493 plateset.ce_switch = False 494 for cell in bms_hardware.cells.values(): 495 assert cell.volts >= 2.6 496 cell.disengage_safety_protocols = False 497 498 bms_hardware.csv.cycle = old_cycle_function 499 serial_watcher.stop()
Discharge down to 2.5 volts and cause an undervoltage discharge fault, clear by CE and charging.
502def test_5v_charge(): 503 """Check if we can charge the cells back up from as low as 5V.""" 504 logger.write_info_to_report("Testing 5V Charging") 505 serial_watcher.start() 506 for cell in bms_hardware.cells.values(): 507 cell.disengage_safety_protocols = True 508 cell.volts = 1.4 509 old_cycle_function = bms_hardware.csv.cycle 510 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 511 bms_hardware.csv.cycle.create_file() 512 513 plateset.ce_switch = True # Allows current charging in excess of 400 mA 514 try: 515 standard_charge(max_time=5 * 60, sample_interval=10, charge_current=2) 516 except TimeoutExceededError: 517 bms_hardware.charger.disable() 518 plateset.ce_switch = False 519 520 for cell in bms_hardware.cells.values(): 521 assert cell.volts >= 2.6 # FIXME(JA): set to expected voltage after 5 minutes 522 cell.disengage_safety_protocols = False 523 524 bms_hardware.csv.cycle = old_cycle_function 525 serial_watcher.stop()
Check if we can charge the cells back up from as low as 5V.
528def test_resting(max_time: int = 30, sample_interval=10): 529 """Record SMBus data while at rest.""" 530 old_cycle_function = bms_hardware.csv.cycle 531 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 532 bms_hardware.csv.cycle.create_file() 533 start_time = time.perf_counter() 534 while time.perf_counter() - start_time <= max_time: 535 bms_hardware.csv.cycle.record(time.perf_counter() - start_time, 0.0) 536 time.sleep(sample_interval) 537 bms_hardware.csv.cycle = old_cycle_function
Record SMBus data while at rest.
540def test_standard_rest(): 541 """Stabilize the batteries for some time.""" 542 old_cycle_function = bms_hardware.csv.cycle 543 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 544 standard_rest() 545 bms_hardware.csv.cycle = old_cycle_function
Stabilize the batteries for some time.
548def test_soc_rest(): 549 """ 550 We can change cell voltages while the state machine is relaxed, does smbus and serial report the proper state of 551 charge? 552 """ 553 logger.write_info_to_report("Testing SOC Rest") 554 serial_watcher.start() 555 old_cycle_function = bms_hardware.csv.cycle 556 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 557 bms_hardware.csv.cycle.create_file() 558 for soc in range(10, 100, 10): # 10% to 90% 559 for cell in bms_hardware.cells.values(): 560 cell.state_of_charge = soc / 100 561 test_resting(max_time=30, sample_interval=10) 562 serial_watcher.assert_true("percent_charged", soc, error=(5, 5)) 563 564 bms_hardware.csv.cycle = old_cycle_function 565 serial_watcher.stop()
We can change cell voltages while the state machine is relaxed, does smbus and serial report the proper state of charge?
568def test_soc_discharge(): 569 """ 570 Discharging: exceed threshold of 113mamps, we discharge at 1 amp for ~30 minutes, have bms report back estimated 571 state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the 572 time between excited and fully relaxed. 573 """ 574 logger.write_info_to_report("Testing SOC Discharging") 575 serial_watcher.start() 576 old_cycle_function = bms_hardware.csv.cycle 577 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 578 bms_hardware.csv.cycle.create_file() 579 580 for cell in bms_hardware.cells.values(): 581 cell.state_of_charge = 0.80 582 try: 583 standard_discharge(max_time=30 * 60, sample_interval=10, discharge_current=1) 584 except TimeoutExceededError: 585 bms_hardware.load.disable() 586 test_resting(max_time=30 * 60) 587 588 bms_hardware.csv.cycle = old_cycle_function 589 serial_watcher.stop()
Discharging: exceed threshold of 113mamps, we discharge at 1 amp for ~30 minutes, have bms report back estimated state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the time between excited and fully relaxed.
592def test_soc_charge(): 593 """ 594 Charging: exceed threshold of 143mamps, we charge at 1 amp for ~30 minutes, have bms report back estimated 595 state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the 596 time between excited and fully relaxed. 597 """ 598 logger.write_info_to_report("Testing SOC Charging") 599 serial_watcher.start() 600 old_cycle_function = bms_hardware.csv.cycle 601 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 602 bms_hardware.csv.cycle.create_file() 603 604 for cell in bms_hardware.cells.values(): 605 cell.state_of_charge = 0.20 606 plateset.ce_switch = True 607 try: 608 standard_charge(max_time=30 * 60, sample_interval=10, charge_current=1) 609 except TimeoutExceededError: 610 bms_hardware.charger.disable() 611 plateset.ce_switch = False 612 test_resting(max_time=30 * 60) 613 614 bms_hardware.csv.cycle = old_cycle_function 615 serial_watcher.stop()
Charging: exceed threshold of 143mamps, we charge at 1 amp for ~30 minutes, have bms report back estimated state of charge, then turn the current off, until it gets back to relaxed. See how the SOC fluctuates during the time between excited and fully relaxed.
618def test_wakeup_counter(): 619 """ 620 Charge at around 10mA to repeatedly trigger the wakeup counter. 621 This occurs as we are very close to the trigger point. 622 """ 623 logger.write_info_to_report("Testing Wakeup Counter") 624 old_cycle_function = bms_hardware.csv.cycle 625 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 626 bms_hardware.csv.cycle.create_file() 627 serial_watcher.start(csv=True) 628 629 for cell in bms_hardware.cells.values(): 630 cell.state_of_charge = 0.20 631 plateset.ce_switch = True 632 try: 633 standard_charge(max_time=5 * 60, sample_interval=1, charge_current=WAKEUP_CURRENT, termination_current=0.000001) 634 except TimeoutExceededError: 635 bms_hardware.charger.disable() 636 plateset.ce_switch = False 637 638 assert serial_watcher.events["Wakeup_Counter"][0].value == serial_watcher.events["Wakeup_Counter"][-1].value 639 640 serial_watcher.stop() 641 bms_hardware.csv.cycle = old_cycle_function
Charge at around 10mA to repeatedly trigger the wakeup counter. This occurs as we are very close to the trigger point.
644def test_charge_increment(): 645 """Increment current.""" 646 logger.write_info_to_report("Charging Incrementally") 647 old_cycle_function = bms_hardware.csv.cycle 648 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 649 650 max_time = 3 * 60 * 60 651 elapsed_time = 0 652 charge_sample_interval = 1 653 termination_current = 0.0 654 current_limit = CHARGE_INC_STOP 655 charge_current = CHARGE_INC_START 656 latest_i = termination_current + 0.1 657 ov_protection = 17 658 latest_v = 0 659 max_charge_current = 0 660 661 current_time = StopWatch() 662 current_time.start() 663 664 # Enable charging and timer 665 plateset.ce_switch = True # Does not allow current charging in excess of 400 mA or less 666 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 667 bms_hardware.charger.enable() # Enables the output of the charger 668 bms_hardware.timer.reset() # Keep track of runtime 669 670 # Charge until termination current, current limit, ov_protection, or max_time 671 while current_limit >= latest_i >= termination_current: 672 if latest_v >= ov_protection: 673 raise OverVoltageError( 674 f"Overvoltage protection triggered at {time.strftime('%x %X')}. " 675 f"Voltage {latest_v} is higher than {ov_protection}." 676 ) 677 if elapsed_time >= max_time: 678 raise TimeoutExceededError(f"Current of {current_limit}A was not reached after {max_time} seconds") 679 680 elapsed_time = bms_hardware.timer.elapsed_time 681 latest_v = bms_hardware.dmm.volts 682 latest_i = bms_hardware.charger.amps 683 max_charge_current = max(max_charge_current, latest_i) 684 logger.write_info_to_report(f"Elapsed Time: {elapsed_time}, Voltage: {latest_v}, Current: {latest_i}") 685 686 bms_hardware.csv.cycle.record(elapsed_time) 687 if current_time.elapsed_time > CHARGE_INC_TIME: 688 current_time.reset() 689 charge_current += CHARGE_INC_STEP 690 bms_hardware.charger.set_profile(volts=16.8, amps=charge_current) 691 time.sleep(charge_sample_interval) 692 bms_hardware.charger.disable() # Charging is complete, turn off the charger 693 plateset.ce_switch = False 694 bms_hardware.csv.cycle = old_cycle_function
Increment current.
697def test_cell_sim_voltage(): 698 """Set the cell sims to some voltage.""" 699 # old_cycle_function = bms_hardware.csv.cycle 700 # bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 701 # bms_hardware.csv.cycle.create_file() 702 703 logger.write_info_to_report("Powering down cell sims") 704 for cell in bms_hardware.cells.values(): 705 cell.disengage_safety_protocols = True 706 cell.volts = 0.0001 707 time.sleep(5) 708 logger.write_info_to_report("Powering up cell sims") 709 for cell in bms_hardware.cells.values(): 710 cell.volts = CELL_VOLTAGE 711 cell.disengage_safety_protocols = False 712 # for cell in bms_hardware.cells.values(): 713 # cell.exact_volts = CELL_VOLTAGE 714 logger.write_info_to_report("Setting temperature to 15°C") 715 plateset.thermistor1 = DEFAULT_TEMPERATURE_C 716 plateset.thermistor2 = DEFAULT_TEMPERATURE_C 717 718 time.sleep(5) 719 720 standard_discharge(sample_interval=10, discharge_current=DISCHARGE_CURRENT / 1000, discharge_voltage=-0.05) 721 722 bms_hardware.resting_time = REST_TIME 723 bms_hardware.max_time = REST_TIME 724 bms_hardware.resting_sample_interval = REST_INTERVAL 725 bms_hardware.sample_interval = REST_INTERVAL 726 bms_hardware.run_resting_cycle() 727 728 # bms_hardware.csv.cycle = old_cycle_function
Set the cell sims to some voltage.
731def test_cycle_count(): 732 """Repeat capacity test to confirm cycle count functionality.""" 733 test_reset_cell_sims() 734 735 old_cycle_function = bms_hardware.csv.cycle 736 bms_hardware.csv.cycle = bms_hardware.csv.cycle_smbus 737 bms_hardware.csv.cycle.create_file() 738 # serial_watcher.start(csv=True) 739 740 # Change cell sim capacity 741 for cell in bms_hardware.cells.values(): 742 cell.data.capacity = CYCLE_CAPACITY 743 744 # Run cycles 745 for i in range(CYCLE_COUNT): 746 logger.write_info_to_report(f"Running cycle {i}") 747 plateset.ce_switch = True # Allows current charging in excess of 400 mA 748 standard_charge(termination_current=1.0) 749 plateset.ce_switch = False 750 standard_discharge(discharge_voltage=2.9 * 4) 751 752 # serial_watcher.stop() 753 bms_hardware.csv.cycle = old_cycle_function 754 755 # FIXME(JA): merge NGI branch to allow threads to access the same hardware 756 # assert serial_watcher.events["charge_cycles"][-1].value == CYCLE_COUNT
Repeat capacity test to confirm cycle count functionality.