hitl_tester.test_cases.bms.flash_saving
| Test | MIL-PRF Compatability |
|---|---|
| GitHub Issue(s) | turnaroundfactor/HITL#327 turnaroundfactor/HITL#342 |
| Description | Tests for MIL-PRF-32383/5A (16-December-2021) compatibility. |
Used in these test plans:
- prod_bms ⠀⠀⠀(bms/prod_bms.plan)
- flash_save ⠀⠀⠀(bms/flash_save.plan)
Example Command (warning: test plan may run other test cases):
./hitl_tester.py prod_bms -DFAST_MODE=False -DFAST_CAPACITY_AH=0.3 -DFLASH_SLEEP=7
1""" 2| Test | MIL-PRF Compatability | 3| :------------------- | :----------------------------------------------------------- | 4| GitHub Issue(s) | turnaroundfactor/HITL#327 </br>\ 5 turnaroundfactor/HITL#342 | 6| Description | Tests for MIL-PRF-32383/5A (16-December-2021) compatibility. | 7""" 8 9from __future__ import annotations 10 11import ctypes 12import time 13from contextlib import suppress 14 15import pytest 16 17from hitl_tester.modules.bms.adc_plate import ADCPlate 18from hitl_tester.modules.bms.bms_hw import BMSHardware 19from hitl_tester.modules.bms.bms_serial import serial_monitor 20from hitl_tester.modules.bms.plateset import Plateset 21from hitl_tester.modules.bms.smbus import SMBus 22from hitl_tester.modules.bms.smbus_types import SMBusReg, BMSCommands 23from hitl_tester.modules.bms_types import DischargeType, TimeoutExceededError 24from hitl_tester.modules.logger import logger 25 26FAST_MODE = False # In this mode test times are shortened 27"""Whether to use smaller capacity or not (speeds up testing).""" 28 29FAST_CAPACITY_AH = 0.300 30"""The capacity to use in fast mode.""" 31 32FLASH_SLEEP = 7 33"""Time to wait for flash to write.""" 34 35_bms = BMSHardware(pytest.flags) # type: ignore[arg-type] 36_bms.init() 37_plateset = Plateset() 38_adc_plate = ADCPlate() 39_smbus = SMBus() 40 41 42@pytest.fixture(scope="function", autouse=True) 43def reset_test_environment(): 44 """ 45 Before each test, set cell sim capacity. 46 After each test, clean up modified objects. 47 """ 48 49 # Set cell sim capacity 50 if len(_bms.cells) > 0: 51 if FAST_MODE: 52 logger.write_info_to_report(f"Setting cell capacity to {FAST_CAPACITY_AH} Ah") 53 for cell in _bms.cells.values(): 54 cell.data.capacity = FAST_CAPACITY_AH 55 56 # Save logging function 57 old_cycle_function = _bms.csv.cycle 58 59 yield # Run test 60 61 _bms.csv.cycle = old_cycle_function # Restore logging 62 63 64def power_cycle_bms(temperature: float = 23, cell_soc: float = 0.50): 65 """Reset cell sims.""" 66 67 if len(_bms.cells) > 0: 68 logger.write_info_to_report(f"Setting temperature to {temperature}°C") 69 _plateset.thermistor1 = _plateset.thermistor2 = temperature 70 logger.write_info_to_report("Powering down cell sims") 71 time.sleep(5) 72 for cell in _bms.cells.values(): 73 cell.disengage_safety_protocols = True 74 cell.volts = 0.0001 75 time.sleep(5) 76 for cell in _bms.cells.values(): 77 logger.write_info_to_report(f"Powering up cell sim {cell.id} to {cell_soc:%}") 78 cell.state_of_charge = cell_soc 79 cell.disengage_safety_protocols = False 80 81 logger.write_info_to_report("Sleeping for 10 seconds before starting...") 82 time.sleep(10) 83 84 85def standard_charge( 86 charge_current: float = 2, 87 max_time: int = 8 * 3600, 88 sample_interval: int = 10, 89 minimum_readings: int = 3, 90 termination_current: float = 0.100, 91): 92 """ 93 Helper function to charge batteries in accordance with 4.3.1 for not greater than three hours. 94 4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge. 95 """ 96 _bms.voltage = 16.8 97 _bms.ov_protection = _bms.voltage + 0.050 # 50mV above the charging voltage 98 _bms.current = charge_current 99 _bms.termination_current = termination_current # 100 mA 100 _bms.max_time = max_time 101 _bms.sample_interval = sample_interval 102 _bms.minimum_readings = minimum_readings 103 104 # Run the Charge cycle 105 _plateset.ce_switch = True 106 _bms.run_li_charge_cycle() 107 _plateset.ce_switch = False 108 109 110def standard_rest(seconds: float = 2 * 3600, sample_interval: int = 10): 111 """Helper function to stabilize the batteries for 2+ hours.""" 112 _bms.max_time = seconds 113 _bms.sample_interval = sample_interval 114 _bms.run_resting_cycle() 115 116 117def standard_discharge( 118 discharge_current: float = 2, max_time: int = 8 * 3600, sample_interval: int = 10, discharge_voltage: float = 10 119): 120 """Helper function to discharge at 2A until 10V.""" 121 _bms.voltage = discharge_voltage 122 _bms.uv_protection = _bms.voltage - 0.500 # 500mV below voltage cutoff 123 _bms.current = discharge_current 124 _bms.discharge_type = DischargeType.CONSTANT_CURRENT 125 _bms.max_time = max_time 126 _bms.sample_interval = sample_interval 127 128 # Run the discharge cycle, returning the capacity 129 capacity = _bms.run_discharge_cycle() 130 logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh") 131 return capacity 132 133 134@pytest.mark.sim_cells 135class TestCycleCount: 136 """Confirm cycle count is retained.""" 137 138 def test_extended_cycle(self): 139 """ 140 | Description | Perform a long charge / discharge | 141 | :------------------- | :--------------------------------------------------------------------- | 142 | GitHub Issue | turnaroundfactor/HITL#413 | 143 | Instructions | 1. Charge/discharge the battery </br>\ 144 2. Charge for a little bit (flash only saves while charging </br>\ 145 3. Power cycle </br>\ 146 4. Confirm cycle count is still incremented by 1 | 147 | Pass / Fail Criteria | Charge cycle increments by 1 | 148 | Estimated Duration | 12 hours | 149 """ 150 151 original_charge_cycle = serial_monitor.read()["charge_cycles"] 152 standard_charge() 153 standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600) 154 standard_discharge() 155 156 logger.write_info_to_report("Charging for 30 seconds.") 157 with suppress(TimeoutExceededError): 158 standard_charge(max_time=30) 159 160 power_cycle_bms() 161 162 serial_monitor.read() # Clear serial queue to get new packet 163 cycle_count = serial_monitor.read()["charge_cycles"] 164 logger.write_result_to_html_report(f"Cycle count: {original_charge_cycle} -> {cycle_count}") 165 assert cycle_count == original_charge_cycle + 1, "Cycle count did not increment." 166 167 168@pytest.mark.sim_cells 169class TestCalibration: 170 """Calibrate, power cycle, confirm calibration is good.""" 171 172 average = 0 173 readings = 0 174 175 def bms_current(self): 176 """Measure serial current and calculate an average.""" 177 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 178 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 179 self.readings += 1 180 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample 181 182 def test_calibration(self): 183 """ 184 | Description | Test calibration retention | 185 | :------------------- | :--------------------------------------------------------------------- | 186 | GitHub Issue | turnaroundfactor/HITL#413 | 187 | Instructions | 1. Calibrate the BMS </br>\ 188 2. Power cycle </br>\ 189 3. Confirm BMS is still calibrated | 190 | Pass / Fail Criteria | Pass if calibrated | 191 | Estimated Duration | 1 minute | 192 """ 193 acceptable_error_ma = 5 194 195 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 196 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 197 self.readings = 0 198 199 standard_rest(30, 5) 200 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 201 offset = int(round(self.average, 0)) 202 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 203 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 204 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 205 time.sleep(FLASH_SLEEP) 206 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 207 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 208 209 power_cycle_bms() 210 211 # Confirm current is in acceptable range 212 self.readings = 0 # Reset average 213 standard_rest(30, 5) 214 logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA") 215 assert ( 216 acceptable_error_ma > self.average > -acceptable_error_ma 217 ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA" 218 219 220@pytest.mark.sim_cells 221class TestPermanentFault: 222 """Cause overtemp permanent fault, power cycle, confirm faulted.""" 223 224 timeout_s = 60 225 226 def test_permanent_fault(self): 227 """ 228 | Description | Test permanent fault retention | 229 | :------------------- | :--------------------------------------------------------------------- | 230 | GitHub Issue | turnaroundfactor/HITL#413 | 231 | Instructions | 1. Cause a permanent fault </br>\ 232 2. Power cycle </br>\ 233 3. Confirm permanent fault is active | 234 | Pass / Fail Criteria | Pass if faulted | 235 | Estimated Duration | 1 minute | 236 """ 237 _plateset.disengage_safety_protocols = True 238 _plateset.thermistor1 = 94 239 _plateset.disengage_safety_protocols = False 240 241 start = time.perf_counter() 242 serial_data = serial_monitor.read() 243 while not ( 244 serial_data["flags.permanentdisable_overtemp"] and serial_data["flags.measure_output_fets_disabled"] 245 ): 246 serial_data = serial_monitor.read() 247 if time.perf_counter() - start > self.timeout_s: 248 logger.write_result_to_html_report(f"FETs: {serial_data['flags.measure_output_fets_disabled']}") 249 logger.write_result_to_html_report(f"Overtemp Fault: {serial_data['flags.permanentdisable_overtemp']}") 250 raise TimeoutError(f"Permanent temp fault or FETs were not set after {self.timeout_s} seconds.") 251 252 power_cycle_bms() 253 254 serial_monitor.read() # Clear serial queue to get new packet 255 serial_data = serial_monitor.read() 256 fets_disable = serial_data["flags.measure_output_fets_disabled"] 257 permanent_disable = serial_data["flags.permanentdisable_overtemp"] 258 logger.write_result_to_html_report(f"FETs: {'Disabled' if fets_disable else 'Enabled'}") 259 logger.write_result_to_html_report(f"Overtemp Disable: {'Active' if permanent_disable else 'Inactive'}") 260 assert fets_disable, "FETs were not disabled." 261 assert permanent_disable, "Permanent Temp Disable was inactive." 262 263 264@pytest.mark.sim_cells 265class TestFlashErase: 266 """Test erasing flash.""" 267 268 average = 0 269 readings = 0 270 271 def bms_current(self): 272 """Measure serial current and calculate an average.""" 273 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 274 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 275 self.readings += 1 276 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample 277 278 def test_flash_erase(self): 279 """ 280 | Description | Test erasing flash | 281 | :------------------- | :--------------------------------------------------------------------- | 282 | GitHub Issue | turnaroundfactor/HITL#435 | 283 | Instructions | 1. Calibrate the BMS </br>\ 284 2. Power cycle </br>\ 285 3. Confirm BMS is still calibrated </br>\ 286 4. Send "Flash Erase" command with SMBus </br>\ 287 5. Confirm BMS is no longer calibrated | 288 | Pass / Fail Criteria | Pass if not calibrated | 289 | Estimated Duration | 1 minute | 290 """ 291 acceptable_error_ma = 5 292 293 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 294 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 295 self.readings = 0 296 standard_rest(30, 5) 297 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 298 299 # Calibrate 300 offset = int(round(self.average, 0)) 301 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 302 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 303 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 304 time.sleep(FLASH_SLEEP) 305 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 306 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 307 308 power_cycle_bms() 309 310 # Confirm current is in acceptable range 311 self.readings = 0 # Reset average 312 standard_rest(30, 5) 313 logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA") 314 assert ( 315 acceptable_error_ma > self.average > -acceptable_error_ma 316 ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA" 317 318 # Erase flash 319 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH) 320 time.sleep(FLASH_SLEEP) # Wait for erase to complete 321 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 322 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 323 324 # Confirm current is not in acceptable range 325 self.readings = 0 # Reset average 326 standard_rest(30, 5) 327 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 328 assert ( 329 not acceptable_error_ma > self.average > -acceptable_error_ma 330 ), f"Appears to be calibrated. {self.average:.3f} mA is within the limit of ±{acceptable_error_ma:.3f} mA" 331 332 333@pytest.mark.usefixtures("cycle_smbus") 334@pytest.mark.sim_cells 335class TestOTP: 336 """Test OTP registers.""" 337 338 average = 0 339 readings = 0 340 serial_id = 0xCAFE 341 342 def bms_current(self): 343 """Measure serial current and calculate an average.""" 344 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 345 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 346 self.readings += 1 347 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample 348 349 def calibrate_current(self): 350 """Calibrate BMS current in flash.""" 351 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 352 self.readings = 0 353 standard_rest(30, 5) 354 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 355 356 # Calibrate 357 offset = int(round(self.average, 0)) 358 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 359 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 360 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 361 time.sleep(FLASH_SLEEP) 362 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 363 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 364 365 _bms.csv.cycle.postfix_fn = lambda: ... 366 367 def is_calibrated(self) -> bool: 368 """Confirm current is in acceptable range""" 369 acceptable_error_ma = 5 370 371 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 372 self.readings = 0 # Reset average 373 standard_rest(30, 5) 374 logger.write_result_to_html_report(f"Average rest current (calibrated?): {self.average:.3f} mA") 375 return acceptable_error_ma > self.average > -acceptable_error_ma 376 377 def set_serial_id(self): 378 """Set the 16-bit serial ID.""" 379 _smbus.write_register(SMBusReg.SERIAL_NUM, self.serial_id) 380 time.sleep(FLASH_SLEEP) 381 382 def is_serial_set(self) -> bool: 383 """Check if the 16-bit serial ID has been set.""" 384 return _smbus.read_register(SMBusReg.SERIAL_NUM)[1] == self.serial_id.to_bytes(2, byteorder="little") 385 386 def enable_faults(self): 387 """Enable faults via SMBus.""" 388 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.FAULT_ENABLE) 389 time.sleep(FLASH_SLEEP) 390 391 def is_faults_enabled(self) -> bool: 392 """Check if overtemp faults can be raised.""" 393 timeout_s = 10 394 395 # Raise a fault 396 _plateset.thermistor1 = 65 397 start = time.perf_counter() 398 while (serial_data := serial_monitor.read(latest=True)) and not serial_data["flags.fault_overtemp_discharge"]: 399 if time.perf_counter() - start > timeout_s: 400 logger.write_debug_to_report(f"Over-temperature fault was not raised after {timeout_s} seconds.") 401 return False 402 403 # Clear the fault 404 _plateset.thermistor1 = 45 405 start = time.perf_counter() 406 while (serial_data := serial_monitor.read(latest=True)) and serial_data["flags.fault_overtemp_discharge"]: 407 if time.perf_counter() - start > timeout_s: 408 logger.write_debug_to_report(f"Over-temperature fault was not cleared after {timeout_s} seconds.") 409 return False 410 411 return True 412 413 def erase_flash(self): 414 """Erase the internal BMS flash.""" 415 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH) 416 time.sleep(FLASH_SLEEP) # Wait for erase to complete 417 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 418 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 419 420 def test_flash_otp(self): 421 """ 422 | Description | Test OTP registers | 423 | :------------------- | :--------------------------------------------------------------------- | 424 | GitHub Issue | turnaroundfactor/HITL#486 | 425 | Instructions | 1. Set up flash values </br>\ 426 ⠀⠀⦁ Enable assembly mode </br>\ 427 ⠀⠀⦁ Calibrate the BMS and verify </br>\ 428 ⠀⠀⦁ Set the serial number and verify </br>\ 429 ⠀⠀⦁ Enable the faults and verify </br>\ 430 2. Send "Flash Erase" command with SMBus </br>\ 431 3. Confirm BMS flash is erased </br>\ 432 ⠀⠀⦁ BMS isn't calibrated </br>\ 433 ⠀⠀⦁ Faults aren't enabled </br>\ 434 4. Set up flash values </br>\ 435 ⠀⠀⦁ Calibrate the BMS and verify </br>\ 436 ⠀⠀⦁ Enable the faults and verify </br>\ 437 5. Enter manufacturing mode </br>\ 438 6. Send "Flash Erase" command with SMBus </br>\ 439 7. Confirm BMS flash is not erased </br>\ 440 ⠀⠀⦁ BMS is calibrated </br>\ 441 ⠀⠀⦁ Faults are enabled | 442 | Pass / Fail Criteria | Pass flash cannot be erased | 443 | Estimated Duration | 1 minute | 444 """ 445 446 # 1. Set up flash values and enter assembly mode 447 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ASSEMBLY_MODE) 448 time.sleep(FLASH_SLEEP) 449 self.calibrate_current() 450 self.set_serial_id() 451 self.enable_faults() 452 power_cycle_bms() # Power cycle 453 assert self.is_calibrated(), "Current was not calibrated after power cycle." 454 assert self.is_serial_set(), "Serial was not set after power cycle." 455 assert self.is_faults_enabled(), "Faults were not enabled after power cycle." 456 457 # 2/3. Erase flash, confirm BMS flash is erased 458 self.erase_flash() 459 assert not self.is_calibrated(), "Current was still calibrated after erase." 460 assert self.is_serial_set(), "Serial was not set after erase." 461 assert not self.is_faults_enabled(), "Faults were still enabled after erase." 462 463 # 4. Set up flash values 464 self.calibrate_current() 465 self.enable_faults() 466 power_cycle_bms() # Power cycle 467 assert self.is_calibrated(), "Current was not calibrated after power cycle." 468 assert self.is_serial_set(), "Serial was not set after power cycle." 469 assert self.is_faults_enabled(), "Faults were not enabled after power cycle." 470 471 # 5. Enter manufacturing mode 472 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.PRODUCTION_MODE) 473 time.sleep(FLASH_SLEEP) 474 475 # 6. Erase flash, confirm BMS flash is not erased 476 self.erase_flash() 477 assert self.is_calibrated(), "Current was not calibrated after erase in manufacturing mode." 478 assert self.is_serial_set(), "Serial was not set after erase in manufacturing mode." 479 assert self.is_faults_enabled(), "Faults were not enabled after erase in manufacturing mode."
Whether to use smaller capacity or not (speeds up testing).
The capacity to use in fast mode.
Time to wait for flash to write.
43@pytest.fixture(scope="function", autouse=True) 44def reset_test_environment(): 45 """ 46 Before each test, set cell sim capacity. 47 After each test, clean up modified objects. 48 """ 49 50 # Set cell sim capacity 51 if len(_bms.cells) > 0: 52 if FAST_MODE: 53 logger.write_info_to_report(f"Setting cell capacity to {FAST_CAPACITY_AH} Ah") 54 for cell in _bms.cells.values(): 55 cell.data.capacity = FAST_CAPACITY_AH 56 57 # Save logging function 58 old_cycle_function = _bms.csv.cycle 59 60 yield # Run test 61 62 _bms.csv.cycle = old_cycle_function # Restore logging
Before each test, set cell sim capacity. After each test, clean up modified objects.
65def power_cycle_bms(temperature: float = 23, cell_soc: float = 0.50): 66 """Reset cell sims.""" 67 68 if len(_bms.cells) > 0: 69 logger.write_info_to_report(f"Setting temperature to {temperature}°C") 70 _plateset.thermistor1 = _plateset.thermistor2 = temperature 71 logger.write_info_to_report("Powering down cell sims") 72 time.sleep(5) 73 for cell in _bms.cells.values(): 74 cell.disengage_safety_protocols = True 75 cell.volts = 0.0001 76 time.sleep(5) 77 for cell in _bms.cells.values(): 78 logger.write_info_to_report(f"Powering up cell sim {cell.id} to {cell_soc:%}") 79 cell.state_of_charge = cell_soc 80 cell.disengage_safety_protocols = False 81 82 logger.write_info_to_report("Sleeping for 10 seconds before starting...") 83 time.sleep(10)
Reset cell sims.
86def standard_charge( 87 charge_current: float = 2, 88 max_time: int = 8 * 3600, 89 sample_interval: int = 10, 90 minimum_readings: int = 3, 91 termination_current: float = 0.100, 92): 93 """ 94 Helper function to charge batteries in accordance with 4.3.1 for not greater than three hours. 95 4.3.1 = 23 ± 5°C (73.4°F) ambient pressure/relative humidity, with 2+ hours between charge and discharge. 96 """ 97 _bms.voltage = 16.8 98 _bms.ov_protection = _bms.voltage + 0.050 # 50mV above the charging voltage 99 _bms.current = charge_current 100 _bms.termination_current = termination_current # 100 mA 101 _bms.max_time = max_time 102 _bms.sample_interval = sample_interval 103 _bms.minimum_readings = minimum_readings 104 105 # Run the Charge cycle 106 _plateset.ce_switch = True 107 _bms.run_li_charge_cycle() 108 _plateset.ce_switch = False
Helper function to 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.
111def standard_rest(seconds: float = 2 * 3600, sample_interval: int = 10): 112 """Helper function to stabilize the batteries for 2+ hours.""" 113 _bms.max_time = seconds 114 _bms.sample_interval = sample_interval 115 _bms.run_resting_cycle()
Helper function to stabilize the batteries for 2+ hours.
118def standard_discharge( 119 discharge_current: float = 2, max_time: int = 8 * 3600, sample_interval: int = 10, discharge_voltage: float = 10 120): 121 """Helper function to discharge at 2A until 10V.""" 122 _bms.voltage = discharge_voltage 123 _bms.uv_protection = _bms.voltage - 0.500 # 500mV below voltage cutoff 124 _bms.current = discharge_current 125 _bms.discharge_type = DischargeType.CONSTANT_CURRENT 126 _bms.max_time = max_time 127 _bms.sample_interval = sample_interval 128 129 # Run the discharge cycle, returning the capacity 130 capacity = _bms.run_discharge_cycle() 131 logger.write_info_to_report(f"Discharge complete, capacity was {capacity * 1000.0} mAh") 132 return capacity
Helper function to discharge at 2A until 10V.
135@pytest.mark.sim_cells 136class TestCycleCount: 137 """Confirm cycle count is retained.""" 138 139 def test_extended_cycle(self): 140 """ 141 | Description | Perform a long charge / discharge | 142 | :------------------- | :--------------------------------------------------------------------- | 143 | GitHub Issue | turnaroundfactor/HITL#413 | 144 | Instructions | 1. Charge/discharge the battery </br>\ 145 2. Charge for a little bit (flash only saves while charging </br>\ 146 3. Power cycle </br>\ 147 4. Confirm cycle count is still incremented by 1 | 148 | Pass / Fail Criteria | Charge cycle increments by 1 | 149 | Estimated Duration | 12 hours | 150 """ 151 152 original_charge_cycle = serial_monitor.read()["charge_cycles"] 153 standard_charge() 154 standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600) 155 standard_discharge() 156 157 logger.write_info_to_report("Charging for 30 seconds.") 158 with suppress(TimeoutExceededError): 159 standard_charge(max_time=30) 160 161 power_cycle_bms() 162 163 serial_monitor.read() # Clear serial queue to get new packet 164 cycle_count = serial_monitor.read()["charge_cycles"] 165 logger.write_result_to_html_report(f"Cycle count: {original_charge_cycle} -> {cycle_count}") 166 assert cycle_count == original_charge_cycle + 1, "Cycle count did not increment."
Confirm cycle count is retained.
139 def test_extended_cycle(self): 140 """ 141 | Description | Perform a long charge / discharge | 142 | :------------------- | :--------------------------------------------------------------------- | 143 | GitHub Issue | turnaroundfactor/HITL#413 | 144 | Instructions | 1. Charge/discharge the battery </br>\ 145 2. Charge for a little bit (flash only saves while charging </br>\ 146 3. Power cycle </br>\ 147 4. Confirm cycle count is still incremented by 1 | 148 | Pass / Fail Criteria | Charge cycle increments by 1 | 149 | Estimated Duration | 12 hours | 150 """ 151 152 original_charge_cycle = serial_monitor.read()["charge_cycles"] 153 standard_charge() 154 standard_rest(seconds=30 if FAST_MODE else 3.1 * 3600) 155 standard_discharge() 156 157 logger.write_info_to_report("Charging for 30 seconds.") 158 with suppress(TimeoutExceededError): 159 standard_charge(max_time=30) 160 161 power_cycle_bms() 162 163 serial_monitor.read() # Clear serial queue to get new packet 164 cycle_count = serial_monitor.read()["charge_cycles"] 165 logger.write_result_to_html_report(f"Cycle count: {original_charge_cycle} -> {cycle_count}") 166 assert cycle_count == original_charge_cycle + 1, "Cycle count did not increment."
| Description | Perform a long charge / discharge |
|---|---|
| GitHub Issue | turnaroundfactor/HITL#413 |
| Instructions | 1. Charge/discharge the battery 2. Charge for a little bit (flash only saves while charging 3. Power cycle 4. Confirm cycle count is still incremented by 1 |
| Pass / Fail Criteria | Charge cycle increments by 1 |
| Estimated Duration | 12 hours |
169@pytest.mark.sim_cells 170class TestCalibration: 171 """Calibrate, power cycle, confirm calibration is good.""" 172 173 average = 0 174 readings = 0 175 176 def bms_current(self): 177 """Measure serial current and calculate an average.""" 178 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 179 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 180 self.readings += 1 181 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample 182 183 def test_calibration(self): 184 """ 185 | Description | Test calibration retention | 186 | :------------------- | :--------------------------------------------------------------------- | 187 | GitHub Issue | turnaroundfactor/HITL#413 | 188 | Instructions | 1. Calibrate the BMS </br>\ 189 2. Power cycle </br>\ 190 3. Confirm BMS is still calibrated | 191 | Pass / Fail Criteria | Pass if calibrated | 192 | Estimated Duration | 1 minute | 193 """ 194 acceptable_error_ma = 5 195 196 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 197 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 198 self.readings = 0 199 200 standard_rest(30, 5) 201 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 202 offset = int(round(self.average, 0)) 203 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 204 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 205 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 206 time.sleep(FLASH_SLEEP) 207 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 208 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 209 210 power_cycle_bms() 211 212 # Confirm current is in acceptable range 213 self.readings = 0 # Reset average 214 standard_rest(30, 5) 215 logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA") 216 assert ( 217 acceptable_error_ma > self.average > -acceptable_error_ma 218 ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA"
Calibrate, power cycle, confirm calibration is good.
176 def bms_current(self): 177 """Measure serial current and calculate an average.""" 178 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 179 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 180 self.readings += 1 181 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample
Measure serial current and calculate an average.
183 def test_calibration(self): 184 """ 185 | Description | Test calibration retention | 186 | :------------------- | :--------------------------------------------------------------------- | 187 | GitHub Issue | turnaroundfactor/HITL#413 | 188 | Instructions | 1. Calibrate the BMS </br>\ 189 2. Power cycle </br>\ 190 3. Confirm BMS is still calibrated | 191 | Pass / Fail Criteria | Pass if calibrated | 192 | Estimated Duration | 1 minute | 193 """ 194 acceptable_error_ma = 5 195 196 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 197 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 198 self.readings = 0 199 200 standard_rest(30, 5) 201 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 202 offset = int(round(self.average, 0)) 203 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 204 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 205 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 206 time.sleep(FLASH_SLEEP) 207 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 208 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 209 210 power_cycle_bms() 211 212 # Confirm current is in acceptable range 213 self.readings = 0 # Reset average 214 standard_rest(30, 5) 215 logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA") 216 assert ( 217 acceptable_error_ma > self.average > -acceptable_error_ma 218 ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA"
| Description | Test calibration retention |
|---|---|
| GitHub Issue | turnaroundfactor/HITL#413 |
| Instructions | 1. Calibrate the BMS 2. Power cycle 3. Confirm BMS is still calibrated |
| Pass / Fail Criteria | Pass if calibrated |
| Estimated Duration | 1 minute |
221@pytest.mark.sim_cells 222class TestPermanentFault: 223 """Cause overtemp permanent fault, power cycle, confirm faulted.""" 224 225 timeout_s = 60 226 227 def test_permanent_fault(self): 228 """ 229 | Description | Test permanent fault retention | 230 | :------------------- | :--------------------------------------------------------------------- | 231 | GitHub Issue | turnaroundfactor/HITL#413 | 232 | Instructions | 1. Cause a permanent fault </br>\ 233 2. Power cycle </br>\ 234 3. Confirm permanent fault is active | 235 | Pass / Fail Criteria | Pass if faulted | 236 | Estimated Duration | 1 minute | 237 """ 238 _plateset.disengage_safety_protocols = True 239 _plateset.thermistor1 = 94 240 _plateset.disengage_safety_protocols = False 241 242 start = time.perf_counter() 243 serial_data = serial_monitor.read() 244 while not ( 245 serial_data["flags.permanentdisable_overtemp"] and serial_data["flags.measure_output_fets_disabled"] 246 ): 247 serial_data = serial_monitor.read() 248 if time.perf_counter() - start > self.timeout_s: 249 logger.write_result_to_html_report(f"FETs: {serial_data['flags.measure_output_fets_disabled']}") 250 logger.write_result_to_html_report(f"Overtemp Fault: {serial_data['flags.permanentdisable_overtemp']}") 251 raise TimeoutError(f"Permanent temp fault or FETs were not set after {self.timeout_s} seconds.") 252 253 power_cycle_bms() 254 255 serial_monitor.read() # Clear serial queue to get new packet 256 serial_data = serial_monitor.read() 257 fets_disable = serial_data["flags.measure_output_fets_disabled"] 258 permanent_disable = serial_data["flags.permanentdisable_overtemp"] 259 logger.write_result_to_html_report(f"FETs: {'Disabled' if fets_disable else 'Enabled'}") 260 logger.write_result_to_html_report(f"Overtemp Disable: {'Active' if permanent_disable else 'Inactive'}") 261 assert fets_disable, "FETs were not disabled." 262 assert permanent_disable, "Permanent Temp Disable was inactive."
Cause overtemp permanent fault, power cycle, confirm faulted.
227 def test_permanent_fault(self): 228 """ 229 | Description | Test permanent fault retention | 230 | :------------------- | :--------------------------------------------------------------------- | 231 | GitHub Issue | turnaroundfactor/HITL#413 | 232 | Instructions | 1. Cause a permanent fault </br>\ 233 2. Power cycle </br>\ 234 3. Confirm permanent fault is active | 235 | Pass / Fail Criteria | Pass if faulted | 236 | Estimated Duration | 1 minute | 237 """ 238 _plateset.disengage_safety_protocols = True 239 _plateset.thermistor1 = 94 240 _plateset.disengage_safety_protocols = False 241 242 start = time.perf_counter() 243 serial_data = serial_monitor.read() 244 while not ( 245 serial_data["flags.permanentdisable_overtemp"] and serial_data["flags.measure_output_fets_disabled"] 246 ): 247 serial_data = serial_monitor.read() 248 if time.perf_counter() - start > self.timeout_s: 249 logger.write_result_to_html_report(f"FETs: {serial_data['flags.measure_output_fets_disabled']}") 250 logger.write_result_to_html_report(f"Overtemp Fault: {serial_data['flags.permanentdisable_overtemp']}") 251 raise TimeoutError(f"Permanent temp fault or FETs were not set after {self.timeout_s} seconds.") 252 253 power_cycle_bms() 254 255 serial_monitor.read() # Clear serial queue to get new packet 256 serial_data = serial_monitor.read() 257 fets_disable = serial_data["flags.measure_output_fets_disabled"] 258 permanent_disable = serial_data["flags.permanentdisable_overtemp"] 259 logger.write_result_to_html_report(f"FETs: {'Disabled' if fets_disable else 'Enabled'}") 260 logger.write_result_to_html_report(f"Overtemp Disable: {'Active' if permanent_disable else 'Inactive'}") 261 assert fets_disable, "FETs were not disabled." 262 assert permanent_disable, "Permanent Temp Disable was inactive."
| Description | Test permanent fault retention |
|---|---|
| GitHub Issue | turnaroundfactor/HITL#413 |
| Instructions | 1. Cause a permanent fault 2. Power cycle 3. Confirm permanent fault is active |
| Pass / Fail Criteria | Pass if faulted |
| Estimated Duration | 1 minute |
265@pytest.mark.sim_cells 266class TestFlashErase: 267 """Test erasing flash.""" 268 269 average = 0 270 readings = 0 271 272 def bms_current(self): 273 """Measure serial current and calculate an average.""" 274 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 275 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 276 self.readings += 1 277 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample 278 279 def test_flash_erase(self): 280 """ 281 | Description | Test erasing flash | 282 | :------------------- | :--------------------------------------------------------------------- | 283 | GitHub Issue | turnaroundfactor/HITL#435 | 284 | Instructions | 1. Calibrate the BMS </br>\ 285 2. Power cycle </br>\ 286 3. Confirm BMS is still calibrated </br>\ 287 4. Send "Flash Erase" command with SMBus </br>\ 288 5. Confirm BMS is no longer calibrated | 289 | Pass / Fail Criteria | Pass if not calibrated | 290 | Estimated Duration | 1 minute | 291 """ 292 acceptable_error_ma = 5 293 294 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 295 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 296 self.readings = 0 297 standard_rest(30, 5) 298 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 299 300 # Calibrate 301 offset = int(round(self.average, 0)) 302 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 303 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 304 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 305 time.sleep(FLASH_SLEEP) 306 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 307 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 308 309 power_cycle_bms() 310 311 # Confirm current is in acceptable range 312 self.readings = 0 # Reset average 313 standard_rest(30, 5) 314 logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA") 315 assert ( 316 acceptable_error_ma > self.average > -acceptable_error_ma 317 ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA" 318 319 # Erase flash 320 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH) 321 time.sleep(FLASH_SLEEP) # Wait for erase to complete 322 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 323 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 324 325 # Confirm current is not in acceptable range 326 self.readings = 0 # Reset average 327 standard_rest(30, 5) 328 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 329 assert ( 330 not acceptable_error_ma > self.average > -acceptable_error_ma 331 ), f"Appears to be calibrated. {self.average:.3f} mA is within the limit of ±{acceptable_error_ma:.3f} mA"
Test erasing flash.
272 def bms_current(self): 273 """Measure serial current and calculate an average.""" 274 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 275 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 276 self.readings += 1 277 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample
Measure serial current and calculate an average.
279 def test_flash_erase(self): 280 """ 281 | Description | Test erasing flash | 282 | :------------------- | :--------------------------------------------------------------------- | 283 | GitHub Issue | turnaroundfactor/HITL#435 | 284 | Instructions | 1. Calibrate the BMS </br>\ 285 2. Power cycle </br>\ 286 3. Confirm BMS is still calibrated </br>\ 287 4. Send "Flash Erase" command with SMBus </br>\ 288 5. Confirm BMS is no longer calibrated | 289 | Pass / Fail Criteria | Pass if not calibrated | 290 | Estimated Duration | 1 minute | 291 """ 292 acceptable_error_ma = 5 293 294 _bms.csv.cycle = _bms.csv.cycle_smbus # Record serial and SMBus 295 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 296 self.readings = 0 297 standard_rest(30, 5) 298 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 299 300 # Calibrate 301 offset = int(round(self.average, 0)) 302 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 303 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 304 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 305 time.sleep(FLASH_SLEEP) 306 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 307 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 308 309 power_cycle_bms() 310 311 # Confirm current is in acceptable range 312 self.readings = 0 # Reset average 313 standard_rest(30, 5) 314 logger.write_result_to_html_report(f"Average rest current (calibrated): {self.average:.3f} mA") 315 assert ( 316 acceptable_error_ma > self.average > -acceptable_error_ma 317 ), f"{self.average:.3f} mA outside limit of ±{acceptable_error_ma:.3f} mA" 318 319 # Erase flash 320 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH) 321 time.sleep(FLASH_SLEEP) # Wait for erase to complete 322 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 323 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 324 325 # Confirm current is not in acceptable range 326 self.readings = 0 # Reset average 327 standard_rest(30, 5) 328 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 329 assert ( 330 not acceptable_error_ma > self.average > -acceptable_error_ma 331 ), f"Appears to be calibrated. {self.average:.3f} mA is within the limit of ±{acceptable_error_ma:.3f} mA"
| Description | Test erasing flash |
|---|---|
| GitHub Issue | turnaroundfactor/HITL#435 |
| Instructions | 1. Calibrate the BMS 2. Power cycle 3. Confirm BMS is still calibrated 4. Send "Flash Erase" command with SMBus 5. Confirm BMS is no longer calibrated |
| Pass / Fail Criteria | Pass if not calibrated |
| Estimated Duration | 1 minute |
334@pytest.mark.usefixtures("cycle_smbus") 335@pytest.mark.sim_cells 336class TestOTP: 337 """Test OTP registers.""" 338 339 average = 0 340 readings = 0 341 serial_id = 0xCAFE 342 343 def bms_current(self): 344 """Measure serial current and calculate an average.""" 345 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 346 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 347 self.readings += 1 348 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample 349 350 def calibrate_current(self): 351 """Calibrate BMS current in flash.""" 352 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 353 self.readings = 0 354 standard_rest(30, 5) 355 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 356 357 # Calibrate 358 offset = int(round(self.average, 0)) 359 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 360 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 361 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 362 time.sleep(FLASH_SLEEP) 363 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 364 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 365 366 _bms.csv.cycle.postfix_fn = lambda: ... 367 368 def is_calibrated(self) -> bool: 369 """Confirm current is in acceptable range""" 370 acceptable_error_ma = 5 371 372 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 373 self.readings = 0 # Reset average 374 standard_rest(30, 5) 375 logger.write_result_to_html_report(f"Average rest current (calibrated?): {self.average:.3f} mA") 376 return acceptable_error_ma > self.average > -acceptable_error_ma 377 378 def set_serial_id(self): 379 """Set the 16-bit serial ID.""" 380 _smbus.write_register(SMBusReg.SERIAL_NUM, self.serial_id) 381 time.sleep(FLASH_SLEEP) 382 383 def is_serial_set(self) -> bool: 384 """Check if the 16-bit serial ID has been set.""" 385 return _smbus.read_register(SMBusReg.SERIAL_NUM)[1] == self.serial_id.to_bytes(2, byteorder="little") 386 387 def enable_faults(self): 388 """Enable faults via SMBus.""" 389 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.FAULT_ENABLE) 390 time.sleep(FLASH_SLEEP) 391 392 def is_faults_enabled(self) -> bool: 393 """Check if overtemp faults can be raised.""" 394 timeout_s = 10 395 396 # Raise a fault 397 _plateset.thermistor1 = 65 398 start = time.perf_counter() 399 while (serial_data := serial_monitor.read(latest=True)) and not serial_data["flags.fault_overtemp_discharge"]: 400 if time.perf_counter() - start > timeout_s: 401 logger.write_debug_to_report(f"Over-temperature fault was not raised after {timeout_s} seconds.") 402 return False 403 404 # Clear the fault 405 _plateset.thermistor1 = 45 406 start = time.perf_counter() 407 while (serial_data := serial_monitor.read(latest=True)) and serial_data["flags.fault_overtemp_discharge"]: 408 if time.perf_counter() - start > timeout_s: 409 logger.write_debug_to_report(f"Over-temperature fault was not cleared after {timeout_s} seconds.") 410 return False 411 412 return True 413 414 def erase_flash(self): 415 """Erase the internal BMS flash.""" 416 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH) 417 time.sleep(FLASH_SLEEP) # Wait for erase to complete 418 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 419 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 420 421 def test_flash_otp(self): 422 """ 423 | Description | Test OTP registers | 424 | :------------------- | :--------------------------------------------------------------------- | 425 | GitHub Issue | turnaroundfactor/HITL#486 | 426 | Instructions | 1. Set up flash values </br>\ 427 ⠀⠀⦁ Enable assembly mode </br>\ 428 ⠀⠀⦁ Calibrate the BMS and verify </br>\ 429 ⠀⠀⦁ Set the serial number and verify </br>\ 430 ⠀⠀⦁ Enable the faults and verify </br>\ 431 2. Send "Flash Erase" command with SMBus </br>\ 432 3. Confirm BMS flash is erased </br>\ 433 ⠀⠀⦁ BMS isn't calibrated </br>\ 434 ⠀⠀⦁ Faults aren't enabled </br>\ 435 4. Set up flash values </br>\ 436 ⠀⠀⦁ Calibrate the BMS and verify </br>\ 437 ⠀⠀⦁ Enable the faults and verify </br>\ 438 5. Enter manufacturing mode </br>\ 439 6. Send "Flash Erase" command with SMBus </br>\ 440 7. Confirm BMS flash is not erased </br>\ 441 ⠀⠀⦁ BMS is calibrated </br>\ 442 ⠀⠀⦁ Faults are enabled | 443 | Pass / Fail Criteria | Pass flash cannot be erased | 444 | Estimated Duration | 1 minute | 445 """ 446 447 # 1. Set up flash values and enter assembly mode 448 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ASSEMBLY_MODE) 449 time.sleep(FLASH_SLEEP) 450 self.calibrate_current() 451 self.set_serial_id() 452 self.enable_faults() 453 power_cycle_bms() # Power cycle 454 assert self.is_calibrated(), "Current was not calibrated after power cycle." 455 assert self.is_serial_set(), "Serial was not set after power cycle." 456 assert self.is_faults_enabled(), "Faults were not enabled after power cycle." 457 458 # 2/3. Erase flash, confirm BMS flash is erased 459 self.erase_flash() 460 assert not self.is_calibrated(), "Current was still calibrated after erase." 461 assert self.is_serial_set(), "Serial was not set after erase." 462 assert not self.is_faults_enabled(), "Faults were still enabled after erase." 463 464 # 4. Set up flash values 465 self.calibrate_current() 466 self.enable_faults() 467 power_cycle_bms() # Power cycle 468 assert self.is_calibrated(), "Current was not calibrated after power cycle." 469 assert self.is_serial_set(), "Serial was not set after power cycle." 470 assert self.is_faults_enabled(), "Faults were not enabled after power cycle." 471 472 # 5. Enter manufacturing mode 473 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.PRODUCTION_MODE) 474 time.sleep(FLASH_SLEEP) 475 476 # 6. Erase flash, confirm BMS flash is not erased 477 self.erase_flash() 478 assert self.is_calibrated(), "Current was not calibrated after erase in manufacturing mode." 479 assert self.is_serial_set(), "Serial was not set after erase in manufacturing mode." 480 assert self.is_faults_enabled(), "Faults were not enabled after erase in manufacturing mode."
Test OTP registers.
343 def bms_current(self): 344 """Measure serial current and calculate an average.""" 345 new_reading = _bms.csv.cycle.last_serial_data["mamps"] 346 self.average = (new_reading + self.readings * self.average) / (self.readings + 1) 347 self.readings += 1 348 logger.write_info_to_report(f"BMS Serial Current (mA): {new_reading:.3f}") # Output current on every sample
Measure serial current and calculate an average.
350 def calibrate_current(self): 351 """Calibrate BMS current in flash.""" 352 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 353 self.readings = 0 354 standard_rest(30, 5) 355 logger.write_result_to_html_report(f"Average rest current: {self.average:.3f} mA") 356 357 # Calibrate 358 offset = int(round(self.average, 0)) 359 logger.write_info_to_report(f"Setting offset current to {offset:.3f} mA") 360 data = (ctypes.c_uint8(offset).value << 8) | BMSCommands.CALIBRATE 361 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, data) 362 time.sleep(FLASH_SLEEP) 363 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 364 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}") 365 366 _bms.csv.cycle.postfix_fn = lambda: ...
Calibrate BMS current in flash.
368 def is_calibrated(self) -> bool: 369 """Confirm current is in acceptable range""" 370 acceptable_error_ma = 5 371 372 _bms.csv.cycle.postfix_fn = self.bms_current # Get current on each sample 373 self.readings = 0 # Reset average 374 standard_rest(30, 5) 375 logger.write_result_to_html_report(f"Average rest current (calibrated?): {self.average:.3f} mA") 376 return acceptable_error_ma > self.average > -acceptable_error_ma
Confirm current is in acceptable range
378 def set_serial_id(self): 379 """Set the 16-bit serial ID.""" 380 _smbus.write_register(SMBusReg.SERIAL_NUM, self.serial_id) 381 time.sleep(FLASH_SLEEP)
Set the 16-bit serial ID.
383 def is_serial_set(self) -> bool: 384 """Check if the 16-bit serial ID has been set.""" 385 return _smbus.read_register(SMBusReg.SERIAL_NUM)[1] == self.serial_id.to_bytes(2, byteorder="little")
Check if the 16-bit serial ID has been set.
387 def enable_faults(self): 388 """Enable faults via SMBus.""" 389 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.FAULT_ENABLE) 390 time.sleep(FLASH_SLEEP)
Enable faults via SMBus.
392 def is_faults_enabled(self) -> bool: 393 """Check if overtemp faults can be raised.""" 394 timeout_s = 10 395 396 # Raise a fault 397 _plateset.thermistor1 = 65 398 start = time.perf_counter() 399 while (serial_data := serial_monitor.read(latest=True)) and not serial_data["flags.fault_overtemp_discharge"]: 400 if time.perf_counter() - start > timeout_s: 401 logger.write_debug_to_report(f"Over-temperature fault was not raised after {timeout_s} seconds.") 402 return False 403 404 # Clear the fault 405 _plateset.thermistor1 = 45 406 start = time.perf_counter() 407 while (serial_data := serial_monitor.read(latest=True)) and serial_data["flags.fault_overtemp_discharge"]: 408 if time.perf_counter() - start > timeout_s: 409 logger.write_debug_to_report(f"Over-temperature fault was not cleared after {timeout_s} seconds.") 410 return False 411 412 return True
Check if overtemp faults can be raised.
414 def erase_flash(self): 415 """Erase the internal BMS flash.""" 416 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ERASE_FLASH) 417 time.sleep(FLASH_SLEEP) # Wait for erase to complete 418 data = _smbus.read_register(SMBusReg.MANUFACTURING_ACCESS)[0] 419 logger.write_info_to_report(f"{SMBusReg.MANUFACTURING_ACCESS.fname}: {data:04X}")
Erase the internal BMS flash.
421 def test_flash_otp(self): 422 """ 423 | Description | Test OTP registers | 424 | :------------------- | :--------------------------------------------------------------------- | 425 | GitHub Issue | turnaroundfactor/HITL#486 | 426 | Instructions | 1. Set up flash values </br>\ 427 ⠀⠀⦁ Enable assembly mode </br>\ 428 ⠀⠀⦁ Calibrate the BMS and verify </br>\ 429 ⠀⠀⦁ Set the serial number and verify </br>\ 430 ⠀⠀⦁ Enable the faults and verify </br>\ 431 2. Send "Flash Erase" command with SMBus </br>\ 432 3. Confirm BMS flash is erased </br>\ 433 ⠀⠀⦁ BMS isn't calibrated </br>\ 434 ⠀⠀⦁ Faults aren't enabled </br>\ 435 4. Set up flash values </br>\ 436 ⠀⠀⦁ Calibrate the BMS and verify </br>\ 437 ⠀⠀⦁ Enable the faults and verify </br>\ 438 5. Enter manufacturing mode </br>\ 439 6. Send "Flash Erase" command with SMBus </br>\ 440 7. Confirm BMS flash is not erased </br>\ 441 ⠀⠀⦁ BMS is calibrated </br>\ 442 ⠀⠀⦁ Faults are enabled | 443 | Pass / Fail Criteria | Pass flash cannot be erased | 444 | Estimated Duration | 1 minute | 445 """ 446 447 # 1. Set up flash values and enter assembly mode 448 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.ASSEMBLY_MODE) 449 time.sleep(FLASH_SLEEP) 450 self.calibrate_current() 451 self.set_serial_id() 452 self.enable_faults() 453 power_cycle_bms() # Power cycle 454 assert self.is_calibrated(), "Current was not calibrated after power cycle." 455 assert self.is_serial_set(), "Serial was not set after power cycle." 456 assert self.is_faults_enabled(), "Faults were not enabled after power cycle." 457 458 # 2/3. Erase flash, confirm BMS flash is erased 459 self.erase_flash() 460 assert not self.is_calibrated(), "Current was still calibrated after erase." 461 assert self.is_serial_set(), "Serial was not set after erase." 462 assert not self.is_faults_enabled(), "Faults were still enabled after erase." 463 464 # 4. Set up flash values 465 self.calibrate_current() 466 self.enable_faults() 467 power_cycle_bms() # Power cycle 468 assert self.is_calibrated(), "Current was not calibrated after power cycle." 469 assert self.is_serial_set(), "Serial was not set after power cycle." 470 assert self.is_faults_enabled(), "Faults were not enabled after power cycle." 471 472 # 5. Enter manufacturing mode 473 _smbus.write_register(SMBusReg.MANUFACTURING_ACCESS, BMSCommands.PRODUCTION_MODE) 474 time.sleep(FLASH_SLEEP) 475 476 # 6. Erase flash, confirm BMS flash is not erased 477 self.erase_flash() 478 assert self.is_calibrated(), "Current was not calibrated after erase in manufacturing mode." 479 assert self.is_serial_set(), "Serial was not set after erase in manufacturing mode." 480 assert self.is_faults_enabled(), "Faults were not enabled after erase in manufacturing mode."
| Description | Test OTP registers |
|---|---|
| GitHub Issue | turnaroundfactor/HITL#486 |
| Instructions | 1. Set up flash values ⠀⠀⦁ Enable assembly mode ⠀⠀⦁ Calibrate the BMS and verify ⠀⠀⦁ Set the serial number and verify ⠀⠀⦁ Enable the faults and verify 2. Send "Flash Erase" command with SMBus 3. Confirm BMS flash is erased ⠀⠀⦁ BMS isn't calibrated ⠀⠀⦁ Faults aren't enabled 4. Set up flash values ⠀⠀⦁ Calibrate the BMS and verify ⠀⠀⦁ Enable the faults and verify 5. Enter manufacturing mode 6. Send "Flash Erase" command with SMBus 7. Confirm BMS flash is not erased ⠀⠀⦁ BMS is calibrated ⠀⠀⦁ Faults are enabled |
| Pass / Fail Criteria | Pass flash cannot be erased |
| Estimated Duration | 1 minute |