hitl_tester.test_cases.bms.test_flash_firmware
| Test | Flash the BMS |
|---|---|
| GitHub Issue(s) | turnaroundfactor/HITL#374 |
| Description | Power on the cell sims, flash the board, and power down |
Used in these test plans:
- dev_bms ⠀⠀⠀(bms/dev_bms.plan)
- flash_dev_b ⠀⠀⠀(bms/flash_dev_b.plan)
- flash_dev_a ⠀⠀⠀(bms/flash_dev_a.plan)
- flash_live_a ⠀⠀⠀(bms/flash_live_a.plan)
- prod_bms ⠀⠀⠀(bms/prod_bms.plan)
- flash_live_b ⠀⠀⠀(bms/flash_live_b.plan)
- qa_bms ⠀⠀⠀(bms/qa_bms.plan)
Example Command (warning: test plan may run other test cases):
./hitl_tester.py dev_bms -DELF_PATH=. -DELF_FILE=battery-benchtop-rev1-barechip.elf -DBRANCH="" -DCAPACITY=0 -DDEFAULT_SOC_PERCENT=0.8 -DDEFAULT_TEMPERATURE_C=15 -DMAX_ATTEMPTS=5 -DHITL_DEBUG=0 -DBUILD=BB2590_v2_BOARD -DCONFIG_TEMPLATE=/opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/site-packages/hitl_tester/test_cases/bms/flash_template.cfg
1""" 2| Test | Flash the BMS | 3| :------------------- | :------------------------------------------------------ | 4| GitHub Issue(s) | turnaroundfactor/HITL#374 | 5| Description | Power on the cell sims, flash the board, and power down | 6""" 7 8import shlex 9import signal 10import subprocess 11import tempfile 12import time 13from pathlib import Path 14from string import Template 15 16import pytest 17 18from hitl_tester.modules.bms.bms_hw import BMSHardware 19from hitl_tester.modules.bms.plateset import Plateset 20from hitl_tester.modules.bms_types import BMSFlags 21from hitl_tester.modules.logger import logger 22from hitl_tester.modules import properties 23 24ELF_PATH = Path(".") 25"""Where to search for the elf file.""" 26 27ELF_FILE = Path("battery-benchtop-rev1-barechip.elf") 28"""The elf file to use.""" 29 30BRANCH = "" 31"""The name of the branch to build from, otherwise use local elf file.""" 32 33CAPACITY = 0 34"""The desired BMS capacity in mAh, otherwise default.""" 35 36DEFAULT_SOC_PERCENT = 0.80 37"""The cell SOC to start at if on cell sims.""" 38 39DEFAULT_TEMPERATURE_C = 15 40"""The thermistor temperature to start at if on cell sims.""" 41 42MAX_ATTEMPTS = 5 43"""How many flash attempts to make before failing.""" 44 45HITL_DEBUG = 0 46"""Puts the firmware into debug mode, lowering thresholds.""" 47 48BUILD = "BB2590_v2_BOARD" 49"""The board type to use.""" 50 51CONFIG_TEMPLATE = Path(__file__).resolve().parent / "flash_template.cfg" # Path relative to this file 52"""The template for the flash config.""" 53 54properties.apply() # Allow modifying the above globals 55_bms = BMSHardware(pytest.flags) # type: ignore[arg-type] 56_bms.init() 57_plateset = Plateset() 58 59 60def reset_cell_sims(): 61 """Activate cell sims and set appropriate temperatures.""" 62 logger.write_info_to_report("Setting temperature to 15°C") 63 _plateset.thermistor1 = DEFAULT_TEMPERATURE_C 64 _plateset.thermistor2 = DEFAULT_TEMPERATURE_C 65 logger.write_info_to_report("Powering up cell sims") 66 for cell in _bms.cells.values(): 67 cell.state_of_charge = DEFAULT_SOC_PERCENT 68 cell.disengage_safety_protocols = False 69 70 71def terminal_task(command: str, check: bool = False) -> str: 72 """Execute a terminal command, outputting live text. and confirming it succeeded.""" 73 old_signal_handler = signal.getsignal(signal.SIGCHLD) 74 signal.signal(signal.SIGCHLD, signal.SIG_DFL) 75 output = "" 76 with subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as process: 77 if process.stdout is not None: 78 for line in process.stdout: 79 output += line 80 if line := line.strip("\n"): 81 logger.write_info_to_report(f" {line}") 82 if process.wait() and check: 83 logger.write_critical_to_report(f"[{process.returncode}] {process.stderr}") 84 raise RuntimeError("Command failed") 85 signal.signal(signal.SIGCHLD, old_signal_handler) 86 return output 87 88 89def execute_flash(config: Path | str): 90 """Run the flash command.""" 91 command = f'openocd -s /usr/share/openocd/scripts -f {config} -c "program {ELF_PATH / ELF_FILE} verify reset exit"' 92 logger.write_debug_to_report(f"Using command: {command}") 93 return "Verified OK" in terminal_task(command) 94 95 96def test_flash_firmware(): 97 """ 98 | Requirement | Flash new firmware | 99 | :------------------- | :------------------------------------------------ | 100 | GitHub Issue | turnaroundfactor/HITL#216 | 101 | Instructions | 1. If a branch is provided, build and use it </br>\ 102 2. Activate the given ST-Link </br>\ 103 3. Flash using openocd </br>\ 104 4. Deactivate the given ST-Link | 105 | Pass / Fail Criteria | Flash succeeded | 106 | Estimated Duration | 30 seconds | 107 """ 108 global ELF_PATH 109 reset_cell_sims() 110 111 # Checkout branch 112 if BRANCH: 113 firmware_path = Path("../battery-benchtop-rev1") 114 if not firmware_path.is_dir(): 115 pytest.fail("Firmware repo is not installed. Cannot build ELF.") 116 117 fetch_cmd = f"git -C {firmware_path} fetch" 118 checkout_cmd = f"git -C {firmware_path} checkout {BRANCH}" 119 merge_cmd = f"git -C {firmware_path} merge" 120 build_cmake_cmd = ( 121 f"cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_C_COMPILER=/usr/bin/clang-11 " 122 f"-DCMAKE_CXX_COMPILER=/usr/bin/clang-cpp-11 -G Ninja {firmware_path} " 123 f"-B {firmware_path / 'cmake-build-debug'} -DBUILD={BUILD} -DCAPACITY={CAPACITY} " 124 f"-DHITL_DEBUG={HITL_DEBUG:d} -U CMAKE_C_COMPILER -U CMAKE_CXX_COMPILER" 125 ) 126 build_elf_cmd = f"cmake --build {firmware_path / 'cmake-build-debug'} --target all -- -j 8" 127 logger.write_info_to_report("Fetching remote branches") 128 terminal_task(fetch_cmd, check=False) 129 logger.write_info_to_report(f"Checking out branch {BRANCH}") 130 terminal_task(checkout_cmd, check=False) 131 logger.write_info_to_report("Merging changes") 132 terminal_task(merge_cmd, check=False) 133 logger.write_info_to_report("Building...") 134 terminal_task(build_cmake_cmd, check=False) 135 terminal_task(build_elf_cmd, check=False) 136 ELF_PATH = firmware_path / "cmake-build-debug" 137 138 # Confirm that the files exist 139 if not Path(ELF_PATH / ELF_FILE).is_file(): 140 raise FileNotFoundError(f"{ELF_PATH / ELF_FILE} does not exist.") 141 logger.write_info_to_report(f"ELF Filename: {ELF_PATH / ELF_FILE}") 142 143 # Create config file 144 assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags) 145 serial_number = pytest.flags.config["st_link_serial"] 146 with tempfile.NamedTemporaryFile("w+", encoding="UTF-8") as temp_fp: 147 logger.write_info_to_report(f"Configuration Filename: {temp_fp.name}") 148 with open(CONFIG_TEMPLATE, encoding="UTF-8") as config_fp: 149 template = Template(config_fp.read()) 150 temp_fp.write(template.substitute({"serial_number": serial_number})) 151 temp_fp.flush() 152 temp_fp.seek(0) 153 logger.write_debug_to_report(f"Template contents:\n{temp_fp.read()}") 154 155 # Attempt to flash 156 logger.write_info_to_report("Flashing...") 157 time.sleep(3) 158 for attempt in range(MAX_ATTEMPTS): 159 if execute_flash(temp_fp.name): 160 break 161 logger.write_error_to_report(f"Flashing failed. Retrying {attempt + 1}/{MAX_ATTEMPTS}.") 162 time.sleep(3) 163 else: 164 raise RuntimeError(f"Flashing failed after {MAX_ATTEMPTS} attempts.")
ELF_PATH =
PosixPath('.')
Where to search for the elf file.
ELF_FILE =
PosixPath('battery-benchtop-rev1-barechip.elf')
The elf file to use.
BRANCH =
''
The name of the branch to build from, otherwise use local elf file.
CAPACITY =
0
The desired BMS capacity in mAh, otherwise default.
DEFAULT_SOC_PERCENT =
0.8
The cell SOC to start at if on cell sims.
DEFAULT_TEMPERATURE_C =
15
The thermistor temperature to start at if on cell sims.
MAX_ATTEMPTS =
5
How many flash attempts to make before failing.
HITL_DEBUG =
0
Puts the firmware into debug mode, lowering thresholds.
BUILD =
'BB2590_v2_BOARD'
The board type to use.
CONFIG_TEMPLATE =
PosixPath('/opt/hostedtoolcache/Python/3.12.7/x64/lib/python3.12/site-packages/hitl_tester/test_cases/bms/flash_template.cfg')
The template for the flash config.
def
reset_cell_sims():
61def reset_cell_sims(): 62 """Activate cell sims and set appropriate temperatures.""" 63 logger.write_info_to_report("Setting temperature to 15°C") 64 _plateset.thermistor1 = DEFAULT_TEMPERATURE_C 65 _plateset.thermistor2 = DEFAULT_TEMPERATURE_C 66 logger.write_info_to_report("Powering up cell sims") 67 for cell in _bms.cells.values(): 68 cell.state_of_charge = DEFAULT_SOC_PERCENT 69 cell.disengage_safety_protocols = False
Activate cell sims and set appropriate temperatures.
def
terminal_task(command: str, check: bool = False) -> str:
72def terminal_task(command: str, check: bool = False) -> str: 73 """Execute a terminal command, outputting live text. and confirming it succeeded.""" 74 old_signal_handler = signal.getsignal(signal.SIGCHLD) 75 signal.signal(signal.SIGCHLD, signal.SIG_DFL) 76 output = "" 77 with subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as process: 78 if process.stdout is not None: 79 for line in process.stdout: 80 output += line 81 if line := line.strip("\n"): 82 logger.write_info_to_report(f" {line}") 83 if process.wait() and check: 84 logger.write_critical_to_report(f"[{process.returncode}] {process.stderr}") 85 raise RuntimeError("Command failed") 86 signal.signal(signal.SIGCHLD, old_signal_handler) 87 return output
Execute a terminal command, outputting live text. and confirming it succeeded.
def
execute_flash(config: pathlib.Path | str):
90def execute_flash(config: Path | str): 91 """Run the flash command.""" 92 command = f'openocd -s /usr/share/openocd/scripts -f {config} -c "program {ELF_PATH / ELF_FILE} verify reset exit"' 93 logger.write_debug_to_report(f"Using command: {command}") 94 return "Verified OK" in terminal_task(command)
Run the flash command.
def
test_flash_firmware():
97def test_flash_firmware(): 98 """ 99 | Requirement | Flash new firmware | 100 | :------------------- | :------------------------------------------------ | 101 | GitHub Issue | turnaroundfactor/HITL#216 | 102 | Instructions | 1. If a branch is provided, build and use it </br>\ 103 2. Activate the given ST-Link </br>\ 104 3. Flash using openocd </br>\ 105 4. Deactivate the given ST-Link | 106 | Pass / Fail Criteria | Flash succeeded | 107 | Estimated Duration | 30 seconds | 108 """ 109 global ELF_PATH 110 reset_cell_sims() 111 112 # Checkout branch 113 if BRANCH: 114 firmware_path = Path("../battery-benchtop-rev1") 115 if not firmware_path.is_dir(): 116 pytest.fail("Firmware repo is not installed. Cannot build ELF.") 117 118 fetch_cmd = f"git -C {firmware_path} fetch" 119 checkout_cmd = f"git -C {firmware_path} checkout {BRANCH}" 120 merge_cmd = f"git -C {firmware_path} merge" 121 build_cmake_cmd = ( 122 f"cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_C_COMPILER=/usr/bin/clang-11 " 123 f"-DCMAKE_CXX_COMPILER=/usr/bin/clang-cpp-11 -G Ninja {firmware_path} " 124 f"-B {firmware_path / 'cmake-build-debug'} -DBUILD={BUILD} -DCAPACITY={CAPACITY} " 125 f"-DHITL_DEBUG={HITL_DEBUG:d} -U CMAKE_C_COMPILER -U CMAKE_CXX_COMPILER" 126 ) 127 build_elf_cmd = f"cmake --build {firmware_path / 'cmake-build-debug'} --target all -- -j 8" 128 logger.write_info_to_report("Fetching remote branches") 129 terminal_task(fetch_cmd, check=False) 130 logger.write_info_to_report(f"Checking out branch {BRANCH}") 131 terminal_task(checkout_cmd, check=False) 132 logger.write_info_to_report("Merging changes") 133 terminal_task(merge_cmd, check=False) 134 logger.write_info_to_report("Building...") 135 terminal_task(build_cmake_cmd, check=False) 136 terminal_task(build_elf_cmd, check=False) 137 ELF_PATH = firmware_path / "cmake-build-debug" 138 139 # Confirm that the files exist 140 if not Path(ELF_PATH / ELF_FILE).is_file(): 141 raise FileNotFoundError(f"{ELF_PATH / ELF_FILE} does not exist.") 142 logger.write_info_to_report(f"ELF Filename: {ELF_PATH / ELF_FILE}") 143 144 # Create config file 145 assert hasattr(pytest, "flags") and isinstance(pytest.flags, BMSFlags) 146 serial_number = pytest.flags.config["st_link_serial"] 147 with tempfile.NamedTemporaryFile("w+", encoding="UTF-8") as temp_fp: 148 logger.write_info_to_report(f"Configuration Filename: {temp_fp.name}") 149 with open(CONFIG_TEMPLATE, encoding="UTF-8") as config_fp: 150 template = Template(config_fp.read()) 151 temp_fp.write(template.substitute({"serial_number": serial_number})) 152 temp_fp.flush() 153 temp_fp.seek(0) 154 logger.write_debug_to_report(f"Template contents:\n{temp_fp.read()}") 155 156 # Attempt to flash 157 logger.write_info_to_report("Flashing...") 158 time.sleep(3) 159 for attempt in range(MAX_ATTEMPTS): 160 if execute_flash(temp_fp.name): 161 break 162 logger.write_error_to_report(f"Flashing failed. Retrying {attempt + 1}/{MAX_ATTEMPTS}.") 163 time.sleep(3) 164 else: 165 raise RuntimeError(f"Flashing failed after {MAX_ATTEMPTS} attempts.")
| Requirement | Flash new firmware |
|---|---|
| GitHub Issue | turnaroundfactor/HITL#216 |
| Instructions | 1. If a branch is provided, build and use it 2. Activate the given ST-Link 3. Flash using openocd 4. Deactivate the given ST-Link |
| Pass / Fail Criteria | Flash succeeded |
| Estimated Duration | 30 seconds |