hitl_tester.test_cases.bms.test_nicd_cell
Turn on and off relays for charging. 700 mA for 14 hours After charged, turn on relay for discharge resistor Measure how long (time) it takes for the cell to get to 1.21V and 1.2V Stop discharging at 1.2V Cells that last >5 hours before the 1.21V cutoff need to go to the 20S pack Cells that last >5 hours before the 1.2V cutoff can go to 17S or 20S pack Any cells that do not last 5 hours should not be used in a pack
We are using this hardware: ADCplate (https://pi-plates.com/adcplate-users-guide/) x3 RELAYplate2 (https://pi-plates.com/relayplate2/) x3
D0 - D3 = voltages 3 plates = 12 voltages
Used in these test plans:
- nicd_qc_test ⠀⠀⠀(bms/nicd_qc_test.plan)
Example Command (warning: test plan may run other test cases):
./hitl_tester.py nicd_qc_test -DREPORT_FILE=""
1""" 2Turn on and off relays for charging. 700 mA for 14 hours 3After charged, turn on relay for discharge resistor 4Measure how long (time) it takes for the cell to get to 1.21V and 1.2V 5Stop discharging at 1.2V 6Cells that last >5 hours before the 1.21V cutoff need to go to the 20S pack 7Cells that last >5 hours before the 1.2V cutoff can go to 17S or 20S pack 8Any cells that do not last 5 hours should not be used in a pack 9 10We are using this hardware: 11ADCplate (https://pi-plates.com/adcplate-users-guide/) x3 12RELAYplate2 (https://pi-plates.com/relayplate2/) x3 13 14D0 - D3 = voltages 153 plates = 12 voltages 16""" 17 18from __future__ import annotations 19 20import atexit 21import datetime 22import pathlib 23import platform 24import signal 25import sys 26import time 27from dataclasses import dataclass 28from enum import Enum 29 30import pytest 31import yaml 32from colorama import init, Fore 33 34from hitl_tester.modules.bms.bms_hw import BMSHardware 35from hitl_tester.modules.bms_types import BMSFlags, ResourceNotFoundError 36from hitl_tester.modules.bms.csv_tables import CSVRecorders 37from hitl_tester.modules.logger import logger 38 39if hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags) and pytest.flags.dry_run: 40 from hitl_tester.modules.bms.pseudo_hardware import ADC 41 from hitl_tester.modules.bms.pseudo_hardware import RELAY2 42else: 43 import piplates.ADCplate as ADC # type: ignore[no-redef] 44 import piplates.RELAYplate2 as RELAY2 # type: ignore[no-redef] 45 46init(True, strip=True) # Initialize colorama (autoreset) 47 48REPORT_FILE = "" 49 50 51@dataclass 52class Port: 53 """The board and channel on a PiPlate.""" 54 55 board_id: int 56 channel_id: int 57 58 59class CellState(Enum): 60 """The current state of the NiCd cell.""" 61 62 RESTING = Fore.LIGHTBLACK_EX 63 DISCHARGING = Fore.CYAN 64 PASSED_20S = Fore.LIGHTGREEN_EX 65 PASSED_17S_OR_20S = Fore.GREEN 66 FAILED = Fore.LIGHTRED_EX 67 68 69@dataclass 70class Cell: 71 """A NiCd cell attached to the HITL.""" 72 73 adc_port: Port 74 relay_port: Port 75 ident: int 76 timestamp_start: float = 0.0 77 timestamp_1210_ma: float = 0.0 78 timestamp_1200_ma: float = 0.0 79 _state: CellState = CellState.RESTING 80 csv: CSVRecorders | None = None 81 82 def __post_init__(self): 83 """Set sample rate for cell voltage.""" 84 ADC.setMODE(self.adc_port.board_id, "ADV") # Put board_id into advanced 85 ADC.configINPUT( 86 self.adc_port.board_id, f"D{self.adc_port.channel_id}", 7, True 87 ) # 0nly 8 can be enabled at any time 88 89 BMSHardware().report_filename = REPORT_FILE 90 self.csv = CSVRecorders(BMSHardware()) 91 92 @atexit.register 93 def __atexit__(): 94 """Configure a safe shut down for when the class instance is destroyed.""" 95 logger.write_info_to_report(f"Disabling cell B{self.relay_port.board_id}:C{self.relay_port.channel_id}") 96 self.state = CellState.RESTING 97 98 @property 99 def state(self): 100 """Get current cell state.""" 101 return self._state 102 103 @state.setter 104 def state(self, new_state: CellState): 105 """Set current cell state.""" 106 if new_state == CellState.DISCHARGING: 107 RELAY2.relayON(self.relay_port.board_id, self.relay_port.channel_id) 108 else: 109 RELAY2.relayOFF(self.relay_port.board_id, self.relay_port.channel_id) 110 self._state = new_state 111 112 @property 113 def voltage(self) -> float: 114 """Measure a voltage using the ADC plates.""" 115 return float(ADC.readSINGLE(self.adc_port.board_id, f"D{self.adc_port.channel_id}") or 0.0) 116 117 def __str__(self) -> str: 118 """Formatted string representation.""" 119 return ( 120 f"{self.state.value}{self.state.name}{Fore.RESET}" 121 f"[B{self.relay_port.board_id}:C{self.relay_port.channel_id}, {self.voltage}V]" 122 ) 123 124 125def safe_shutdown_handler(_signo, _stack_frame): 126 """ 127 This handler will be called when a HANGUP signal is received. 128 i.e. ssh connection drops out. In this event, we're going to 129 ensure that the test setup is transitioned to a safe state. 130 """ 131 signal.signal(_signo, signal.SIG_IGN) # Ignore additional signals 132 logger.write_critical_to_report(f"Caught {signal.Signals(_signo).name}, Powering OFF HITL!!!") 133 sys.exit(0) # exit gracefully (Calls bms_teardown) 134 135 136def test_safe_shutdown(): 137 """Initialize safe shutdown handler.""" 138 for signal_flag in (signal.SIGABRT, signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGCHLD): 139 signal.signal(signal_flag, safe_shutdown_handler) 140 141 142def test_logger(): 143 """Initialize the log file.""" 144 global REPORT_FILE 145 146 hitl_config = pathlib.Path(__file__).parent.resolve() / ".." / "hitl_config.yml" 147 with open(hitl_config, "r", encoding="utf-8") as config_file: 148 for config in yaml.safe_load_all(config_file): 149 if config["plate_set_id"] == pytest.flags.plateset_id: 150 # Record the report filename 151 REPORT_FILE = pytest.flags.report_filename 152 153 # initialize the logger 154 logger.use_file_mode(REPORT_FILE, config["log_level"]) 155 logger.write_info_to_report("Found plateset") 156 break 157 else: 158 raise ResourceNotFoundError(f'Plateset "{pytest.flags.plateset_id}" does not exist in "{hitl_config.name}"') 159 160 161def formatted_time(seconds: float) -> str: 162 """Return time formatted as hh:mm:ss.""" 163 return str(datetime.timedelta(seconds=seconds)).partition(".")[0] 164 165 166def test_discharge(): 167 """Charge at 700 mA for 14 hours.""" 168 logger.write_info_to_report("Beginning discharge test") 169 cell_holder = "A" if platform.node() == "HITL-4-1" else "B" if platform.node() == "HITL-4-2" else "?" 170 all_cells = [ 171 Cell(adc_port=Port(0, 0), relay_port=Port(3, 8), ident=1), 172 Cell(adc_port=Port(0, 1), relay_port=Port(3, 1), ident=2), 173 Cell(adc_port=Port(0, 2), relay_port=Port(3, 2), ident=3), 174 Cell(adc_port=Port(0, 3), relay_port=Port(3, 3), ident=4), 175 Cell(adc_port=Port(1, 0), relay_port=Port(3, 4), ident=5), 176 Cell(adc_port=Port(1, 1), relay_port=Port(3, 5), ident=6), 177 Cell(adc_port=Port(1, 2), relay_port=Port(3, 6), ident=7), 178 Cell(adc_port=Port(1, 3), relay_port=Port(3, 7), ident=8), 179 Cell(adc_port=Port(2, 0), relay_port=Port(4, 8), ident=9), 180 Cell(adc_port=Port(2, 1), relay_port=Port(4, 1), ident=10), 181 Cell(adc_port=Port(2, 2), relay_port=Port(4, 2), ident=11), 182 Cell(adc_port=Port(2, 3), relay_port=Port(4, 3), ident=12), 183 ] 184 logger.write_info_to_report(", ".join(map(str, all_cells))) 185 test_start = time.perf_counter() 186 187 # Initialize cells 188 for cell in all_cells: 189 cell.timestamp_start = time.perf_counter() 190 cell.state = CellState.DISCHARGING 191 cell.csv.nicd.create_file("0000_", f"_{cell_holder}{cell.ident:02}") 192 193 # Discharge cells to 1.2V 194 while CellState.DISCHARGING in [cell.state for cell in all_cells]: 195 time_remaining_seconds = max(0.0, (5 * 3600) - (time.perf_counter() - test_start)) 196 time_remaining_string = formatted_time(time_remaining_seconds) 197 logger.write_info_to_report(f"Time remaining (H:M:S): {time_remaining_string}") 198 logger.write_info_to_report(", ".join(map(str, all_cells))) 199 for cell in all_cells: 200 old_cell_state = cell.state 201 volts = cell.voltage 202 if not cell.timestamp_1210_ma and volts <= 1.21: 203 cell.timestamp_1210_ma = time.perf_counter() 204 elif not cell.timestamp_1200_ma and volts <= 1.20: 205 cell.timestamp_1200_ma = time.perf_counter() 206 207 if cell.timestamp_1210_ma - cell.timestamp_start >= 5 * 3600: # >5 hours before 1.21V = 20S 208 cell.state = CellState.PASSED_20S 209 elif cell.timestamp_1200_ma - cell.timestamp_start >= 5 * 3600: # >5 hours before 1.2V = 17S/20S 210 cell.state = CellState.PASSED_17S_OR_20S 211 else: 212 cell.state = CellState.FAILED 213 214 # Record CSV 215 if cell.state == CellState.DISCHARGING or cell.state != old_cell_state: 216 cell.csv.nicd.record( 217 time.perf_counter() - test_start, 218 cell.relay_port.board_id, 219 cell.relay_port.channel_id, 220 volts, 221 cell.state.name.title(), 222 int(cell.timestamp_1210_ma - cell.timestamp_start) if cell.timestamp_1210_ma else None, 223 int(cell.timestamp_1200_ma - cell.timestamp_start) if cell.timestamp_1200_ma else None, 224 ) 225 time.sleep(10) 226 227 # Log results 228 logger.write_info_to_report("Completed discharge test") 229 for cell in all_cells: 230 logger.write_info_to_report( 231 f"Relay board: {cell.relay_port.board_id}, " 232 f"Relay channel: {cell.relay_port.channel_id}, " 233 f"Time to 1.21V (H:M:S): {formatted_time(cell.timestamp_1210_ma - cell.timestamp_start)}, " 234 f"Time to 1.20V (H:M:S): {formatted_time(cell.timestamp_1200_ma - cell.timestamp_start)}, " 235 f"Result: {cell.state.value}{cell.state.name}{Fore.RESET}" 236 )
52@dataclass 53class Port: 54 """The board and channel on a PiPlate.""" 55 56 board_id: int 57 channel_id: int
The board and channel on a PiPlate.
60class CellState(Enum): 61 """The current state of the NiCd cell.""" 62 63 RESTING = Fore.LIGHTBLACK_EX 64 DISCHARGING = Fore.CYAN 65 PASSED_20S = Fore.LIGHTGREEN_EX 66 PASSED_17S_OR_20S = Fore.GREEN 67 FAILED = Fore.LIGHTRED_EX
The current state of the NiCd cell.
Inherited Members
- enum.Enum
- name
- value
70@dataclass 71class Cell: 72 """A NiCd cell attached to the HITL.""" 73 74 adc_port: Port 75 relay_port: Port 76 ident: int 77 timestamp_start: float = 0.0 78 timestamp_1210_ma: float = 0.0 79 timestamp_1200_ma: float = 0.0 80 _state: CellState = CellState.RESTING 81 csv: CSVRecorders | None = None 82 83 def __post_init__(self): 84 """Set sample rate for cell voltage.""" 85 ADC.setMODE(self.adc_port.board_id, "ADV") # Put board_id into advanced 86 ADC.configINPUT( 87 self.adc_port.board_id, f"D{self.adc_port.channel_id}", 7, True 88 ) # 0nly 8 can be enabled at any time 89 90 BMSHardware().report_filename = REPORT_FILE 91 self.csv = CSVRecorders(BMSHardware()) 92 93 @atexit.register 94 def __atexit__(): 95 """Configure a safe shut down for when the class instance is destroyed.""" 96 logger.write_info_to_report(f"Disabling cell B{self.relay_port.board_id}:C{self.relay_port.channel_id}") 97 self.state = CellState.RESTING 98 99 @property 100 def state(self): 101 """Get current cell state.""" 102 return self._state 103 104 @state.setter 105 def state(self, new_state: CellState): 106 """Set current cell state.""" 107 if new_state == CellState.DISCHARGING: 108 RELAY2.relayON(self.relay_port.board_id, self.relay_port.channel_id) 109 else: 110 RELAY2.relayOFF(self.relay_port.board_id, self.relay_port.channel_id) 111 self._state = new_state 112 113 @property 114 def voltage(self) -> float: 115 """Measure a voltage using the ADC plates.""" 116 return float(ADC.readSINGLE(self.adc_port.board_id, f"D{self.adc_port.channel_id}") or 0.0) 117 118 def __str__(self) -> str: 119 """Formatted string representation.""" 120 return ( 121 f"{self.state.value}{self.state.name}{Fore.RESET}" 122 f"[B{self.relay_port.board_id}:C{self.relay_port.channel_id}, {self.voltage}V]" 123 )
A NiCd cell attached to the HITL.
126def safe_shutdown_handler(_signo, _stack_frame): 127 """ 128 This handler will be called when a HANGUP signal is received. 129 i.e. ssh connection drops out. In this event, we're going to 130 ensure that the test setup is transitioned to a safe state. 131 """ 132 signal.signal(_signo, signal.SIG_IGN) # Ignore additional signals 133 logger.write_critical_to_report(f"Caught {signal.Signals(_signo).name}, Powering OFF HITL!!!") 134 sys.exit(0) # exit gracefully (Calls bms_teardown)
This handler will be called when a HANGUP signal is received. i.e. ssh connection drops out. In this event, we're going to ensure that the test setup is transitioned to a safe state.
137def test_safe_shutdown(): 138 """Initialize safe shutdown handler.""" 139 for signal_flag in (signal.SIGABRT, signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGCHLD): 140 signal.signal(signal_flag, safe_shutdown_handler)
Initialize safe shutdown handler.
143def test_logger(): 144 """Initialize the log file.""" 145 global REPORT_FILE 146 147 hitl_config = pathlib.Path(__file__).parent.resolve() / ".." / "hitl_config.yml" 148 with open(hitl_config, "r", encoding="utf-8") as config_file: 149 for config in yaml.safe_load_all(config_file): 150 if config["plate_set_id"] == pytest.flags.plateset_id: 151 # Record the report filename 152 REPORT_FILE = pytest.flags.report_filename 153 154 # initialize the logger 155 logger.use_file_mode(REPORT_FILE, config["log_level"]) 156 logger.write_info_to_report("Found plateset") 157 break 158 else: 159 raise ResourceNotFoundError(f'Plateset "{pytest.flags.plateset_id}" does not exist in "{hitl_config.name}"')
Initialize the log file.
162def formatted_time(seconds: float) -> str: 163 """Return time formatted as hh:mm:ss.""" 164 return str(datetime.timedelta(seconds=seconds)).partition(".")[0]
Return time formatted as hh:mm:ss.
167def test_discharge(): 168 """Charge at 700 mA for 14 hours.""" 169 logger.write_info_to_report("Beginning discharge test") 170 cell_holder = "A" if platform.node() == "HITL-4-1" else "B" if platform.node() == "HITL-4-2" else "?" 171 all_cells = [ 172 Cell(adc_port=Port(0, 0), relay_port=Port(3, 8), ident=1), 173 Cell(adc_port=Port(0, 1), relay_port=Port(3, 1), ident=2), 174 Cell(adc_port=Port(0, 2), relay_port=Port(3, 2), ident=3), 175 Cell(adc_port=Port(0, 3), relay_port=Port(3, 3), ident=4), 176 Cell(adc_port=Port(1, 0), relay_port=Port(3, 4), ident=5), 177 Cell(adc_port=Port(1, 1), relay_port=Port(3, 5), ident=6), 178 Cell(adc_port=Port(1, 2), relay_port=Port(3, 6), ident=7), 179 Cell(adc_port=Port(1, 3), relay_port=Port(3, 7), ident=8), 180 Cell(adc_port=Port(2, 0), relay_port=Port(4, 8), ident=9), 181 Cell(adc_port=Port(2, 1), relay_port=Port(4, 1), ident=10), 182 Cell(adc_port=Port(2, 2), relay_port=Port(4, 2), ident=11), 183 Cell(adc_port=Port(2, 3), relay_port=Port(4, 3), ident=12), 184 ] 185 logger.write_info_to_report(", ".join(map(str, all_cells))) 186 test_start = time.perf_counter() 187 188 # Initialize cells 189 for cell in all_cells: 190 cell.timestamp_start = time.perf_counter() 191 cell.state = CellState.DISCHARGING 192 cell.csv.nicd.create_file("0000_", f"_{cell_holder}{cell.ident:02}") 193 194 # Discharge cells to 1.2V 195 while CellState.DISCHARGING in [cell.state for cell in all_cells]: 196 time_remaining_seconds = max(0.0, (5 * 3600) - (time.perf_counter() - test_start)) 197 time_remaining_string = formatted_time(time_remaining_seconds) 198 logger.write_info_to_report(f"Time remaining (H:M:S): {time_remaining_string}") 199 logger.write_info_to_report(", ".join(map(str, all_cells))) 200 for cell in all_cells: 201 old_cell_state = cell.state 202 volts = cell.voltage 203 if not cell.timestamp_1210_ma and volts <= 1.21: 204 cell.timestamp_1210_ma = time.perf_counter() 205 elif not cell.timestamp_1200_ma and volts <= 1.20: 206 cell.timestamp_1200_ma = time.perf_counter() 207 208 if cell.timestamp_1210_ma - cell.timestamp_start >= 5 * 3600: # >5 hours before 1.21V = 20S 209 cell.state = CellState.PASSED_20S 210 elif cell.timestamp_1200_ma - cell.timestamp_start >= 5 * 3600: # >5 hours before 1.2V = 17S/20S 211 cell.state = CellState.PASSED_17S_OR_20S 212 else: 213 cell.state = CellState.FAILED 214 215 # Record CSV 216 if cell.state == CellState.DISCHARGING or cell.state != old_cell_state: 217 cell.csv.nicd.record( 218 time.perf_counter() - test_start, 219 cell.relay_port.board_id, 220 cell.relay_port.channel_id, 221 volts, 222 cell.state.name.title(), 223 int(cell.timestamp_1210_ma - cell.timestamp_start) if cell.timestamp_1210_ma else None, 224 int(cell.timestamp_1200_ma - cell.timestamp_start) if cell.timestamp_1200_ma else None, 225 ) 226 time.sleep(10) 227 228 # Log results 229 logger.write_info_to_report("Completed discharge test") 230 for cell in all_cells: 231 logger.write_info_to_report( 232 f"Relay board: {cell.relay_port.board_id}, " 233 f"Relay channel: {cell.relay_port.channel_id}, " 234 f"Time to 1.21V (H:M:S): {formatted_time(cell.timestamp_1210_ma - cell.timestamp_start)}, " 235 f"Time to 1.20V (H:M:S): {formatted_time(cell.timestamp_1200_ma - cell.timestamp_start)}, " 236 f"Result: {cell.state.value}{cell.state.name}{Fore.RESET}" 237 )
Charge at 700 mA for 14 hours.